diff --git a/.buildkite/cache-builder.yml b/.buildkite/cache-builder.yml index 405b5c32eae3..0cf900094f06 100644 --- a/.buildkite/cache-builder.yml +++ b/.buildkite/cache-builder.yml @@ -14,7 +14,7 @@ common_params: # Common environment values to use with the `env` key. - &common_env # Be sure to also update the `.xcode-version` file when updating the Xcode image/version here - IMAGE_ID: xcode-14.3.1 + IMAGE_ID: xcode-15.1 steps: diff --git a/.buildkite/commands/checkout-release-branch.sh b/.buildkite/commands/checkout-release-branch.sh new file mode 100755 index 000000000000..394e18560bb9 --- /dev/null +++ b/.buildkite/commands/checkout-release-branch.sh @@ -0,0 +1,14 @@ +#!/bin/bash -eu + +RELEASE_NUMBER=$1 + +if [[ -z "${RELEASE_NUMBER}" ]]; then + echo "Usage $0 " + exit 1 +fi + +# Buildkite, by default, checks out a specific commit. +# For many release actions, we need to be on a release branch instead. +BRANCH_NAME="release/${RELEASE_NUMBER}" +git fetch origin "$BRANCH_NAME" +git checkout "$BRANCH_NAME" diff --git a/.buildkite/commands/complete-code-freeze.sh b/.buildkite/commands/complete-code-freeze.sh new file mode 100755 index 000000000000..94f6bc0366d6 --- /dev/null +++ b/.buildkite/commands/complete-code-freeze.sh @@ -0,0 +1,23 @@ +#!/bin/bash -eu + +RELEASE_NUMBER=$1 + +if [[ -z "${RELEASE_NUMBER}" ]]; then + echo "Usage $0 " + exit 1 +fi + +echo '--- :git: Configure Git for release management' +.buildkite/commands/configure-git-for-release-management.sh + +echo '--- :git: Checkout release branch' +.buildkite/commands/checkout-release-branch.sh "$RELEASE_NUMBER" + +echo '--- :ruby: Setup Ruby tools' +install_gems + +echo '--- :closed_lock_with_key: Access secrets' +bundle exec fastlane run configure_apply + +echo '--- :shipit: Complete code freeze' +bundle exec fastlane complete_code_freeze skip_confirm:true diff --git a/.buildkite/commands/configure-git-for-release-management.sh b/.buildkite/commands/configure-git-for-release-management.sh new file mode 100755 index 000000000000..8d9a770a236e --- /dev/null +++ b/.buildkite/commands/configure-git-for-release-management.sh @@ -0,0 +1,13 @@ +#!/bin/bash -eu + +# The Git command line client is not configured in Buildkite. +# At the moment, steps that need Git access can configure it on deman using this script. +# Later on, we should be able to configure it on the agent instead. + +curl -L https://api.github.com/meta | jq -r '.ssh_keys | .[]' | sed -e 's/^/github.com /' >> ~/.ssh/known_hosts +git config --global user.email "mobile+wpmobilebot@automattic.com" +git config --global user.name "Automattic Release Bot" + +# Buildkite is currently using the HTTPS URL to checkout. +# We need to override it to be able to use the deploy key. +git remote set-url origin git@github.com:wordpress-mobile/WordPress-iOS.git diff --git a/.buildkite/commands/danger-pr-check.sh b/.buildkite/commands/danger-pr-check.sh new file mode 100755 index 000000000000..0ad81cfede4c --- /dev/null +++ b/.buildkite/commands/danger-pr-check.sh @@ -0,0 +1,7 @@ +#!/bin/bash -eu + +echo "--- :rubygems: Setting up Gems" +bundle install + +echo "--- Running Danger: PR Check" +bundle exec danger --fail-on-errors=true --remove-previous-comments --danger_id=pr-check diff --git a/.buildkite/commands/finalize-release.sh b/.buildkite/commands/finalize-release.sh new file mode 100755 index 000000000000..2f3a7c7b78ce --- /dev/null +++ b/.buildkite/commands/finalize-release.sh @@ -0,0 +1,23 @@ +#!/bin/bash -eu + +RELEASE_NUMBER=$1 + +if [[ -z "${RELEASE_NUMBER}" ]]; then + echo "Usage $0 " + exit 1 +fi + +echo '--- :git: Configure Git for release management' +.buildkite/commands/configure-git-for-release-management.sh + +echo '--- :git: Checkout release branch' +.buildkite/commands/checkout-release-branch.sh "$RELEASE_NUMBER" + +echo '--- :ruby: Setup Ruby tools' +install_gems + +echo '--- :closed_lock_with_key: Access secrets' +bundle exec fastlane run configure_apply + +echo '--- :shipit: Finalize release' +bundle exec fastlane finalize_release skip_confirm:true diff --git a/.buildkite/commands/rubocop-via-danger.sh b/.buildkite/commands/rubocop-via-danger.sh deleted file mode 100755 index 45cac5f21ae2..000000000000 --- a/.buildkite/commands/rubocop-via-danger.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -eu - -echo "--- :rubygems: Setting up Gems" -install_gems - -echo "--- :rubocop: Run Rubocop via Danger" -bundle exec danger --fail-on-errors=true diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index ac9e016e4030..77c5649d306c 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -3,13 +3,10 @@ common_params: # Common plugin settings to use with the `plugins` key. - &common_plugins - automattic/a8c-ci-toolkit#2.18.1 - - automattic/git-s3-cache#1.1.4: - bucket: "a8c-repo-mirrors" - repo: "automattic/wordpress-ios/" # Common environment values to use with the `env` key. - &common_env # Be sure to also update the `.xcode-version` file when updating the Xcode image/version here - IMAGE_ID: xcode-14.3.1 + IMAGE_ID: xcode-15.1 # This is the default pipeline – it will build and test the app steps: @@ -80,7 +77,7 @@ steps: - group: "🔬 UI Tests" steps: - label: "🔬 :jetpack: UI Tests (iPhone)" - command: .buildkite/commands/run-ui-tests.sh 'iPhone SE (3rd generation)' + command: .buildkite/commands/run-ui-tests.sh 'iPhone 15' depends_on: "build_jetpack" env: *common_env plugins: *common_plugins @@ -92,7 +89,7 @@ steps: context: "UI Tests (iPhone)" - label: "🔬 :jetpack: UI Tests (iPad)" - command: .buildkite/commands/run-ui-tests.sh 'iPad Air (5th generation)' + command: .buildkite/commands/run-ui-tests.sh 'iPad Pro (12.9-inch) (6th generation)' depends_on: "build_jetpack" env: *common_env plugins: *common_plugins @@ -108,6 +105,20 @@ steps: ################# - group: "Linters" steps: + - label: "☢️ Danger - PR Check" + command: .buildkite/commands/danger-pr-check.sh + plugins: + - docker#v5.8.0: + image: "public.ecr.aws/docker/library/ruby:3.2.2" + propagate-environment: true + environment: + - "DANGER_GITHUB_API_TOKEN" + if: "build.pull_request.id != null" + agents: + queue: "default" + retry: + manual: + permit_on_passed: true - label: "🧹 Lint Translations" command: "gplint /workdir/WordPress/Resources/AppStoreStrings.po" plugins: @@ -118,13 +129,6 @@ steps: notify: - github_commit_status: context: "Lint Translations" - # This step uses Danger to run RuboCop, but it's "agnostic" about it. - # That is, it outwardly only mentions RuboCop, not Danger - - label: ":rubocop: Lint Ruby Tooling" - command: .buildkite/commands/rubocop-via-danger.sh - plugins: *common_plugins - agents: - queue: "android" - label: ":sleuth_or_spy: Lint Localized Strings Format" command: .buildkite/commands/lint-localized-strings-format.sh plugins: *common_plugins diff --git a/.buildkite/release-builds.yml b/.buildkite/release-builds.yml index 53447e466cdc..d1eeeb4fda69 100644 --- a/.buildkite/release-builds.yml +++ b/.buildkite/release-builds.yml @@ -11,7 +11,7 @@ common_params: # Common environment values to use with the `env` key. - &common_env # Be sure to also update the `.xcode-version` file when updating the Xcode image/version here - IMAGE_ID: xcode-14.3.1 + IMAGE_ID: xcode-15.1 steps: diff --git a/.buildkite/release-pipelines/code-freeze.yml b/.buildkite/release-pipelines/code-freeze.yml new file mode 100644 index 000000000000..0e11af8022c0 --- /dev/null +++ b/.buildkite/release-pipelines/code-freeze.yml @@ -0,0 +1,20 @@ +steps: + - label: Code Freeze + plugins: + - automattic/a8c-ci-toolkit#2.18.2 + # The first client to implement releases in CI was Android so the automation works in that queue. + # We might want to move it to a leaner one in the future. + agents: + queue: android + command: | + echo '--- :git: Configure Git for release management' + .buildkite/commands/configure-git-for-release-management.sh + + echo '--- :ruby: Setup Ruby tools' + install_gems + + echo '--- :closed_lock_with_key: Access secrets' + bundle exec fastlane run configure_apply + + echo '--- :shipit: Run code freeze' + bundle exec fastlane code_freeze skip_confirm:true diff --git a/.buildkite/release-pipelines/complete-code-freeze.yml b/.buildkite/release-pipelines/complete-code-freeze.yml new file mode 100644 index 000000000000..77fa242f1371 --- /dev/null +++ b/.buildkite/release-pipelines/complete-code-freeze.yml @@ -0,0 +1,10 @@ +steps: + - label: Complete Code Freeze + plugins: + - automattic/a8c-ci-toolkit#2.18.2 + # The code freeze completion needs to run on macOS because it uses genstrings under the hood + agents: + queue: mac + env: + IMAGE_ID: xcode-15.1 + command: ".buildkite/commands/complete-code-freeze.sh $RELEASE_VERSION" diff --git a/.buildkite/release-pipelines/finalize-release.yml b/.buildkite/release-pipelines/finalize-release.yml new file mode 100644 index 000000000000..4a94c6f2b517 --- /dev/null +++ b/.buildkite/release-pipelines/finalize-release.yml @@ -0,0 +1,10 @@ +steps: + - label: Finalize Release + plugins: + - automattic/a8c-ci-toolkit#2.18.2 + # The finalization needs to run on macOS because of localization linting + agents: + queue: mac + env: + IMAGE_ID: xcode-15.1 + command: ".buildkite/commands/finalize-release.sh $RELEASE_VERSION" diff --git a/.buildkite/release-pipelines/new-beta-release.yml b/.buildkite/release-pipelines/new-beta-release.yml new file mode 100644 index 000000000000..d93ed039b321 --- /dev/null +++ b/.buildkite/release-pipelines/new-beta-release.yml @@ -0,0 +1,21 @@ +steps: + - label: New Beta Deployment + plugins: + - automattic/a8c-ci-toolkit#2.18.2 + # The beta needs to run on macOS because it uses genstrings under the hood + agents: + queue: mac + env: + IMAGE_ID: xcode-15.1 + command: | + echo '--- :git: Configure Git for release management' + .buildkite/commands/configure-git-for-release-management.sh + + echo '--- :ruby: Setup Ruby tools' + install_gems + + echo '--- :closed_lock_with_key: Access secrets' + bundle exec fastlane run configure_apply + + echo '--- :shipit: Deploy new beta' + bundle exec fastlane new_beta_release skip_confirm:true diff --git a/.buildkite/release-pipelines/update-app-store-strings.yml b/.buildkite/release-pipelines/update-app-store-strings.yml new file mode 100644 index 000000000000..4d4a898d7c7e --- /dev/null +++ b/.buildkite/release-pipelines/update-app-store-strings.yml @@ -0,0 +1,20 @@ +steps: + - label: Update App Store Strings + plugins: + - automattic/a8c-ci-toolkit#2.18.2 + # The first client to implement releases in CI was Android so the automation works in that queue. + # We might want to move it to a leaner one in the future. + agents: + queue: android + command: | + echo '--- :git: Configure Git for release management' + .buildkite/commands/configure-git-for-release-management.sh + + echo '--- :ruby: Setup Ruby tools' + install_gems + + echo '--- :closed_lock_with_key: Access secrets' + bundle exec fastlane run configure_apply + + echo '--- :shipit: Update relaese notes and other App Store metadata' + bundle exec fastlane update_appstore_strings skip_confirm:true diff --git a/.bundle/config b/.bundle/config index 59c8ccc55a7d..ae257f93d28f 100644 --- a/.bundle/config +++ b/.bundle/config @@ -3,3 +3,4 @@ BUNDLE_PATH: "vendor/bundle" BUNDLE_JOBS: "3" BUNDLE_WITHOUT: "screenshots" BUNDLE_RETRY: "3" +BUNDLE_SPECIFIC_PLATFORM: "false" diff --git a/.xcode-version b/.xcode-version index 6dfe8b1298c0..adbc6d2b1bde 100644 --- a/.xcode-version +++ b/.xcode-version @@ -1 +1 @@ -14.3.1 +15.1 diff --git a/API-Mocks/WordPressMocks/src/main/assets/mocks/mappings/wpcom/dashboard/106707880 - Tri-County Real Estate/sites_106707880_dashboard.json b/API-Mocks/WordPressMocks/src/main/assets/mocks/mappings/wpcom/dashboard/106707880 - Tri-County Real Estate/sites_106707880_dashboard.json index a785ceb938f3..4e92085741ec 100644 --- a/API-Mocks/WordPressMocks/src/main/assets/mocks/mappings/wpcom/dashboard/106707880 - Tri-County Real Estate/sites_106707880_dashboard.json +++ b/API-Mocks/WordPressMocks/src/main/assets/mocks/mappings/wpcom/dashboard/106707880 - Tri-County Real Estate/sites_106707880_dashboard.json @@ -22,7 +22,16 @@ }, "posts": { "has_published": true, - "draft": [], + "draft": [ + { + "id": 52, + "title": "Draft", + "content": "A draft post • • • • • •", + "status": "draft", + "modified": "2023-06-18 10:33:38", + "date": "2023-06-18 10:33:38" + } + ], "scheduled": [] }, "pages": [ diff --git a/API-Mocks/WordPressMocks/src/main/assets/mocks/mappings/wpcom/pages/sites_181851495_pages_after.json b/API-Mocks/WordPressMocks/src/main/assets/mocks/mappings/wpcom/pages/sites_181851495_pages_after.json index bd32bee85ba7..d3483a33c908 100644 --- a/API-Mocks/WordPressMocks/src/main/assets/mocks/mappings/wpcom/pages/sites_181851495_pages_after.json +++ b/API-Mocks/WordPressMocks/src/main/assets/mocks/mappings/wpcom/pages/sites_181851495_pages_after.json @@ -36,7 +36,7 @@ "profile_URL": "http://en.gravatar.com/e2eflowtestingmobile", "site_ID": 1 }, - "date": "{{#assign 'customformat'}}yyyy-MM-dd'T'HH:mm:ss{{/assign}}{{now format=customformat}}", + "date": "{{#assign 'customformat'}}yyyy-MM-dd'T'HH:mm:ssZ{{/assign}}{{now format=customformat}}", "modified": "{{now format=customformat}}", "title": "New Blank Page", "URL": "https://infocusphotographers.com/new-blank-page/", diff --git a/API-Mocks/WordPressMocks/src/main/assets/mocks/mappings/wpcom/sites/Shared/wpcom_sites_blogging_prompts.json b/API-Mocks/WordPressMocks/src/main/assets/mocks/mappings/wpcom/sites/Shared/wpcom_sites_blogging_prompts.json index df70e16ba39c..3fbe0123d741 100644 --- a/API-Mocks/WordPressMocks/src/main/assets/mocks/mappings/wpcom/sites/Shared/wpcom_sites_blogging_prompts.json +++ b/API-Mocks/WordPressMocks/src/main/assets/mocks/mappings/wpcom/sites/Shared/wpcom_sites_blogging_prompts.json @@ -1,12 +1,15 @@ { "request": { "method": "GET", - "urlPathPattern": "/wpcom/v2/sites/([0-9]+)/blogging-prompts", + "urlPathPattern": "/wpcom/v3/sites/([0-9]+)/blogging-prompts", "queryParameters": { - "number": { + "per_page": { "matches": "(.*)" }, - "from": { + "after": { + "matches": "(.*)" + }, + "force_year": { "matches": "(.*)" }, "_locale": { @@ -16,239 +19,237 @@ }, "response": { "status": 200, - "jsonBody": { - "prompts": [ - { - "id": 2003, - "text": "What foods would you like to make?", - "title": "Prompt number 200", - "content": "\n

What foods would you like to make?

\n", - "attribution": "dayone", - "date": "2022-07-20", - "answered": false, - "answered_users_count": 3, - "answered_users_sample": [ - { - "avatar": "https://2.gravatar.com/avatar/5dffa9e5d426449127114fe04f4820ba?s=96&d=identicon&r=G" - }, - { - "avatar": "https://0.gravatar.com/avatar/9cd92ae622fc905b4908503cf9fbd489?s=96&d=identicon&r=G" - }, - { - "avatar": "https://2.gravatar.com/avatar/e2526cb456f69198425ebeaff0a9225f?s=96&d=identicon&r=G" - } - ] - }, - { - "id": 2004, - "text": "What's your favorite game (card, board, video, etc.)? Why?", - "title": "Prompt number 201", - "content": "\n

What's your favorite game (card, board, video, etc.)? Why?

\n", - "attribution": "", - "date": "2022-07-21", - "answered": false, - "answered_users_count": 0, - "answered_users_sample": [] - }, - { - "id": 2005, - "text": "What’s your go-to comfort food?", - "title": "Prompt number 202", - "content": "\n

What’s your go-to comfort food?

\n", - "attribution": "dayone", - "date": "2022-07-22", - "answered": false, - "answered_users_count": 0, - "answered_users_sample": [] - }, - { - "id": 2006, - "text": "What do you listen to while you work?", - "title": "Prompt number 203", - "content": "\n

What do you listen to while you work?

\n", - "attribution": "dayone", - "date": "2022-07-23", - "answered": false, - "answered_users_count": 0, - "answered_users_sample": [] - }, - { - "id": 2007, - "text": "What would you change about modern society?", - "title": "Prompt number 204", - "content": "\n

What would you change about modern society?

\n", - "attribution": "dayone", - "date": "2022-07-24", - "answered": false, - "answered_users_count": 0, - "answered_users_sample": [] - }, - { - "id": 2008, - "text": "What are your future travel plans?", - "title": "Prompt number 205", - "content": "\n

What are your future travel plans?

\n", - "attribution": "dayone", - "date": "2022-07-25", - "answered": false, - "answered_users_count": 0, - "answered_users_sample": [] - }, - { - "id": 2009, - "text": "What's the sickest you've ever been?", - "title": "Prompt number 206", - "content": "\n

What's the sickest you've ever been?

\n", - "attribution": "dayone", - "date": "2022-07-26", - "answered": false, - "answered_users_count": 0, - "answered_users_sample": [] - }, - { - "id": 2010, - "text": "What's the story behind your nickname?", - "title": "Prompt number 207", - "content": "\n

What's the story behind your nickname?

\n", - "attribution": "dayone", - "date": "2022-07-27", - "answered": false, - "answered_users_count": 0, - "answered_users_sample": [] - }, - { - "id": 2011, - "text": "If you won two free plane tickets, where would you go?", - "title": "Prompt number 208", - "content": "\n

If you won two free plane tickets, where would you go?

\n", - "attribution": "", - "date": "2022-07-28", - "answered": false, - "answered_users_count": 0, - "answered_users_sample": [] - }, - { - "id": 2012, - "text": "If you could bring back one dinosaur, which one would it be?", - "title": "Prompt number 209", - "content": "\n

If you could bring back one dinosaur, which one would it be?

\n", - "attribution": "dayone", - "date": "2022-07-29", - "answered": false, - "answered_users_count": 0, - "answered_users_sample": [] - }, - { - "id": 2013, - "text": "How would you describe yourself to someone?", - "title": "Prompt number 210", - "content": "\n

How would you describe yourself to someone?

\n", - "attribution": "dayone", - "date": "2022-07-30", - "answered": false, - "answered_users_count": 0, - "answered_users_sample": [] - }, - { - "id": 2014, - "text": "Was today typical?", - "title": "Prompt number 211", - "content": "\n

Was today typical?

\n", - "attribution": "dayone", - "date": "2022-07-31", - "answered": false, - "answered_users_count": 0, - "answered_users_sample": [] - }, - { - "id": 2015, - "text": "What traditions have you not kept that your parents had?", - "title": "Prompt number 212", - "content": "\n

What traditions have you not kept that your parents had?

\n", - "attribution": "dayone", - "date": "2022-08-01", - "answered": false, - "answered_users_count": 0, - "answered_users_sample": [] - }, - { - "id": 2016, - "text": "How would you describe yourself to someone who can't see you?", - "title": "Prompt number 213", - "content": "\n

How would you describe yourself to someone who can't see you?

\n", - "attribution": "dayone", - "date": "2022-08-02", - "answered": false, - "answered_users_count": 0, - "answered_users_sample": [] - }, - { - "id": 2017, - "text": "Write about a random act of kindness you've done for someone.", - "title": "Prompt number 214", - "content": "\n

Write about a random act of kindness you've done for someone.

\n", - "attribution": "dayone", - "date": "2022-08-03", - "answered": false, - "answered_users_count": 0, - "answered_users_sample": [] - }, - { - "id": 2018, - "text": "What are you curious about?", - "title": "Prompt number 215", - "content": "\n

What are you curious about?

\n", - "attribution": "dayone", - "date": "2022-08-04", - "answered": false, - "answered_users_count": 0, - "answered_users_sample": [] - }, - { - "id": 2019, - "text": "Do you have any habits you're currently trying to break?", - "title": "Prompt number 216", - "content": "\n

Do you have any habits you're currently trying to break?

\n", - "attribution": "dayone", - "date": "2022-08-05", - "answered": false, - "answered_users_count": 0, - "answered_users_sample": [] - }, - { - "id": 2020, - "text": "List 30 things that make you happy.", - "title": "Prompt number 217", - "content": "\n

List 30 things that make you happy.

\n", - "attribution": "dayone", - "date": "2022-08-06", - "answered": false, - "answered_users_count": 0, - "answered_users_sample": [] - }, - { - "id": 2021, - "text": "Scour the news for an entirely uninteresting story. Consider how it connects to your life. Write about that.", - "title": "Prompt number 218", - "content": "\n

Scour the news for an entirely uninteresting story. Consider how it connects to your life. Write about that.

\n", - "attribution": "", - "date": "2022-08-07", - "answered": false, - "answered_users_count": 0, - "answered_users_sample": [] - }, - { - "id": 2022, - "text": "What's the most money you've ever spent on a meal? Was it worth it?", - "title": "Prompt number 219", - "content": "\n

What's the most money you've ever spent on a meal? Was it worth it?

\n", - "attribution": "", - "date": "{{now format='yyyy-MM-dd'}}", - "answered": false, - "answered_users_count": 0, - "answered_users_sample": [] - } - ] - } + "jsonBody": [ + { + "id": 2003, + "text": "What foods would you like to make?", + "attribution": "dayone", + "date": "2022-07-20", + "answered": false, + "answered_users_count": 3, + "answered_users_sample": [ + { + "avatar": "https://2.gravatar.com/avatar/5dffa9e5d426449127114fe04f4820ba?s=96&d=identicon&r=G" + }, + { + "avatar": "https://0.gravatar.com/avatar/9cd92ae622fc905b4908503cf9fbd489?s=96&d=identicon&r=G" + }, + { + "avatar": "https://2.gravatar.com/avatar/e2526cb456f69198425ebeaff0a9225f?s=96&d=identicon&r=G" + } + ], + "answered_link": "https://wordpress.com/tag/dailyprompt-2003", + "answered_link_text": "View all responses" + }, + { + "id": 2004, + "text": "What's your favorite game (card, board, video, etc.)? Why?", + "attribution": "", + "date": "2022-07-21", + "answered": false, + "answered_users_count": 0, + "answered_users_sample": [], + "answered_link": "https://wordpress.com/tag/dailyprompt-2004", + "answered_link_text": "View all responses" + }, + { + "id": 2005, + "text": "What’s your go-to comfort food?", + "attribution": "dayone", + "date": "2022-07-22", + "answered": false, + "answered_users_count": 0, + "answered_users_sample": [], + "answered_link": "https://wordpress.com/tag/dailyprompt-2005", + "answered_link_text": "View all responses" + }, + { + "id": 2006, + "text": "What do you listen to while you work?", + "attribution": "dayone", + "date": "2022-07-23", + "answered": false, + "answered_users_count": 0, + "answered_users_sample": [], + "answered_link": "https://wordpress.com/tag/dailyprompt-2006", + "answered_link_text": "View all responses" + }, + { + "id": 2007, + "text": "What would you change about modern society?", + "attribution": "dayone", + "date": "2022-07-24", + "answered": false, + "answered_users_count": 0, + "answered_users_sample": [], + "answered_link": "https://wordpress.com/tag/dailyprompt-2007", + "answered_link_text": "View all responses" + }, + { + "id": 2008, + "text": "What are your future travel plans?", + "attribution": "dayone", + "date": "2022-07-25", + "answered": false, + "answered_users_count": 0, + "answered_users_sample": [], + "answered_link": "https://wordpress.com/tag/dailyprompt-2008", + "answered_link_text": "View all responses" + }, + { + "id": 2009, + "text": "What's the sickest you've ever been?", + "attribution": "dayone", + "date": "2022-07-26", + "answered": false, + "answered_users_count": 0, + "answered_users_sample": [], + "answered_link": "https://wordpress.com/tag/dailyprompt-2009", + "answered_link_text": "View all responses" + }, + { + "id": 2010, + "text": "What's the story behind your nickname?", + "attribution": "dayone", + "date": "2022-07-27", + "answered": false, + "answered_users_count": 0, + "answered_users_sample": [], + "answered_link": "https://wordpress.com/tag/dailyprompt-2010", + "answered_link_text": "View all responses" + }, + { + "id": 2011, + "text": "If you won two free plane tickets, where would you go?", + "attribution": "", + "date": "2022-07-28", + "answered": false, + "answered_users_count": 0, + "answered_users_sample": [], + "answered_link": "https://wordpress.com/tag/dailyprompt-2011", + "answered_link_text": "View all responses" + }, + { + "id": 2012, + "text": "If you could bring back one dinosaur, which one would it be?", + "attribution": "dayone", + "date": "2022-07-29", + "answered": false, + "answered_users_count": 0, + "answered_users_sample": [], + "answered_link": "https://wordpress.com/tag/dailyprompt-2012", + "answered_link_text": "View all responses" + }, + { + "id": 2013, + "text": "How would you describe yourself to someone?", + "attribution": "dayone", + "date": "2022-07-30", + "answered": false, + "answered_users_count": 0, + "answered_users_sample": [], + "answered_link": "https://wordpress.com/tag/dailyprompt-2013", + "answered_link_text": "View all responses" + }, + { + "id": 2014, + "text": "Was today typical?", + "attribution": "dayone", + "date": "2022-07-31", + "answered": false, + "answered_users_count": 0, + "answered_users_sample": [], + "answered_link": "https://wordpress.com/tag/dailyprompt-2014", + "answered_link_text": "View all responses" + }, + { + "id": 2015, + "text": "What traditions have you not kept that your parents had?", + "attribution": "dayone", + "date": "2022-08-01", + "answered": false, + "answered_users_count": 0, + "answered_users_sample": [], + "answered_link": "https://wordpress.com/tag/dailyprompt-2015", + "answered_link_text": "View all responses" + }, + { + "id": 2016, + "text": "How would you describe yourself to someone who can't see you?", + "attribution": "dayone", + "date": "2022-08-02", + "answered": false, + "answered_users_count": 0, + "answered_users_sample": [], + "answered_link": "https://wordpress.com/tag/dailyprompt-2016", + "answered_link_text": "View all responses" + }, + { + "id": 2017, + "text": "Write about a random act of kindness you've done for someone.", + "attribution": "dayone", + "date": "2022-08-03", + "answered": false, + "answered_users_count": 0, + "answered_users_sample": [], + "answered_link": "https://wordpress.com/tag/dailyprompt-2017", + "answered_link_text": "View all responses" + }, + { + "id": 2018, + "text": "What are you curious about?", + "attribution": "dayone", + "date": "2022-08-04", + "answered": false, + "answered_users_count": 0, + "answered_users_sample": [], + "answered_link": "https://wordpress.com/tag/dailyprompt-2018", + "answered_link_text": "View all responses" + }, + { + "id": 2019, + "text": "Do you have any habits you're currently trying to break?", + "attribution": "dayone", + "date": "2022-08-05", + "answered": false, + "answered_users_count": 0, + "answered_users_sample": [], + "answered_link": "https://wordpress.com/tag/dailyprompt-2019", + "answered_link_text": "View all responses" + }, + { + "id": 2020, + "text": "List 30 things that make you happy.", + "attribution": "dayone", + "date": "2022-08-06", + "answered": false, + "answered_users_count": 0, + "answered_users_sample": [], + "answered_link": "https://wordpress.com/tag/dailyprompt-2020", + "answered_link_text": "View all responses" + }, + { + "id": 2021, + "text": "Scour the news for an entirely uninteresting story. Consider how it connects to your life. Write about that.", + "attribution": "", + "date": "2022-08-07", + "answered": false, + "answered_users_count": 0, + "answered_users_sample": [], + "answered_link": "https://wordpress.com/tag/dailyprompt-2021", + "answered_link_text": "View all responses" + }, + { + "id": 2022, + "text": "What's the most money you've ever spent on a meal? Was it worth it?", + "attribution": "", + "date": "{{now format='yyyy-MM-dd'}}", + "answered": false, + "answered_users_count": 0, + "answered_users_sample": [], + "answered_link": "https://wordpress.com/tag/dailyprompt-2022", + "answered_link_text": "View all responses" + } + ] } } diff --git a/API-Mocks/WordPressMocks/src/main/assets/mocks/mappings/wpcom/stats/stats_visits-year.json b/API-Mocks/WordPressMocks/src/main/assets/mocks/mappings/wpcom/stats/stats_visits-year.json index 44426144b0c2..2da5f0249f3c 100644 --- a/API-Mocks/WordPressMocks/src/main/assets/mocks/mappings/wpcom/stats/stats_visits-year.json +++ b/API-Mocks/WordPressMocks/src/main/assets/mocks/mappings/wpcom/stats/stats_visits-year.json @@ -20,7 +20,7 @@ "response": { "status": 200, "jsonBody": { - "date": "2019-07-16", + "date": "{{now format='yyyy-MM-dd'}}", "unit": "year", "fields": [ "period", @@ -33,7 +33,7 @@ ], "data": [ [ - "2005-01-01", + "{{now offset='-7 years' format='yyyy-MM-dd'}}", 0, 0, 0, @@ -42,7 +42,7 @@ 0 ], [ - "2006-01-01", + "{{now offset='-6 years' format='yyyy-MM-dd'}}", 0, 0, 0, @@ -51,7 +51,7 @@ 0 ], [ - "2007-01-01", + "{{now offset='-5 years' format='yyyy-MM-dd'}}", 0, 0, 0, @@ -60,7 +60,7 @@ 0 ], [ - "2008-01-01", + "{{now offset='-4 years' format='yyyy-MM-dd'}}", 0, 0, 0, @@ -69,70 +69,7 @@ 0 ], [ - "2009-01-01", - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - "2010-01-01", - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - "2011-01-01", - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - "2012-01-01", - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - "2013-01-01", - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - "2014-01-01", - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - "2015-01-01", - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - "2016-01-01", + "{{now offset='-3 years' format='yyyy-MM-dd'}}", 48, 12, 0, @@ -141,7 +78,7 @@ 13 ], [ - "2017-01-01", + "{{now offset='-2 years' format='yyyy-MM-dd'}}", 788, 465, 0, @@ -150,7 +87,7 @@ 3 ], [ - "2018-01-01", + "{{now offset='-1 years' format='yyyy-MM-dd'}}", 1215, 632, 0, @@ -159,7 +96,7 @@ 3 ], [ - "2019-01-01", + "{{now format='yyyy-MM-dd'}}", 9148, 4216, 1351, diff --git a/Dangerfile b/Dangerfile index 8ae75506c404..44585e4c3012 100644 --- a/Dangerfile +++ b/Dangerfile @@ -1,4 +1,37 @@ # frozen_string_literal: true +def release_branch? + danger.github.branch_for_base.start_with?('release/') || danger.github.branch_for_base.start_with?('hotfix/') +end + +def main_branch? + danger.github.branch_for_base == 'trunk' +end + +def wip_feature? + has_wip_label = github.pr_labels.any? { |label| label.include?('WIP') } + has_wip_title = github.pr_title.include?('WIP') + + has_wip_label || has_wip_title +end + +return if github.pr_labels.include?('Releases') + github.dismiss_out_of_range_messages -rubocop.lint inline_comment: true, fail_on_inline_comment: true + +manifest_pr_checker.check_all_manifest_lock_updated + +labels_checker.check( + do_not_merge_labels: ['[Status] DO NOT MERGE'], + required_labels: [//], + required_labels_error: 'PR requires at least one label.' +) + +view_changes_need_screenshots.view_changes_need_screenshots + +pr_size_checker.check_diff_size + +# skip check for draft PRs and for WIP features unless the PR is against the main branch or release branch +milestone_checker.check_milestone_due_date(days_before_due: 4) unless github.pr_draft? || (wip_feature? && !(release_branch? || main_branch?)) + +rubocop.lint(inline_comment: true, fail_on_inline_comment: true, include_cop_names: true) diff --git a/Gemfile b/Gemfile index 5dac5a796eed..8a11ae824018 100644 --- a/Gemfile +++ b/Gemfile @@ -2,13 +2,12 @@ source 'https://rubygems.org' -# 1.12.x and higher, starting from 1.12.1, because that hotfix fixes Xcode 14.3 compatibility -gem 'cocoapods', '~> 1.12', '>= 1.12.1' +gem 'cocoapods', '~> 1.14' gem 'commonmarker' -gem 'danger', '~> 9.3' -gem 'danger-rubocop', '~> 0.10' +gem 'danger-dangermattic', git: 'https://github.com/Automattic/dangermattic' gem 'dotenv' -gem 'fastlane', '~> 2.174' +# 2.217.0 includes a fix for Xcode 15 test results parsing in CI +gem 'fastlane', '~> 2.217' gem 'fastlane-plugin-appcenter', '~> 2.1' gem 'fastlane-plugin-sentry' # This comment avoids typing to switch to a development version for testing. @@ -17,7 +16,7 @@ gem 'fastlane-plugin-sentry' # waiting for the fix to be shipped. # gem 'fastlane-plugin-wpmreleasetoolkit', git: 'git@github.com:wordpress-mobile/release-toolkit', branch: 'mokagio/auto-retry-on-strings-glotpress-429' # -gem 'fastlane-plugin-wpmreleasetoolkit', '~> 9.0' +gem 'fastlane-plugin-wpmreleasetoolkit', '~> 9.1' gem 'rake' gem 'rubocop', '~> 1.30' gem 'rubocop-rake', '~> 0.6' diff --git a/Gemfile.lock b/Gemfile.lock index c2993246632d..6a16ac1d447c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,16 @@ +GIT + remote: https://github.com/Automattic/dangermattic + revision: 06a54db4f546d20c0465e4d144049d061a2a1e20 + specs: + danger-dangermattic (0.0.1) + danger (~> 9.3) + danger-junit (~> 1.0) + danger-plugin-api (~> 1.0) + danger-rubocop (~> 0.11) + danger-swiftlint (~> 0.29) + danger-xcode_summary (~> 1.0) + rubocop (~> 1.56) + GEM remote: https://rubygems.org/ specs: @@ -8,7 +21,7 @@ GEM i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) - addressable (2.8.5) + addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) algoliasearch (1.27.5) httpclient (~> 2.8, >= 2.8.3) @@ -16,21 +29,21 @@ GEM artifactory (3.0.15) ast (2.4.2) atomos (0.1.3) - aws-eventstream (1.2.0) - aws-partitions (1.830.0) - aws-sdk-core (3.184.0) - aws-eventstream (~> 1, >= 1.0.2) + aws-eventstream (1.3.0) + aws-partitions (1.865.0) + aws-sdk-core (3.190.0) + aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) - aws-sigv4 (~> 1.5) + aws-sigv4 (~> 1.8) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.72.0) - aws-sdk-core (~> 3, >= 3.184.0) + aws-sdk-kms (1.74.0) + aws-sdk-core (~> 3, >= 3.188.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.136.0) - aws-sdk-core (~> 3, >= 3.181.0) + aws-sdk-s3 (1.141.0) + aws-sdk-core (~> 3, >= 3.189.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.6) - aws-sigv4 (1.6.0) + aws-sigv4 (~> 1.8) + aws-sigv4 (1.8.0) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) buildkit (1.5.0) @@ -41,12 +54,12 @@ GEM cork nap open4 (~> 1.3) - cocoapods (1.12.1) + cocoapods (1.14.2) addressable (~> 2.8) claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.12.1) + cocoapods-core (= 1.14.2) cocoapods-deintegrate (>= 1.0.3, < 2.0) - cocoapods-downloader (>= 1.6.0, < 2.0) + cocoapods-downloader (>= 2.0) cocoapods-plugins (>= 1.0.0, < 2.0) cocoapods-search (>= 1.0.0, < 2.0) cocoapods-trunk (>= 1.6.0, < 2.0) @@ -58,8 +71,8 @@ GEM molinillo (~> 0.8.0) nap (~> 1.0) ruby-macho (>= 2.3.0, < 3.0) - xcodeproj (>= 1.21.0, < 2.0) - cocoapods-core (1.12.1) + xcodeproj (>= 1.23.0, < 2.0) + cocoapods-core (1.14.2) activesupport (>= 5.0, < 8) addressable (~> 2.8) algoliasearch (~> 1.0) @@ -70,7 +83,7 @@ GEM public_suffix (~> 4.0) typhoeus (~> 1.0) cocoapods-deintegrate (1.0.5) - cocoapods-downloader (1.6.3) + cocoapods-downloader (2.0) cocoapods-plugins (1.0.0) nap cocoapods-search (1.0.1) @@ -86,7 +99,7 @@ GEM concurrent-ruby (1.2.2) cork (0.3.0) colored2 (~> 3.1) - danger (9.3.1) + danger (9.3.2) claide (~> 1.0) claide-plugins (>= 0.9.2) colored2 (~> 3.1) @@ -99,21 +112,32 @@ GEM no_proxy_fix octokit (~> 6.0) terminal-table (>= 1, < 4) - danger-rubocop (0.10.0) + danger-junit (1.0.2) + danger (> 2.0) + ox (~> 2.0) + danger-plugin-api (1.0.0) + danger (> 2.0) + danger-rubocop (0.12.0) danger rubocop (~> 1.0) + danger-swiftlint (0.33.0) + danger + rake (> 10) + thor (~> 0.19) + danger-xcode_summary (1.2.0) + danger-plugin-api (~> 1.0) + xcresult (~> 0.2) declarative (0.0.20) diffy (3.4.2) digest-crc (0.6.5) rake (>= 12.0.0, < 14.0.0) - domain_name (0.5.20190701) - unf (>= 0.0.5, < 1.0.0) + domain_name (0.6.20231109) dotenv (2.8.1) emoji_regex (3.2.3) escape (0.0.4) ethon (0.16.0) ffi (>= 1.15.0) - excon (0.104.0) + excon (0.105.0) faraday (1.10.3) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) @@ -145,7 +169,7 @@ GEM faraday_middleware (1.2.0) faraday (~> 1.0) fastimage (2.2.7) - fastlane (2.216.0) + fastlane (2.217.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -188,7 +212,7 @@ GEM fastlane-plugin-appcenter (2.1.1) fastlane-plugin-sentry (1.15.0) os (~> 1.1, >= 1.1.4) - fastlane-plugin-wpmreleasetoolkit (9.1.0) + fastlane-plugin-wpmreleasetoolkit (9.2.0) activesupport (>= 6.1.7.1) buildkit (~> 1.5) chroma (= 0.2.0) @@ -205,16 +229,16 @@ GEM rake (>= 12.3, < 14.0) rake-compiler (~> 1.0) xcodeproj (~> 1.22) - ffi (1.15.5) + ffi (1.16.3) fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) git (1.18.0) addressable (~> 2.8) rchardet (~> 1.8) - google-apis-androidpublisher_v3 (0.50.0) + google-apis-androidpublisher_v3 (0.53.0) google-apis-core (>= 0.11.0, < 2.a) - google-apis-core (0.11.1) + google-apis-core (0.11.2) addressable (~> 2.5, >= 2.5.1) googleauth (>= 0.16.2, < 2.a) httpclient (>= 2.8.1, < 3.a) @@ -227,24 +251,25 @@ GEM google-apis-core (>= 0.11.0, < 2.a) google-apis-playcustomapp_v1 (0.13.0) google-apis-core (>= 0.11.0, < 2.a) - google-apis-storage_v1 (0.19.0) - google-apis-core (>= 0.9.0, < 2.a) - google-cloud-core (1.6.0) - google-cloud-env (~> 1.0) + google-apis-storage_v1 (0.29.0) + google-apis-core (>= 0.11.0, < 2.a) + google-cloud-core (1.6.1) + google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) - google-cloud-env (1.6.0) - faraday (>= 0.17.3, < 3.0) + google-cloud-env (2.1.0) + faraday (>= 1.0, < 3.a) google-cloud-errors (1.3.1) - google-cloud-storage (1.44.0) + google-cloud-storage (1.45.0) addressable (~> 2.8) digest-crc (~> 0.4) google-apis-iamcredentials_v1 (~> 0.1) - google-apis-storage_v1 (~> 0.19.0) + google-apis-storage_v1 (~> 0.29.0) google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) - googleauth (1.8.1) - faraday (>= 0.17.3, < 3.a) + googleauth (1.9.1) + faraday (>= 1.0, < 3.a) + google-cloud-env (~> 2.1) jwt (>= 1.4, < 3.0) multi_json (~> 1.11) os (>= 0.9, < 2.0) @@ -257,15 +282,16 @@ GEM concurrent-ruby (~> 1.0) java-properties (0.3.0) jmespath (1.6.2) - json (2.6.3) + json (2.7.1) jwt (2.7.1) kramdown (2.4.0) rexml kramdown-parser-gfm (1.1.0) kramdown (~> 2.0) + language_server-protocol (3.17.0.3) mini_magick (4.12.0) mini_mime (1.1.5) - mini_portile2 (2.8.4) + mini_portile2 (2.8.5) minitest (5.20.0) molinillo (0.8.0) multi_json (1.15.0) @@ -285,9 +311,11 @@ GEM options (2.3.2) optparse (0.1.1) os (1.1.4) + ox (2.14.17) parallel (1.23.0) - parser (3.2.2.1) + parser (3.2.2.4) ast (~> 2.4.1) + racc plist (3.7.0) progress_bar (1.3.3) highline (>= 1.6, < 3) @@ -295,7 +323,7 @@ GEM public_suffix (4.0.7) racc (1.7.1) rainbow (3.1.1) - rake (13.0.6) + rake (13.1.0) rake-compiler (1.2.5) rake rchardet (1.8.0) @@ -308,14 +336,15 @@ GEM rexml (3.2.6) rmagick (3.2.0) rouge (2.0.7) - rubocop (1.51.0) + rubocop (1.57.2) json (~> 2.3) + language_server-protocol (>= 3.17.0) parallel (~> 1.10) - parser (>= 3.2.0.0) + parser (>= 3.2.2.4) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.28.0, < 2.0) + rubocop-ast (>= 1.28.1, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) rubocop-ast (1.28.1) @@ -341,6 +370,7 @@ GEM terminal-notifier (2.0.0) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) + thor (0.20.3) trailblazer-option (0.1.2) tty-cursor (0.7.1) tty-screen (0.8.1) @@ -351,10 +381,7 @@ GEM tzinfo (2.0.6) concurrent-ruby (~> 1.0) uber (0.1.0) - unf (0.1.4) - unf_ext - unf_ext (0.0.8.2) - unicode-display_width (2.4.2) + unicode-display_width (2.5.0) webrick (1.8.1) word_wrap (1.0.0) xcodeproj (1.23.0) @@ -368,20 +395,20 @@ GEM rouge (~> 2.0.7) xcpretty-travis-formatter (1.0.1) xcpretty (~> 0.2, >= 0.0.7) + xcresult (0.2.1) PLATFORMS ruby DEPENDENCIES - cocoapods (~> 1.12, >= 1.12.1) + cocoapods (~> 1.14) commonmarker - danger (~> 9.3) - danger-rubocop (~> 0.10) + danger-dangermattic! dotenv - fastlane (~> 2.174) + fastlane (~> 2.217) fastlane-plugin-appcenter (~> 2.1) fastlane-plugin-sentry - fastlane-plugin-wpmreleasetoolkit (~> 9.0) + fastlane-plugin-wpmreleasetoolkit (~> 9.1) rake rmagick (~> 3.2.0) rubocop (~> 1.30) @@ -389,4 +416,4 @@ DEPENDENCIES xcpretty-travis-formatter BUNDLED WITH - 2.3.23 + 2.4.22 diff --git a/Gutenberg/cocoapods_helpers.rb b/Gutenberg/cocoapods_helpers.rb index 6e43dd4cf5b5..3a038b2e7030 100644 --- a/Gutenberg/cocoapods_helpers.rb +++ b/Gutenberg/cocoapods_helpers.rb @@ -40,7 +40,7 @@ def gutenberg_pod raise "Could not find config YAML at path #{GUTENBERG_CONFIG_PATH}" unless File.exist?(GUTENBERG_CONFIG_PATH) - config = YAML.safe_load(File.read(GUTENBERG_CONFIG_PATH), symbolize_names: true) + config = YAML.safe_load_file(GUTENBERG_CONFIG_PATH, symbolize_names: true) raise 'Gutenberg config does not contain expected key :ref' if config[:ref].nil? @@ -126,6 +126,13 @@ def gutenberg_post_install(installer:) react_native_post_install(installer, react_native_path) end +def gutenberg_post_integrate + return unless should_use_local_gutenberg + + # If the this workaround runs in the post_install step, the changes it makes get overridden somehow. + workaround_broken_search_paths +end + private def should_use_local_gutenberg @@ -172,3 +179,27 @@ def react_native_version!(gutenberg_path:) package_json_version.split('.').map(&:to_i) end + +# A workaround for the issue described at +# https://github.com/wordpress-mobile/WordPress-iOS/pull/21504#issuecomment-1789466523 +# +# For some yet-to-discover reason, something in the process installing the pods +# using local sources messes up the LIBRARY_SEARCH_PATHS. +def workaround_broken_search_paths + project = Xcodeproj::Project.open('WordPress/WordPress.xcodeproj') + + library_search_paths_key = 'LIBRARY_SEARCH_PATHS' + broken_search_paths = '$(SDKROOT)/usr/lib/swift$(inherited)' + + project.targets.each do |target| + target.build_configurations.each do |config| + original_search_paths = config.build_settings[library_search_paths_key] + + if original_search_paths == broken_search_paths + config.build_settings[library_search_paths_key] = ['$(SDKROOT)/usr/lib/swift', '$(inherited)'] + puts "[Gutenberg] Post-processed #{library_search_paths_key} for #{target.name} target to fix incorrect '#{broken_search_paths}' value." + end + end + end + project.save +end diff --git a/Gutenberg/config.yml b/Gutenberg/config.yml index f30457620410..1d1311433fcf 100644 --- a/Gutenberg/config.yml +++ b/Gutenberg/config.yml @@ -1,14 +1,14 @@ + # 'ref' should have either a commit or tag key. + # If both are set, tag will take precedence. + # + # We have automation that reads and updates this file. + # Leaving commented keys is discouraged, because the automation would ignore them and the resulting updates would be noisy + # If you want to use a local version, please use the LOCAL_GUTENBERG environment variable when calling CocoaPods. + # + # Example: + # + # LOCAL_GUTENBERG=../my-gutenberg-fork bundle exec pod install ref: - # This should have either a commit or tag key. - # If both are set, tag will take precedence. - # - # We have automation that reads and updates this file. - # Leaving commented keys is discouraged, because as the automation would ignore them and the resulting updates would be noisy - # If you want to use a local version, please use the LOCAL_GUTENBERG environment variable when calling CocoaPods. - # - # Example: - # - # LOCAL_GUTENBERG=../my-gutenberg-fork bundle exec pod install - tag: v1.106.0 + tag: v1.110.0 github_org: wordpress-mobile repo_name: gutenberg-mobile diff --git a/MIGRATIONS.md b/MIGRATIONS.md index d9a4e2179d0c..4a000fd4a289 100644 --- a/MIGRATIONS.md +++ b/MIGRATIONS.md @@ -3,6 +3,14 @@ This file documents changes in the data model. Please explain any changes to the data model as well as any custom migrations. +## WordPress 153 + +@dvdchr 2023-11-07 + +- `BloggingPrompt`: + - Removed `title` and `content` attributes. + - Added `additionalPostTags` (optional, no default, `Transformable` with type `[String]`) + ## WordPress 152 @kean 2023-07-28 diff --git a/Modules/.gitignore b/Modules/.gitignore new file mode 100644 index 000000000000..0023a5340637 --- /dev/null +++ b/Modules/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Modules/Package.swift b/Modules/Package.swift new file mode 100644 index 000000000000..59739e41bd59 --- /dev/null +++ b/Modules/Package.swift @@ -0,0 +1,25 @@ +// swift-tools-version: 5.8 + +import PackageDescription + +let jetpackStatsWidgetsCoreName = "JetpackStatsWidgetsCore" +let designSystemName = "DesignSystem" + +let package = Package( + name: "Modules", + platforms: [ + .iOS(.v15), + ], + products: [ + .library(name: jetpackStatsWidgetsCoreName, targets: [jetpackStatsWidgetsCoreName]), + .library(name: designSystemName, targets: [designSystemName]), + ], + targets: [ + .target(name: jetpackStatsWidgetsCoreName), + .testTarget( + name: "\(jetpackStatsWidgetsCoreName)Tests", + dependencies: [.target(name: jetpackStatsWidgetsCoreName)] + ), + .target(name: designSystemName) + ] +) diff --git a/Modules/Sources/DesignSystem/Components/DSButton.swift b/Modules/Sources/DesignSystem/Components/DSButton.swift new file mode 100644 index 000000000000..eeb2a82cf84c --- /dev/null +++ b/Modules/Sources/DesignSystem/Components/DSButton.swift @@ -0,0 +1,158 @@ +import SwiftUI + +public struct DSButton: View { + @SwiftUI.Environment(\.isEnabled) private var isEnabled + @SwiftUI.Environment(\.colorScheme) private var colorScheme + private let title: String + private let style: DSButtonStyle + @Binding private var isLoading: Bool + private let action: (() -> Void) + + public init( + title: String, + style: DSButtonStyle, + isLoading: Binding = .constant(false), + action: @escaping () -> Void + ) { + self._isLoading = isLoading + self.action = action + self.title = title + self.style = style + } + + public var body: some View { + switch style.size { + case .large: + button + case .medium, .small: + button + .fixedSize(horizontal: true, vertical: false) + } + } + + private var button: some View { + Button { + action() + UIImpactFeedbackGenerator(style: .medium).impactOccurred() + } label: { + if style.emphasis == .tertiary { + buttonContent + } else { + buttonContent + .contentShape( + RoundedRectangle( + cornerRadius: Length.Radius.small + ) + ) + } + } + .buttonStyle(ScalingButtonStyle(style: style)) + .disabled(isLoading) + } + + private var buttonContent: some View { + ZStack { + buttonBackground + if isLoading { + ProgressView() + .tint(Color.white) + } else { + if style.emphasis != .tertiary { + buttonText + .padding( + .horizontal, + style.size == .small + ? Length.Padding.split + : Length.Padding.medium + ) + } else { + buttonText + } + } + } + .frame( + height: style.size == .small + ? Length.Padding.large + : Length.Padding.max + ) + } + + private var buttonText: some View { + let textStyle: (TextStyle.Weight) -> TextStyle + let weight: TextStyle.Weight + switch style.size { + case .large: + textStyle = TextStyle.bodyLarge + case .medium: + textStyle = TextStyle.bodyMedium + case .small: + textStyle = TextStyle.bodySmall + } + switch style.emphasis { + case .primary, .secondary: + weight = .emphasized + case .tertiary: + weight = .regular + } + return Text(title).style(textStyle(weight)) + .foregroundStyle( + style.foregroundColor + .opacity(foregroundOpacity) + ) + } + + @ViewBuilder + private var buttonBackground: some View { + switch style.emphasis { + case .primary: + RoundedRectangle(cornerRadius: Length.Radius.small) + .fill(style.backgroundColor.opacity(priamryDisabledOpacity)) + case .secondary: + RoundedRectangle(cornerRadius: Length.Radius.small) + .stroke(Color.DS.divider, lineWidth: 1) + .background(Color.clear) + + case .tertiary: + Color.clear + } + } + + private var foregroundOpacity: CGFloat { + if isEnabled { + return 1 + } + + if style.emphasis == .primary { + return 1 + } + + return disabledOpacity + } + + private var priamryDisabledOpacity: CGFloat { + isEnabled ? 1 : disabledOpacity + } + + private var disabledOpacity: CGFloat { + colorScheme == .light ? 0.5 : 0.6 + } +} + +#if DEBUG +struct DSButton_Previews: PreviewProvider { + static var previews: some View { + ZStack { + Color.DS.Background.primary + .ignoresSafeArea() + DSButton( + title: "Get Domain", + style: .init(emphasis: .primary, size: .large, isJetpack: true), + action: { + () + } + ) + .padding(.horizontal, Length.Padding.large) + } + } +} +#endif diff --git a/Modules/Sources/DesignSystem/Components/DSButtonStyle.swift b/Modules/Sources/DesignSystem/Components/DSButtonStyle.swift new file mode 100644 index 000000000000..d3f451ef4ad6 --- /dev/null +++ b/Modules/Sources/DesignSystem/Components/DSButtonStyle.swift @@ -0,0 +1,71 @@ +import SwiftUI + +public struct DSButtonStyle { + public enum Emphasis: CaseIterable { + case primary + case secondary + case tertiary + } + + public enum Size: CaseIterable { + case large + case medium + case small + } + + public let emphasis: Emphasis + public let size: Size + public let isJetpack: Bool + + public init(emphasis: Emphasis, size: Size, isJetpack: Bool) { + self.emphasis = emphasis + self.size = size + self.isJetpack = isJetpack + } +} + +// MARK: - SwiftUI.Button DSButtonStyle helpers +extension DSButtonStyle { + var foregroundColor: Color { + switch self.emphasis { + case .primary: + return .DS.Background.primary + case .secondary: + return .DS.Foreground.primary + case .tertiary: + return .DS.Foreground.brand(isJetpack: isJetpack) + } + } + + var backgroundColor: Color { + switch self.emphasis { + case .primary: + return .DS.Foreground.primary + case .secondary, .tertiary: + return .clear + } + } +} + +public struct ScalingButtonStyle: ButtonStyle { + @SwiftUI.Environment(\.colorScheme) private var colorScheme + + private let style: DSButtonStyle + + public init(style: DSButtonStyle) { + self.style = style + } + + private var pressedStateBrightness: CGFloat { + colorScheme == .light ? 0.2 : -0.1 + } + + public func makeBody(configuration: Configuration) -> some View { + configuration.label + .scaleEffect((configuration.isPressed && style.emphasis != .tertiary) ? 0.98 : 1) + .brightness( + (configuration.isPressed && style.emphasis != .secondary) ? pressedStateBrightness : 0 + ) + .animation(.linear(duration: 0.15), value: configuration.isPressed) + } +} diff --git a/Modules/Sources/DesignSystem/Components/Modifiers/Text+DesignSystem.swift b/Modules/Sources/DesignSystem/Components/Modifiers/Text+DesignSystem.swift new file mode 100644 index 000000000000..303331bc7be5 --- /dev/null +++ b/Modules/Sources/DesignSystem/Components/Modifiers/Text+DesignSystem.swift @@ -0,0 +1,68 @@ +import SwiftUI + +// MARK: - SwiftUI.Font: TextStyle +extension TextStyle { + var font: Font { + switch self { + case .heading1: + return Font.DS.heading1 + + case .heading2: + return Font.DS.heading2 + + case .heading3: + return Font.DS.heading3 + + case .heading4: + return Font.DS.heading4 + + case .bodySmall(let weight): + switch weight { + case .regular: + return Font.DS.Body.small + case .emphasized: + return Font.DS.Body.Emphasized.small + } + + case .bodyMedium(let weight): + switch weight { + case .regular: + return Font.DS.Body.medium + case .emphasized: + return Font.DS.Body.Emphasized.medium + } + + case .bodyLarge(let weight): + switch weight { + case .regular: + return Font.DS.Body.large + case .emphasized: + return Font.DS.Body.Emphasized.large + } + + case .footnote: + return Font.DS.footnote + + case .caption: + return Font.DS.caption + } + } + + var `case`: Text.Case? { + switch self { + case .caption: + return .uppercase + default: + return nil + } + } +} + +// MARK: - SwiftUI.Text +public extension Text { + @ViewBuilder + func style(_ style: TextStyle) -> some View { + self.font(style.font) + .textCase(style.case) + } +} diff --git a/Modules/Sources/DesignSystem/Components/Modifiers/TextStyle.swift b/Modules/Sources/DesignSystem/Components/Modifiers/TextStyle.swift new file mode 100644 index 000000000000..787a1571be62 --- /dev/null +++ b/Modules/Sources/DesignSystem/Components/Modifiers/TextStyle.swift @@ -0,0 +1,16 @@ +public enum TextStyle { + case heading1 + case heading2 + case heading3 + case heading4 + case bodySmall(Weight) + case bodyMedium(Weight) + case bodyLarge(Weight) + case footnote + case caption + + public enum Weight { + case regular + case emphasized + } +} diff --git a/Modules/Sources/DesignSystem/Components/Modifiers/UILabel+DesignSystem.swift b/Modules/Sources/DesignSystem/Components/Modifiers/UILabel+DesignSystem.swift new file mode 100644 index 000000000000..b76ce3b801b2 --- /dev/null +++ b/Modules/Sources/DesignSystem/Components/Modifiers/UILabel+DesignSystem.swift @@ -0,0 +1,115 @@ +import UIKit + +// MARK: - UIKit.UIFont: TextStyle +extension TextStyle { + var uiFont: UIFont { + switch self { + case .heading1: + return UIFont.DS.heading1 + + case .heading2: + return UIFont.DS.heading2 + + case .heading3: + return UIFont.DS.heading3 + + case .heading4: + return UIFont.DS.heading4 + + case .bodySmall(let weight): + switch weight { + case .regular: + return UIFont.DS.Body.small + case .emphasized: + return UIFont.DS.Body.Emphasized.small + } + + case .bodyMedium(let weight): + switch weight { + case .regular: + return UIFont.DS.Body.medium + case .emphasized: + return UIFont.DS.Body.Emphasized.medium + } + + case .bodyLarge(let weight): + switch weight { + case .regular: + return UIFont.DS.Body.large + case .emphasized: + return UIFont.DS.Body.Emphasized.large + } + + case .footnote: + return UIFont.DS.footnote + + case .caption: + return UIFont.DS.caption + } + } +} + +// MARK: - SwiftUI.Text +extension UILabel { + func style(_ style: TextStyle) -> Self { + self.font = style.uiFont + if style.case == .uppercase { + self.text = self.text?.uppercased() + } + return self + } +} + +// MARK: - UIKit.UIFont +fileprivate extension UIFont { + enum DS { + static let heading1 = DynamicFontHelper.fontForTextStyle(.largeTitle, fontWeight: .bold) + static let heading2 = DynamicFontHelper.fontForTextStyle(.title1, fontWeight: .bold) + static let heading3 = DynamicFontHelper.fontForTextStyle(.title2, fontWeight: .bold) + static let heading4 = DynamicFontHelper.fontForTextStyle(.title3, fontWeight: .semibold) + + enum Body { + static let small = DynamicFontHelper.fontForTextStyle(.body, fontWeight: .regular) + static let medium = DynamicFontHelper.fontForTextStyle(.callout, fontWeight: .regular) + static let large = DynamicFontHelper.fontForTextStyle(.subheadline, fontWeight: .regular) + + enum Emphasized { + static let small = DynamicFontHelper.fontForTextStyle(.body, fontWeight: .semibold) + static let medium = DynamicFontHelper.fontForTextStyle(.callout, fontWeight: .semibold) + static let large = DynamicFontHelper.fontForTextStyle(.subheadline, fontWeight: .semibold) + } + } + + static let footnote = DynamicFontHelper.fontForTextStyle(.footnote, fontWeight: .regular) + static let caption = DynamicFontHelper.fontForTextStyle(.caption1, fontWeight: .regular) + } +} + +private enum DynamicFontHelper { + static func fontForTextStyle(_ style: UIFont.TextStyle, fontWeight weight: UIFont.Weight) -> UIFont { + /// WORKAROUND: Some font weights scale up well initially but they don't scale up well if dynamic type + /// is changed in real time. Creating a scaled font offers an alternative solution that works well + /// even in real time. + let weightsThatNeedScaledFont: [UIFont.Weight] = [.black, .bold, .heavy, .semibold] + + guard !weightsThatNeedScaledFont.contains(weight) else { + return scaledFont(for: style, weight: weight) + } + + var fontDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: style) + + let traits = [UIFontDescriptor.TraitKey.weight: weight] + fontDescriptor = fontDescriptor.addingAttributes([.traits: traits]) + + return UIFont(descriptor: fontDescriptor, size: CGFloat(0.0)) + } + + static func scaledFont(for style: UIFont.TextStyle, weight: UIFont.Weight, design: UIFontDescriptor.SystemDesign = .default) -> UIFont { + let fontDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: style) + let fontDescriptorWithDesign = fontDescriptor.withDesign(design) ?? fontDescriptor + let traits = [UIFontDescriptor.TraitKey.weight: weight] + let finalDescriptor = fontDescriptorWithDesign.addingAttributes([.traits: traits]) + + return UIFont(descriptor: finalDescriptor, size: finalDescriptor.pointSize) + } +} diff --git a/Modules/Sources/DesignSystem/Foundation/Color+DesignSystem.swift b/Modules/Sources/DesignSystem/Foundation/Color+DesignSystem.swift new file mode 100644 index 000000000000..2516b6d47687 --- /dev/null +++ b/Modules/Sources/DesignSystem/Foundation/Color+DesignSystem.swift @@ -0,0 +1,75 @@ +import SwiftUI + +/// Design System Color extensions. Keep it in sync with its sibling file `UIColor+DesignSystem` +/// to support borth API's equally. +public extension Color { + enum DS { + public enum Foreground { + public static let primary = colorWithModuleBundle(colorName: DesignSystemColorNames.Foreground.primary) + public static let secondary = colorWithModuleBundle(colorName: DesignSystemColorNames.Foreground.secondary) + public static let tertiary = colorWithModuleBundle(colorName: DesignSystemColorNames.Foreground.tertiary) + public static let quaternary = colorWithModuleBundle(colorName: DesignSystemColorNames.Foreground.quaternary) + public static let success = colorWithModuleBundle(colorName: DesignSystemColorNames.Foreground.success) + public static let warning = colorWithModuleBundle(colorName: DesignSystemColorNames.Foreground.warning) + public static let error = colorWithModuleBundle(colorName: DesignSystemColorNames.Foreground.error) + + public static func brand(isJetpack: Bool) -> Color { + return isJetpack ? jetpack : wordPress + } + + private static let jetpack = colorWithModuleBundle(colorName: DesignSystemColorNames.Foreground.jetpack) + private static let wordPress = colorWithModuleBundle(colorName: DesignSystemColorNames.Foreground.wordPress) + } + + public enum Background { + public static let primary = colorWithModuleBundle(colorName: DesignSystemColorNames.Background.primary) + public static let secondary = colorWithModuleBundle(colorName: DesignSystemColorNames.Background.secondary) + public static let tertiary = colorWithModuleBundle(colorName: DesignSystemColorNames.Background.tertiary) + public static let quaternary = colorWithModuleBundle(colorName: DesignSystemColorNames.Background.quaternary) + + public static func brand(isJetpack: Bool) -> Color { + return isJetpack ? jetpack : wordPress + } + + private static let jetpack = colorWithModuleBundle(colorName: DesignSystemColorNames.Background.jetpack) + private static let wordPress = colorWithModuleBundle(colorName: DesignSystemColorNames.Background.wordPress) + } + + public static let divider = colorWithModuleBundle(colorName: DesignSystemColorNames.divider) + + private static func colorWithModuleBundle(colorName: String) -> Color { + Color(colorName, bundle: .module) + } + + public static func custom(_ colorName: String) -> Color { + return colorWithModuleBundle(colorName: colorName) + } + } +} + +/// Once we move Design System to its own module, we should keep this `internal` +/// as we don't need to expose it to the application module +internal enum DesignSystemColorNames { + internal enum Foreground { + internal static let primary = "foregroundPrimary" + internal static let secondary = "foregroundSecondary" + internal static let tertiary = "foregroundTertiary" + internal static let quaternary = "foregroundQuaternary" + internal static let success = "success" + internal static let warning = "warning" + internal static let error = "error" + internal static let jetpack = "foregroundBrandJP" + internal static let wordPress = "foregroundBrandWP" + } + + internal enum Background { + internal static let primary = "backgroundPrimary" + internal static let secondary = "backgroundSecondary" + internal static let tertiary = "backgroundTertiary" + internal static let quaternary = "backgroundQuaternary" + internal static let jetpack = "backgroundBrandJP" + internal static let wordPress = "backgroundBrandWP" + } + + internal static let divider = "divider" +} diff --git a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Contents.json b/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Contents.json similarity index 100% rename from WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Contents.json rename to Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Contents.json diff --git a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Custom/Contents.json b/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Custom/Contents.json similarity index 100% rename from WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Custom/Contents.json rename to Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Custom/Contents.json diff --git a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Custom/SiteCreation/Contents.json b/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Custom/SiteCreation/Contents.json similarity index 100% rename from WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Custom/SiteCreation/Contents.json rename to Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Custom/SiteCreation/Contents.json diff --git a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Custom/SiteCreation/emptySiteBackgroundPrimary.colorset/Contents.json b/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Custom/SiteCreation/emptySiteBackgroundPrimary.colorset/Contents.json similarity index 100% rename from WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Custom/SiteCreation/emptySiteBackgroundPrimary.colorset/Contents.json rename to Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Custom/SiteCreation/emptySiteBackgroundPrimary.colorset/Contents.json diff --git a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Custom/SiteCreation/emptySiteBackgroundSecondary.colorset/Contents.json b/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Custom/SiteCreation/emptySiteBackgroundSecondary.colorset/Contents.json similarity index 100% rename from WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Custom/SiteCreation/emptySiteBackgroundSecondary.colorset/Contents.json rename to Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Custom/SiteCreation/emptySiteBackgroundSecondary.colorset/Contents.json diff --git a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Custom/SiteCreation/emptySiteForegroundPrimary.colorset/Contents.json b/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Custom/SiteCreation/emptySiteForegroundPrimary.colorset/Contents.json similarity index 100% rename from WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Custom/SiteCreation/emptySiteForegroundPrimary.colorset/Contents.json rename to Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Custom/SiteCreation/emptySiteForegroundPrimary.colorset/Contents.json diff --git a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Custom/SiteCreation/emptySiteForegroundSecondary.colorset/Contents.json b/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Custom/SiteCreation/emptySiteForegroundSecondary.colorset/Contents.json similarity index 100% rename from WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Custom/SiteCreation/emptySiteForegroundSecondary.colorset/Contents.json rename to Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Custom/SiteCreation/emptySiteForegroundSecondary.colorset/Contents.json diff --git a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Custom/SiteCreation/emptySiteGradientInitial.colorset/Contents.json b/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Custom/SiteCreation/emptySiteGradientInitial.colorset/Contents.json similarity index 100% rename from WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Custom/SiteCreation/emptySiteGradientInitial.colorset/Contents.json rename to Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Custom/SiteCreation/emptySiteGradientInitial.colorset/Contents.json diff --git a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Custom/SiteCreation/emptySiteTooltipBackground.colorset/Contents.json b/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Custom/SiteCreation/emptySiteTooltipBackground.colorset/Contents.json similarity index 100% rename from WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Custom/SiteCreation/emptySiteTooltipBackground.colorset/Contents.json rename to Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Custom/SiteCreation/emptySiteTooltipBackground.colorset/Contents.json diff --git a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Custom/SiteCreation/emptySiteTooltipBorder.colorset/Contents.json b/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Custom/SiteCreation/emptySiteTooltipBorder.colorset/Contents.json similarity index 100% rename from WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Custom/SiteCreation/emptySiteTooltipBorder.colorset/Contents.json rename to Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Custom/SiteCreation/emptySiteTooltipBorder.colorset/Contents.json diff --git a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Custom/SiteCreation/emptySiteTooltipGradientInitial.colorset/Contents.json b/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Custom/SiteCreation/emptySiteTooltipGradientInitial.colorset/Contents.json similarity index 100% rename from WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Custom/SiteCreation/emptySiteTooltipGradientInitial.colorset/Contents.json rename to Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Custom/SiteCreation/emptySiteTooltipGradientInitial.colorset/Contents.json diff --git a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Background/Contents.json b/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Background/Contents.json similarity index 100% rename from WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Background/Contents.json rename to Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Background/Contents.json diff --git a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Foreground/error.colorset/Contents.json b/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Background/backgroundBrandJP.colorset/Contents.json similarity index 76% rename from WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Foreground/error.colorset/Contents.json rename to Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Background/backgroundBrandJP.colorset/Contents.json index c72733d8103b..ccafa17064e4 100644 --- a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Foreground/error.colorset/Contents.json +++ b/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Background/backgroundBrandJP.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "56", - "green" : "53", - "red" : "213" + "blue" : "0x10", + "green" : "0x87", + "red" : "0x00" } }, "idiom" : "universal" @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "46", - "green" : "45", - "red" : "180" + "blue" : "0x08", + "green" : "0x9E", + "red" : "0x06" } }, "idiom" : "universal" diff --git a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Foreground/warning.colorset/Contents.json b/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Background/backgroundBrandWP.colorset/Contents.json similarity index 76% rename from WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Foreground/warning.colorset/Contents.json rename to Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Background/backgroundBrandWP.colorset/Contents.json index e93158e7dc6b..15a845b25ba1 100644 --- a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Foreground/warning.colorset/Contents.json +++ b/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Background/backgroundBrandWP.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0", - "green" : "98", - "red" : "178" + "blue" : "0xE3", + "green" : "0x58", + "red" : "0x38" } }, "idiom" : "universal" @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "9", - "green" : "119", - "red" : "214" + "blue" : "0xE3", + "green" : "0x58", + "red" : "0x38" } }, "idiom" : "universal" diff --git a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Background/backgroundSecondary.colorset/Contents.json b/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Background/backgroundPrimary.colorset/Contents.json similarity index 76% rename from WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Background/backgroundSecondary.colorset/Contents.json rename to Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Background/backgroundPrimary.colorset/Contents.json index 89e027c1d029..e4846c314ac4 100644 --- a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Background/backgroundSecondary.colorset/Contents.json +++ b/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Background/backgroundPrimary.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.965", - "green" : "0.945", - "red" : "0.949" + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" } }, "idiom" : "universal" @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.118", - "green" : "0.110", - "red" : "0.110" + "blue" : "0x1E", + "green" : "0x1C", + "red" : "0x1C" } }, "idiom" : "universal" diff --git a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Foreground/success.colorset/Contents.json b/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Background/backgroundQuaternary.colorset/Contents.json similarity index 76% rename from WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Foreground/success.colorset/Contents.json rename to Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Background/backgroundQuaternary.colorset/Contents.json index 050de46797db..4b655781feef 100644 --- a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Foreground/success.colorset/Contents.json +++ b/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Background/backgroundQuaternary.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.063", - "green" : "0.529", - "red" : "0.016" + "blue" : "0x9E", + "green" : "0x9B", + "red" : "0x9B" } }, "idiom" : "universal" @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.125", - "green" : "0.541", - "red" : "0.000" + "blue" : "0x79", + "green" : "0x78", + "red" : "0x78" } }, "idiom" : "universal" diff --git a/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Background/backgroundSecondary.colorset/Contents.json b/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Background/backgroundSecondary.colorset/Contents.json new file mode 100644 index 000000000000..8cea2a23846b --- /dev/null +++ b/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Background/backgroundSecondary.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xF7", + "green" : "0xF2", + "red" : "0xF2" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x2E", + "green" : "0x2C", + "red" : "0x2C" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Background/backgroundTertiary.colorset/Contents.json b/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Background/backgroundTertiary.colorset/Contents.json new file mode 100644 index 000000000000..9cab07b3d0da --- /dev/null +++ b/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Background/backgroundTertiary.colorset/Contents.json @@ -0,0 +1,36 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "extended-gray", + "components" : { + "alpha" : "1.000", + "white" : "0xC2" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x3C", + "green" : "0x3A", + "red" : "0x3A" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Border/Contents.json b/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Contents.json similarity index 100% rename from WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Border/Contents.json rename to Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Contents.json diff --git a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Contents.json b/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Divider/Contents.json similarity index 100% rename from WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Contents.json rename to Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Divider/Contents.json diff --git a/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Divider/divider.colorset/Contents.json b/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Divider/divider.colorset/Contents.json new file mode 100644 index 000000000000..3d8e0fb8b7c2 --- /dev/null +++ b/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Divider/divider.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xC8", + "green" : "0xC6", + "red" : "0xC6" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x3A", + "green" : "0x38", + "red" : "0x38" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Foreground/Contents.json b/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Foreground/Contents.json similarity index 100% rename from WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Foreground/Contents.json rename to Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Foreground/Contents.json diff --git a/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Foreground/error.colorset/Contents.json b/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Foreground/error.colorset/Contents.json new file mode 100644 index 000000000000..5515e26eb801 --- /dev/null +++ b/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Foreground/error.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x38", + "green" : "0x36", + "red" : "0xD6" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x54", + "green" : "0x50", + "red" : "0xE6" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Theme/brandJetpack.colorset/Contents.json b/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Foreground/foregroundBrandJP.colorset/Contents.json similarity index 100% rename from WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Theme/brandJetpack.colorset/Contents.json rename to Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Foreground/foregroundBrandJP.colorset/Contents.json diff --git a/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Foreground/foregroundBrandWP.colorset/Contents.json b/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Foreground/foregroundBrandWP.colorset/Contents.json new file mode 100644 index 000000000000..7916b034cf5c --- /dev/null +++ b/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Foreground/foregroundBrandWP.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xE3", + "green" : "0x9C", + "red" : "0x39" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xE3", + "green" : "0x9C", + "red" : "0x39" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Foreground/foregroundPrimary.colorset/Contents.json b/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Foreground/foregroundPrimary.colorset/Contents.json new file mode 100644 index 000000000000..0c600f92f1e2 --- /dev/null +++ b/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Foreground/foregroundPrimary.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x00", + "green" : "0x00", + "red" : "0x00" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Foreground/foregroundQuaternary.colorset/Contents.json b/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Foreground/foregroundQuaternary.colorset/Contents.json similarity index 100% rename from WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Foreground/foregroundQuaternary.colorset/Contents.json rename to Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Foreground/foregroundQuaternary.colorset/Contents.json diff --git a/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Foreground/foregroundSecondary.colorset/Contents.json b/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Foreground/foregroundSecondary.colorset/Contents.json new file mode 100644 index 000000000000..70b1446d0111 --- /dev/null +++ b/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Foreground/foregroundSecondary.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.600", + "blue" : "0x43", + "green" : "0x3C", + "red" : "0x3C" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.600", + "blue" : "0xF5", + "green" : "0xEB", + "red" : "0xEB" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Foreground/foregroundTertiary.colorset/Contents.json b/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Foreground/foregroundTertiary.colorset/Contents.json new file mode 100644 index 000000000000..8cef96ee1262 --- /dev/null +++ b/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Foreground/foregroundTertiary.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.300", + "blue" : "0x43", + "green" : "0x3C", + "red" : "0x3C" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.300", + "blue" : "0xF5", + "green" : "0xEB", + "red" : "0xEB" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Foreground/success.colorset/Contents.json b/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Foreground/success.colorset/Contents.json new file mode 100644 index 000000000000..c79f29f275e6 --- /dev/null +++ b/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Foreground/success.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x10", + "green" : "0x87", + "red" : "0x00" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x1F", + "green" : "0xB4", + "red" : "0x2F" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Foreground/warning.colorset/Contents.json b/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Foreground/warning.colorset/Contents.json new file mode 100644 index 000000000000..6f16d56e02e5 --- /dev/null +++ b/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Foreground/warning.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x09", + "green" : "0x77", + "red" : "0xD6" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x28", + "green" : "0x8B", + "red" : "0xE6" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/Sources/DesignSystem/Foundation/Font+DesignSystem.swift b/Modules/Sources/DesignSystem/Foundation/Font+DesignSystem.swift new file mode 100644 index 000000000000..4f808eca0b9b --- /dev/null +++ b/Modules/Sources/DesignSystem/Foundation/Font+DesignSystem.swift @@ -0,0 +1,25 @@ +import SwiftUI + +public extension Font { + enum DS { + public static let heading1 = Font.largeTitle.weight(.semibold) + public static let heading2 = Font.title.weight(.semibold) + public static let heading3 = Font.title2.weight(.semibold) + public static let heading4 = Font.title3.weight(.semibold) + + public enum Body { + public static let small = Font.subheadline + public static let medium = Font.callout + public static let large = Font.body + + public enum Emphasized { + public static let small = Body.small.weight(.semibold) + public static let medium = Body.medium.weight(.semibold) + public static let large = Body.large.weight(.semibold) + } + } + + public static let footnote = Font.footnote + public static let caption = Font.caption + } +} diff --git a/Modules/Sources/DesignSystem/Foundation/Length.swift b/Modules/Sources/DesignSystem/Foundation/Length.swift new file mode 100644 index 000000000000..ad2e15dbaab9 --- /dev/null +++ b/Modules/Sources/DesignSystem/Foundation/Length.swift @@ -0,0 +1,28 @@ +import Foundation + +public enum Length { + public enum Padding { + public static let half: CGFloat = 4 + public static let single: CGFloat = 8 + public static let split: CGFloat = 12 + public static let double: CGFloat = 16 + public static let medium: CGFloat = 24 + public static let large: CGFloat = 32 + public static let max: CGFloat = 48 + } + + public enum Hitbox { + public static let minTappableLength: CGFloat = 44 + } + + public enum Radius { + public static let small: CGFloat = 5 + public static let medium: CGFloat = 10 + public static let large: CGFloat = 15 + public static let max: CGFloat = 20 + } + + public enum Border { + public static let thin: CGFloat = 0.5 + } +} diff --git a/Modules/Sources/DesignSystem/Foundation/UIColor+DesignSystem.swift b/Modules/Sources/DesignSystem/Foundation/UIColor+DesignSystem.swift new file mode 100644 index 000000000000..c24626994116 --- /dev/null +++ b/Modules/Sources/DesignSystem/Foundation/UIColor+DesignSystem.swift @@ -0,0 +1,49 @@ +import UIKit + +/// Replica of the `Color` structure +/// The reason for not using the `Color` intializer of `UIColor` is that +/// it has dubious effects. Also the doc advises against it. +/// Even though `UIColor(SwiftUI.Color)` keeps the adaptability for color theme, +/// accessing to light or dark variants specifically via trait collection does not return the right values +/// if the color is initialized as such. Probably one of the reasons why they advise against it. +/// To make these values non-optional, we use `Color` versions as fallback. +public extension UIColor { + enum DS { + public enum Foreground { + public static let primary = colorWithModuleBundle(colorName: DesignSystemColorNames.Foreground.primary) + public static let secondary = colorWithModuleBundle(colorName: DesignSystemColorNames.Foreground.secondary) + public static let tertiary = colorWithModuleBundle(colorName: DesignSystemColorNames.Foreground.tertiary) + public static let quaternary = colorWithModuleBundle(colorName: DesignSystemColorNames.Foreground.quaternary) + public static let success = colorWithModuleBundle(colorName: DesignSystemColorNames.Foreground.success) + public static let warning = colorWithModuleBundle(colorName: DesignSystemColorNames.Foreground.warning) + public static let error = colorWithModuleBundle(colorName: DesignSystemColorNames.Foreground.error) + + public static func brand(isJetpack: Bool) -> UIColor? { + isJetpack ? jetpack : wordPress + } + + private static let jetpack = colorWithModuleBundle(colorName: DesignSystemColorNames.Foreground.jetpack) + private static let wordPress = colorWithModuleBundle(colorName: DesignSystemColorNames.Foreground.wordPress) + } + + public enum Background { + public static let primary = colorWithModuleBundle(colorName: DesignSystemColorNames.Background.primary) + public static let secondary = colorWithModuleBundle(colorName: DesignSystemColorNames.Background.secondary) + public static let tertiary = colorWithModuleBundle(colorName: DesignSystemColorNames.Background.tertiary) + public static let quaternary = colorWithModuleBundle(colorName: DesignSystemColorNames.Background.quaternary) + + public static func brand(isJetpack: Bool) -> UIColor? { + isJetpack ? jetpack : wordPress + } + + private static let jetpack = colorWithModuleBundle(colorName: DesignSystemColorNames.Background.jetpack) + private static let wordPress = colorWithModuleBundle(colorName: DesignSystemColorNames.Background.wordPress) + } + + public static let divider = colorWithModuleBundle(colorName: DesignSystemColorNames.divider) + + private static func colorWithModuleBundle(colorName: String) -> UIColor? { + UIColor(named: colorName, in: .module, compatibleWith: .current) + } + } +} diff --git a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/ColorGallery.swift b/Modules/Sources/DesignSystem/Gallery/ColorGallery.swift similarity index 61% rename from WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/ColorGallery.swift rename to Modules/Sources/DesignSystem/Gallery/ColorGallery.swift index 3668a319f711..435c0c7ab2a8 100644 --- a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/ColorGallery.swift +++ b/Modules/Sources/DesignSystem/Gallery/ColorGallery.swift @@ -5,10 +5,13 @@ struct ColorGallery: View { var body: some View { List { - foregroundSection - backgroundSection - borderSection + Group { + foregroundSection + backgroundSection + dividerSection + } } + .navigationTitle("Colors") } private var foregroundSection: some View { @@ -33,16 +36,31 @@ struct ColorGallery: View { hexString: hexString(for: .DS.Foreground.quaternary), color: Color.DS.Foreground.quaternary ) + listItem( + with: "Brand", + hexString: hexString(for: .DS.Foreground.brand(isJetpack: true)), + color: Color.DS.Foreground.brand(isJetpack: true) + ) + listItem( + with: "Success", + hexString: hexString(for: .DS.Foreground.success), + color: Color.DS.Foreground.brand(isJetpack: true) + ) + listItem( + with: "Warning", + hexString: hexString(for: .DS.Foreground.warning), + color: Color.DS.Foreground.warning + ) + listItem( + with: "Error", + hexString: hexString(for: .DS.Foreground.error), + color: Color.DS.Foreground.error + ) } } private var backgroundSection: some View { Section(header: sectionTitle("Background")) { - listItem( - with: "Brand", - hexString: hexString(for: .DS.Background.brand), - color: Color.DS.Background.brand - ) listItem( with: "Primary", hexString: hexString(for: .DS.Background.primary), @@ -63,25 +81,20 @@ struct ColorGallery: View { hexString: hexString(for: .DS.Background.quaternary), color: Color.DS.Background.quaternary ) + listItem( + with: "Brand", + hexString: hexString(for: .DS.Background.brand(isJetpack: true)), + color: Color.DS.Background.brand(isJetpack: true) + ) } } - private var borderSection: some View { - Section(header: sectionTitle("Border")) { + private var dividerSection: some View { + Section(header: sectionTitle("Divider")) { listItem( with: "Divider", - hexString: hexString(for: .DS.Border.divider), - color: Color.DS.Border.divider - ) - listItem( - with: "Primary", - hexString: hexString(for: .DS.Border.primary), - color: Color.DS.Border.primary - ) - listItem( - with: "Secondary", - hexString: hexString(for: .DS.Border.secondary), - color: Color.DS.Border.secondary + hexString: hexString(for: .DS.divider), + color: Color.DS.divider ) } } @@ -120,3 +133,33 @@ struct ColorGallery: View { } } } + +// MARK: - Helpers for Color Gallery +private extension UIColor { + func color(for trait: UITraitCollection?) -> UIColor { + if let trait = trait { + return resolvedColor(with: trait) + } + return self + } + + func lightVariant() -> UIColor { + return color(for: UITraitCollection(userInterfaceStyle: .light)) + } + + func darkVariant() -> UIColor { + return color(for: UITraitCollection(userInterfaceStyle: .dark)) + } + + func hexString() -> String? { + guard let components = cgColor.components, components.count >= 3 else { + return nil + } + + let r = Float(components[0]) + let g = Float(components[1]) + let b = Float(components[2]) + + return String(format: "%02lX%02lX%02lX", lroundf(r * 255), lroundf(g * 255), lroundf(b * 255)) + } +} diff --git a/Modules/Sources/DesignSystem/Gallery/DSButtonGallery.swift b/Modules/Sources/DesignSystem/Gallery/DSButtonGallery.swift new file mode 100644 index 000000000000..6aa1699514c3 --- /dev/null +++ b/Modules/Sources/DesignSystem/Gallery/DSButtonGallery.swift @@ -0,0 +1,69 @@ +import SwiftUI + +struct DSButtonGallery: View { + var body: some View { + List { + ForEach(DSButtonStyle.Size.allCases, id: \.title) { size in + Section(size.title) { + HStack { + Spacer() + VStack(spacing: Length.Padding.medium) { + ForEach(DSButtonStyle.Emphasis.allCases, id: \.title) { emphasis in + DSButton( + title: emphasis.title, + style: .init( + emphasis: emphasis, + size: size, + isJetpack: true + ) + ) {()} + } + ForEach(DSButtonStyle.Emphasis.allCases, id: \.title) { emphasis in + DSButton( + title: emphasis.title + " Disabled", + style: .init( + emphasis: emphasis, + size: size, + isJetpack: true + ) + ) {()} + .disabled(true) + } + } + Spacer() + } + } + .listRowSeparator(.hidden) + .listRowBackground(Color.clear) + .listRowInsets(.init(top: Length.Padding.split, leading: 0, bottom: Length.Padding.split, trailing: 0)) + } + .navigationTitle("DSButton") + } + } +} + +private extension DSButtonStyle.Size { + var title: String { + switch self { + case .large: + return "Large" + case .medium: + return "Medium" + case .small: + return "Small" + } + } +} + +private extension DSButtonStyle.Emphasis { + var title: String { + switch self { + case .primary: + return "Primary" + case .secondary: + return "Secondary" + case .tertiary: + return "Tertiary" + } + } +} diff --git a/Modules/Sources/DesignSystem/Gallery/DesignSystemGallery.swift b/Modules/Sources/DesignSystem/Gallery/DesignSystemGallery.swift new file mode 100644 index 000000000000..70cbccdeaf06 --- /dev/null +++ b/Modules/Sources/DesignSystem/Gallery/DesignSystemGallery.swift @@ -0,0 +1,29 @@ +import SwiftUI + +public struct DesignSystemGallery: View { + public var body: some View { + List { + NavigationLink("Foundation", destination: foundationList) + NavigationLink("Components", destination: componentsList) + } + .navigationTitle("Design System") + } + + public init() { } + + private var foundationList: some View { + List { + NavigationLink("Colors", destination: ColorGallery()) + NavigationLink("Fonts", destination: FontGallery()) + NavigationLink("Lengths", destination: LengthGallery()) + } + .navigationTitle("Foundation") + } + + private var componentsList: some View { + List { + NavigationLink("DSButton", destination: DSButtonGallery()) + } + .navigationTitle("Components") + } +} diff --git a/Modules/Sources/DesignSystem/Gallery/FontGallery.swift b/Modules/Sources/DesignSystem/Gallery/FontGallery.swift new file mode 100644 index 000000000000..08e574a99c66 --- /dev/null +++ b/Modules/Sources/DesignSystem/Gallery/FontGallery.swift @@ -0,0 +1,41 @@ +import SwiftUI + +struct FontGallery: View { + var body: some View { + List { + Section("Heading") { + Text("Heading1") + .style(.heading1) + Text("Heading2") + .style(.heading2) + Text("Heading3") + .style(.heading3) + Text("Heading4") + .style(.heading4) + } + + Section("Body") { + Text("Body Small Regular") + .style(.bodySmall(.regular)) + Text("Body Medium Regular") + .style(.bodyMedium(.regular)) + Text("Body Large Regular") + .style(.bodyLarge(.regular)) + Text("Body Small Emphasized") + .style(.bodySmall(.emphasized)) + Text("Body Medium Emphasized") + .style(.bodyMedium(.emphasized)) + Text("Body Large Emphasized") + .style(.bodyLarge(.emphasized)) + } + + Section("Miscellaneous") { + Text("Footnote") + .style(.footnote) + Text("Caption") + .style(.caption) + } + } + .navigationTitle("Fonts") + } +} diff --git a/Modules/Sources/DesignSystem/Gallery/LengthGallery.swift b/Modules/Sources/DesignSystem/Gallery/LengthGallery.swift new file mode 100644 index 000000000000..7adceaa93ca7 --- /dev/null +++ b/Modules/Sources/DesignSystem/Gallery/LengthGallery.swift @@ -0,0 +1,66 @@ +import SwiftUI + +struct LengthGallery: View { + var body: some View { + List { + Section("Padding") { + VStack { + paddingRectangle(name: "Half").padding(.trailing, Length.Padding.half) + paddingRectangle(name: "Single").padding(.trailing, Length.Padding.single) + paddingRectangle(name: "Split").padding(.trailing, Length.Padding.split) + paddingRectangle(name: "Double").padding(.trailing, Length.Padding.double) + paddingRectangle(name: "Medium").padding(.trailing, Length.Padding.medium) + paddingRectangle(name: "Large").padding(.trailing, Length.Padding.large) + paddingRectangle(name: "Max").padding(.trailing, Length.Padding.max) + } + .background(Color.DS.Foreground.warning.opacity(0.7)) + .clipShape(RoundedRectangle(cornerRadius: Length.Radius.small)) + } + .listRowBackground(Color.clear) + + Section("Radii") { + radiusBoxesVStack + } + .listRowBackground(Color.clear) + } + .navigationTitle("Lengths") + } + + private func paddingRectangle(name: String) -> some View { + ZStack { + RoundedRectangle(cornerRadius: Length.Radius.small) + .fill(Color.DS.Background.tertiary) + .frame(height: Length.Hitbox.minTappableLength) + HStack { + Text(name) + .offset(x: Length.Padding.double) + .foregroundStyle(Color.DS.Foreground.primary) + Spacer() + } + } + } + + private var radiusBoxesVStack: some View { + VStack(spacing: Length.Padding.double) { + HStack(spacing: Length.Padding.double) { + radiusBox(name: "Small", radius: Length.Radius.small) + radiusBox(name: "Medium", radius: Length.Radius.medium) + } + HStack(spacing: Length.Padding.double) { + radiusBox(name: "Large", radius: Length.Radius.large) + radiusBox(name: "Max", radius: Length.Radius.max) + } + } + } + + private func radiusBox(name: String, radius: CGFloat) -> some View { + ZStack { + RoundedRectangle(cornerRadius: radius) + .fill(Color.DS.Background.tertiary) + .frame(width: 120, height: 120) + Text(name) + .foregroundStyle(Color.DS.Foreground.primary) + + } + } +} diff --git a/Modules/Sources/JetpackStatsWidgetsCore/HomeWidgetData.swift b/Modules/Sources/JetpackStatsWidgetsCore/HomeWidgetData.swift new file mode 100644 index 000000000000..10579af0c304 --- /dev/null +++ b/Modules/Sources/JetpackStatsWidgetsCore/HomeWidgetData.swift @@ -0,0 +1,13 @@ +import Foundation + +public protocol HomeWidgetData: Codable { + + var siteID: Int { get } + var siteName: String { get } + var url: String { get } + var timeZone: TimeZone { get } + var date: Date { get } + var statsURL: URL? { get } + + static var filename: String { get } +} diff --git a/WordPress/JetpackStatsWidgets/Model/ThisWeekWidgetStats.swift b/Modules/Sources/JetpackStatsWidgetsCore/Model/ThisWeekWidgetStats.swift similarity index 60% rename from WordPress/JetpackStatsWidgets/Model/ThisWeekWidgetStats.swift rename to Modules/Sources/JetpackStatsWidgetsCore/Model/ThisWeekWidgetStats.swift index 6bcbc22a79bd..fb61c140a7f5 100644 --- a/WordPress/JetpackStatsWidgets/Model/ThisWeekWidgetStats.swift +++ b/Modules/Sources/JetpackStatsWidgetsCore/Model/ThisWeekWidgetStats.swift @@ -1,39 +1,48 @@ import Foundation -import WordPressKit /// This struct contains data for 'Views This Week' stats to be displayed in the corresponding widget. /// -struct ThisWeekWidgetStats: Codable { - let days: [ThisWeekWidgetDay] +public struct ThisWeekWidgetStats: Codable { + public let days: [ThisWeekWidgetDay] - init(days: [ThisWeekWidgetDay]? = []) { + public init(days: [ThisWeekWidgetDay]? = []) { self.days = days ?? [] } } -struct ThisWeekWidgetDay: Codable, Hashable { - let date: Date - let viewsCount: Int - let dailyChangePercent: Float +public struct ThisWeekWidgetDay: Codable, Hashable { + public let date: Date + public let viewsCount: Int + public let dailyChangePercent: Float - init(date: Date, viewsCount: Int, dailyChangePercent: Float) { + public init(date: Date, viewsCount: Int, dailyChangePercent: Float) { self.date = date self.viewsCount = viewsCount self.dailyChangePercent = dailyChangePercent } } -extension ThisWeekWidgetStats { +public extension ThisWeekWidgetStats { + struct Input { + public let periodStartDate: Date + public let viewsCount: Int + + public init(periodStartDate: Date, viewsCount: Int) { + self.periodStartDate = periodStartDate + self.viewsCount = viewsCount + } + } + static var maxDaysToDisplay: Int { return 7 } - static func daysFrom(summaryData: [StatsSummaryData]) -> [ThisWeekWidgetDay] { + static func daysFrom(summaryData: [ThisWeekWidgetStats.Input]) -> [ThisWeekWidgetDay] { var days = [ThisWeekWidgetDay]() for index in 0.. Bool { + public static func == (lhs: ThisWeekWidgetStats, rhs: ThisWeekWidgetStats) -> Bool { return lhs.days.elementsEqual(rhs.days) } } extension ThisWeekWidgetDay: Equatable { - static func == (lhs: ThisWeekWidgetDay, rhs: ThisWeekWidgetDay) -> Bool { + public static func == (lhs: ThisWeekWidgetDay, rhs: ThisWeekWidgetDay) -> Bool { return lhs.date == rhs.date && lhs.viewsCount == rhs.viewsCount && lhs.dailyChangePercent == rhs.dailyChangePercent diff --git a/WordPress/JetpackStatsWidgets/Extensions/URL+WidgetSource.swift b/Modules/Sources/JetpackStatsWidgetsCore/URL+WidgetUrlSource.swift similarity index 76% rename from WordPress/JetpackStatsWidgets/Extensions/URL+WidgetSource.swift rename to Modules/Sources/JetpackStatsWidgetsCore/URL+WidgetUrlSource.swift index 071ce5966836..439a115ac7a2 100644 --- a/WordPress/JetpackStatsWidgets/Extensions/URL+WidgetSource.swift +++ b/Modules/Sources/JetpackStatsWidgetsCore/URL+WidgetUrlSource.swift @@ -1,11 +1,7 @@ import Foundation -enum WidgetUrlSource: String { - case homeScreenWidget = "widget" - case lockScreenWidget = "lockscreen_widget" -} +public extension URL { -extension URL { func appendingSource(_ source: WidgetUrlSource) -> URL { var components = URLComponents(url: self, resolvingAgainstBaseURL: false) var queryItems = components?.queryItems ?? [] diff --git a/Modules/Sources/JetpackStatsWidgetsCore/WidgetDataCacheReader.swift b/Modules/Sources/JetpackStatsWidgetsCore/WidgetDataCacheReader.swift new file mode 100644 index 000000000000..6298d9595d8b --- /dev/null +++ b/Modules/Sources/JetpackStatsWidgetsCore/WidgetDataCacheReader.swift @@ -0,0 +1,30 @@ +public protocol WidgetDataCacheReader { + func widgetData(for siteID: String) -> T? + func widgetData() -> [T]? +} + +public extension WidgetDataCacheReader { + + func widgetData( + forSiteIdentifier identifier: String?, + defaultSiteID: Int?, + userLoggedIn: Bool + ) -> Result { + if let selectedSite = identifier, let widgetData: T = widgetData(for: selectedSite) { + return .success(widgetData) + } else if let defaultSiteID, let widgetData: T = widgetData(for: String(defaultSiteID)) { + return .success(widgetData) + } else { + if userLoggedIn { + /// In rare cases there could be no default site and no defaultSiteId set + if let firstSiteData: T = widgetData()?.sorted(by: { $0.siteID < $1.siteID }).first { + return .success(firstSiteData) + } else { + return .failure(.noSite) + } + } else { + return .failure(.loggedOut) + } + } + } +} diff --git a/Modules/Sources/JetpackStatsWidgetsCore/WidgetDataReadError.swift b/Modules/Sources/JetpackStatsWidgetsCore/WidgetDataReadError.swift new file mode 100644 index 000000000000..2dcfc44dffd2 --- /dev/null +++ b/Modules/Sources/JetpackStatsWidgetsCore/WidgetDataReadError.swift @@ -0,0 +1,6 @@ +public enum WidgetDataReadError: Error, Equatable { + case jetpackFeatureDisabled + case noData + case noSite + case loggedOut +} diff --git a/Modules/Sources/JetpackStatsWidgetsCore/WidgetUrlSource.swift b/Modules/Sources/JetpackStatsWidgetsCore/WidgetUrlSource.swift new file mode 100644 index 000000000000..e12cd2ed3514 --- /dev/null +++ b/Modules/Sources/JetpackStatsWidgetsCore/WidgetUrlSource.swift @@ -0,0 +1,4 @@ +public enum WidgetUrlSource: String { + case homeScreenWidget = "widget" + case lockScreenWidget = "lockscreen_widget" +} diff --git a/Modules/Tests/JetpackStatsWidgetsCoreTests/ThisWeekWidgetStatsTests.swift b/Modules/Tests/JetpackStatsWidgetsCoreTests/ThisWeekWidgetStatsTests.swift new file mode 100644 index 000000000000..2d3fffb5911a --- /dev/null +++ b/Modules/Tests/JetpackStatsWidgetsCoreTests/ThisWeekWidgetStatsTests.swift @@ -0,0 +1,32 @@ +import XCTest +import JetpackStatsWidgetsCore + +final class ThisWeekWidgetStatsTests: XCTestCase { + func testDaysFromSummaryData_moreSummaryData() { + var summaryData: [ThisWeekWidgetStats.Input] = [] + + // Given there's summary data for more than max days to display + for _ in 0.. + + /// When the cache has data for the requested site, it returns it regardless of whether the user is logged in + func testWidgetDataForKnownSiteIdentifier() { + let data = HomeWidgetDataDouble(siteID: 1) + let sut = WidgetDataCacheReaderDouble(dataToReturn: data) + + assert( + result: sut.widgetData(forSiteIdentifier: "1", defaultSiteID: 2, userLoggedIn: false), + equals: .success(data) + ) + assert( + result: sut.widgetData(forSiteIdentifier: "1", defaultSiteID: nil, userLoggedIn: false), + equals: .success(data) + ) + assert( + result: sut.widgetData(forSiteIdentifier: "1", defaultSiteID: 2, userLoggedIn: true), + equals: .success(data) + ) + assert( + result: sut.widgetData(forSiteIdentifier: "1", defaultSiteID: nil, userLoggedIn: true), + equals: .success(data) + ) + } + + /// When the requested site id is not in the cache but the default site is, + /// it returns it regardless of the user logged in status + func testWidgetDataForNilSiteIdentifierWithDataForDefaultSite() { + let id = 1 + let data = HomeWidgetDataDouble(siteID: id) + let sut = WidgetDataCacheReaderDouble(dataToReturn: data) + + assert( + result: sut.widgetData(forSiteIdentifier: nil, defaultSiteID: id, userLoggedIn: true), + equals: .success(data) + ) + assert( + result: sut.widgetData(forSiteIdentifier: nil, defaultSiteID: id, userLoggedIn: false), + equals: .success(data) + ) + } + + /// When the requested site id is nil but the cache has data for the default site, + /// it returns it data for the default site, regardless of the user logged in status + func testWidgetDataForMissingSiteIdentifierWithDataForDefaultSite() { + let id = 1 + let data = HomeWidgetDataDouble(siteID: id) + let sut = WidgetDataCacheReaderDouble(dataToReturn: data) + + assert( + result: sut.widgetData(forSiteIdentifier: "2", defaultSiteID: id, userLoggedIn: true), + equals: .success(data) + ) + assert( + result: sut.widgetData(forSiteIdentifier: "2", defaultSiteID: id, userLoggedIn: false), + equals: .success(data) + ) + } + + /// When the cache has no data and the user is logged out, it always returns a failure with the "logged out" error + func testWidgetDataWithNoDataAndUserLoggedOut() { + let sut = WidgetDataCacheReaderDouble(dataToReturn: nil) + + assert( + result: sut.widgetData(forSiteIdentifier: "1", defaultSiteID: 2, userLoggedIn: false), + equals: .failure(.loggedOut) + ) + assert( + result: sut.widgetData(forSiteIdentifier: "1", defaultSiteID: nil, userLoggedIn: false), + equals: .failure(.loggedOut) + ) + assert( + result: sut.widgetData(forSiteIdentifier: nil, defaultSiteID: 2, userLoggedIn: false), + equals: .failure(.loggedOut) + ) + assert( + result: sut.widgetData(forSiteIdentifier: nil, defaultSiteID: nil, userLoggedIn: false), + equals: .failure(.loggedOut) + ) + } + + /// When the cache has no data and the user is logged in, it always returns a failure with the "no site" error + func testWidgetDataWithNoDataAndUserLoggedIn() { + let sut = WidgetDataCacheReaderDouble(dataToReturn: nil) + + assert( + result: sut.widgetData(forSiteIdentifier: "id", defaultSiteID: 1, userLoggedIn: true), + equals: .failure(.noSite) + ) + assert( + result: sut.widgetData(forSiteIdentifier: "id", defaultSiteID: nil, userLoggedIn: true), + equals: .failure(.noSite) + ) + assert( + result: sut.widgetData(forSiteIdentifier: nil, defaultSiteID: 1, userLoggedIn: true), + equals: .failure(.noSite) + ) + assert( + result: sut.widgetData(forSiteIdentifier: nil, defaultSiteID: nil, userLoggedIn: true), + equals: .failure(.noSite) + ) + } + + /// When the cache has data that doesn't match the input parameters, and the user is logged in, + /// it returns the site with the lower id + func testWidgetDataFallbackWhenLoggedIn() { + let data = HomeWidgetDataDouble(siteID: 0) + let sut = WidgetDataCacheReaderDouble( + dataToReturn: [ + // Notice the not-sorted order, to verify the SUT uses logic insted of picking the first in the list + HomeWidgetDataDouble(siteID: 2), + data, + HomeWidgetDataDouble(siteID: 1) + ] + ) + + assert( + result: sut.widgetData(forSiteIdentifier: "3", defaultSiteID: 4, userLoggedIn: true), + equals: .success(data) + ) + assert( + result: sut.widgetData(forSiteIdentifier: "3", defaultSiteID: nil, userLoggedIn: true), + equals: .success(data) + ) + assert( + result: sut.widgetData(forSiteIdentifier: nil, defaultSiteID: 4, userLoggedIn: true), + equals: .success(data) + ) + assert( + result: sut.widgetData(forSiteIdentifier: nil, defaultSiteID: nil, userLoggedIn: true), + equals: .success(data) + ) + } + + /// When the cache has data that doesn't match the input parameters, and the user is logged out, + /// it returns the "logged out" error + func testWidgetDataFallbackWhenLoggedOut() { + let sut = WidgetDataCacheReaderDouble( + dataToReturn: [ + HomeWidgetDataDouble(siteID: 2), + HomeWidgetDataDouble(siteID: 1) + ] + ) + + assert( + result: sut.widgetData(forSiteIdentifier: "3", defaultSiteID: 4, userLoggedIn: false), + equals: .failure(.loggedOut) + ) + assert( + result: sut.widgetData(forSiteIdentifier: "3", defaultSiteID: nil, userLoggedIn: false), + equals: .failure(.loggedOut) + ) + assert( + result: sut.widgetData(forSiteIdentifier: nil, defaultSiteID: 4, userLoggedIn: false), + equals: .failure(.loggedOut) + ) + assert( + result: sut.widgetData(forSiteIdentifier: nil, defaultSiteID: nil, userLoggedIn: false), + equals: .failure(.loggedOut) + ) + } + + // MARK: - + + func assert(result actual: Result, equals expected: Result, line: UInt = #line) { + switch (actual, expected) { + case (.success(let actualData), .success(let expectedData)): + XCTAssertEqual(actualData, expectedData, line: line) + case (.failure(let actualError), .failure(let expectedError)): + XCTAssertEqual(actualError, expectedError, line: line) + case (.success(let data), .failure): + XCTFail("Expected to fail but succeeded with \(data)", line: line) + case (.failure(let error), .success): + XCTFail("Expected to succeed but failed with \(error)", line: line) + } + } +} + +struct WidgetDataCacheReaderDouble: WidgetDataCacheReader { + + private let results: [StubbedResult] + + struct StubbedResult { + let id: String + let data: HomeWidgetDataDouble? + + init(id: String, data: HomeWidgetDataDouble?) { + self.id = id + self.data = data + } + + init(double: HomeWidgetDataDouble) { + self.init( + id: "\(double.siteID)", + data: double + ) + } + } + + init(dataToReturn: HomeWidgetDataDouble?) { + self.results = dataToReturn.map { [StubbedResult(double: $0)] } ?? [] + } + + init(dataToReturn: [HomeWidgetDataDouble]) { + self.results = dataToReturn.map { StubbedResult(double: $0) } + } + + init(results: [StubbedResult]) { + self.results = results + } + + func widgetData() -> [T]? { + results.map(\.data) as? [T] + } + + func widgetData(for siteID: String) -> T? where T: HomeWidgetData { + results.first(where: { $0.id == siteID })?.data as? T + } +} + +struct HomeWidgetDataDouble: HomeWidgetData, Equatable { + let siteID: Int + let siteName: String + let url: String + let timeZone: TimeZone + let date: Date + let statsURL: URL? + + static let filename: String = "test" + + init( + siteID: Int = 1, + siteName: String = "name", + url: String = "https://test.com", + timeZone: TimeZone = .current, // Would like to use .gmt (deterministic) once targeting 16+, + date: Date = Date(), // Would like to use .now (clearer) once targeting 15+ + statsURL: URL? = nil + ) { + self.siteID = siteID + self.siteName = siteName + self.url = url + self.timeZone = timeZone + self.date = date + self.statsURL = statsURL + } +} diff --git a/Podfile b/Podfile index b6e2b83649f4..f8e73a0cc86c 100644 --- a/Podfile +++ b/Podfile @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative './Gutenberg/cocoapods_helpers' +require_relative 'Gutenberg/cocoapods_helpers' require 'xcodeproj' # For security reasons, please always keep the wordpress-mobile source first and the CDN second. @@ -50,10 +50,11 @@ def wordpress_ui end def wordpress_kit - pod 'WordPressKit', '~> 8.7', '>= 8.7.1' - # pod 'WordPressKit', git: 'https://github.com/wordpress-mobile/WordPressKit-iOS.git', tag: '' - # pod 'WordPressKit', git: 'https://github.com/wordpress-mobile/WordPressKit-iOS.git', branch: '' + # Anything compatible with 8.9, starting from 8.9.1 which has a breaking change fix + pod 'WordPressKit', '~> 9.0', '>= 9.0.2' # pod 'WordPressKit', git: 'https://github.com/wordpress-mobile/WordPressKit-iOS.git', commit: '' + # pod 'WordPressKit', git: 'https://github.com/wordpress-mobile/WordPressKit-iOS.git', branch: 'trunk' + # pod 'WordPressKit', git: 'https://github.com/wordpress-mobile/WordPressKit-iOS.git', tag: '' # pod 'WordPressKit', path: '../WordPressKit-iOS' end @@ -123,7 +124,6 @@ abstract_target 'Apps' do pod 'FSInteractiveMap', git: 'https://github.com/wordpress-mobile/FSInteractiveMap.git', tag: '0.2.0' pod 'JTAppleCalendar', '~> 8.0.5' pod 'CropViewController', '2.5.3' - pod 'SDWebImage', '~> 5.11.1' ## Automattic libraries ## ==================== @@ -134,7 +134,7 @@ abstract_target 'Apps' do # Production - pod 'Automattic-Tracks-iOS', '~> 2.2' + pod 'Automattic-Tracks-iOS', '~> 3.0' # While in PR # pod 'Automattic-Tracks-iOS', git: 'https://github.com/Automattic/Automattic-Tracks-iOS.git', branch: '' # Local Development @@ -142,14 +142,9 @@ abstract_target 'Apps' do pod 'NSURL+IDN', '~> 0.4' - pod 'WPMediaPicker', '~> 1.8', '>= 1.8.10' - ## while PR is in review: - # pod 'WPMediaPicker', git: 'https://github.com/wordpress-mobile/MediaPicker-iOS.git', branch: '' - # pod 'WPMediaPicker', path: '../MediaPicker-iOS' - - pod 'WordPressAuthenticator', '~> 7.2' - # pod 'WordPressAuthenticator', git: 'https://github.com/wordpress-mobile/WordPressAuthenticator-iOS.git', branch: '' + pod 'WordPressAuthenticator', '~> 8.0', '>= 8.0.1' # pod 'WordPressAuthenticator', git: 'https://github.com/wordpress-mobile/WordPressAuthenticator-iOS.git', commit: '' + # pod 'WordPressAuthenticator', git: 'https://github.com/wordpress-mobile/WordPressAuthenticator-iOS.git', branch: '' # pod 'WordPressAuthenticator', path: '../WordPressAuthenticator-iOS' pod 'MediaEditor', '~> 1.2', '>= 1.2.2' @@ -410,3 +405,7 @@ post_install do |installer| reset_marker = "\033[0m" puts "#{yellow_marker}The abstract target warning below is expected. Feel free to ignore it.#{reset_marker}" end + +post_integrate do + gutenberg_post_integrate +end diff --git a/Podfile.lock b/Podfile.lock index 2ba8a7ade286..daab523c6c77 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -14,7 +14,7 @@ PODS: - AppCenter/Core - AppCenter/Distribute (5.0.3): - AppCenter/Core - - Automattic-Tracks-iOS (2.4.0): + - Automattic-Tracks-iOS (3.2.0): - Sentry (~> 8.0) - Sodium (>= 0.9.1) - UIDeviceIdentifier (~> 2.0) @@ -26,7 +26,7 @@ PODS: - FSInteractiveMap (0.1.0) - Gifu (3.3.1) - Gridicons (1.2.0) - - Gutenberg (1.106.0) + - Gutenberg (1.110.0) - JTAppleCalendar (8.0.5) - Kanvas (1.4.9): - CropViewController @@ -49,15 +49,12 @@ PODS: - OHHTTPStubs/Swift (9.1.0): - OHHTTPStubs/Default - Reachability (3.2) - - SDWebImage (5.11.1): - - SDWebImage/Core (= 5.11.1) - - SDWebImage/Core (5.11.1) - - Sentry (8.13.1): - - Sentry/Core (= 8.13.1) - - SentryPrivate (= 8.13.1) - - Sentry/Core (8.13.1): - - SentryPrivate (= 8.13.1) - - SentryPrivate (8.13.1) + - Sentry (8.18.0): + - Sentry/Core (= 8.18.0) + - SentryPrivate (= 8.18.0) + - Sentry/Core (8.18.0): + - SentryPrivate (= 8.18.0) + - SentryPrivate (8.18.0) - Sodium (0.9.1) - Starscream (3.0.6) - SVProgressHUD (2.2.5) @@ -66,14 +63,14 @@ PODS: - WordPress-Aztec-iOS (1.19.9) - WordPress-Editor-iOS (1.19.9): - WordPress-Aztec-iOS (= 1.19.9) - - WordPressAuthenticator (7.2.0): + - WordPressAuthenticator (8.0.1): - Gridicons (~> 1.0) - "NSURL+IDN (= 0.4)" - SVProgressHUD (~> 2.2.5) - - WordPressKit (~> 8.7-beta) + - WordPressKit (~> 9.0.0) - WordPressShared (~> 2.1-beta) - WordPressUI (~> 1.7-beta) - - WordPressKit (8.7.1): + - WordPressKit (9.0.2): - Alamofire (~> 4.8.0) - NSObject-SafeExpectations (~> 0.0.4) - UIDeviceIdentifier (~> 2.0) @@ -81,7 +78,6 @@ PODS: - wpxmlrpc (~> 0.10) - WordPressShared (2.2.0) - WordPressUI (1.15.0) - - WPMediaPicker (1.8.10) - wpxmlrpc (0.10.0) - ZendeskCommonUISDK (6.1.2) - ZendeskCoreSDK (2.5.1) @@ -104,14 +100,14 @@ DEPENDENCIES: - AlamofireNetworkActivityIndicator (~> 2.4) - AppCenter (~> 5.0) - AppCenter/Distribute (~> 5.0) - - Automattic-Tracks-iOS (~> 2.2) + - Automattic-Tracks-iOS (~> 3.0) - CocoaLumberjack/Swift (~> 3.0) - CropViewController (= 2.5.3) - Down (~> 0.6.6) - FSInteractiveMap (from `https://github.com/wordpress-mobile/FSInteractiveMap.git`, tag `0.2.0`) - Gifu (= 3.3.1) - Gridicons (~> 1.2) - - Gutenberg (from `https://cdn.a8c-ci.services/gutenberg-mobile/Gutenberg-v1.106.0.podspec`) + - Gutenberg (from `https://cdn.a8c-ci.services/gutenberg-mobile/Gutenberg-v1.110.0.podspec`) - JTAppleCalendar (~> 8.0.5) - Kanvas (~> 1.4.4) - MediaEditor (>= 1.2.2, ~> 1.2) @@ -120,16 +116,14 @@ DEPENDENCIES: - OCMock (~> 3.4.3) - OHHTTPStubs/Swift (~> 9.1.0) - Reachability (= 3.2) - - SDWebImage (~> 5.11.1) - Starscream (= 3.0.6) - SVProgressHUD (= 2.2.5) - SwiftLint (~> 0.50) - WordPress-Editor-iOS (~> 1.19.9) - - WordPressAuthenticator (~> 7.2) - - WordPressKit (>= 8.7.1, ~> 8.7) + - WordPressAuthenticator (>= 8.0.1, ~> 8.0) + - WordPressKit (>= 9.0.2, ~> 9.0) - WordPressShared (~> 2.2) - WordPressUI (~> 1.15) - - WPMediaPicker (>= 1.8.10, ~> 1.8) - ZendeskSupportSDK (= 5.3.0) - ZIPFoundation (~> 0.9.8) @@ -155,7 +149,6 @@ SPEC REPOS: - OCMock - OHHTTPStubs - Reachability - - SDWebImage - Sentry - SentryPrivate - Sodium @@ -168,7 +161,6 @@ SPEC REPOS: - WordPressKit - WordPressShared - WordPressUI - - WPMediaPicker - wpxmlrpc - ZendeskCommonUISDK - ZendeskCoreSDK @@ -184,7 +176,7 @@ EXTERNAL SOURCES: :git: https://github.com/wordpress-mobile/FSInteractiveMap.git :tag: 0.2.0 Gutenberg: - :podspec: https://cdn.a8c-ci.services/gutenberg-mobile/Gutenberg-v1.106.0.podspec + :podspec: https://cdn.a8c-ci.services/gutenberg-mobile/Gutenberg-v1.110.0.podspec CHECKOUT OPTIONS: FSInteractiveMap: @@ -200,14 +192,14 @@ SPEC CHECKSUMS: AlamofireImage: 63cfe3baf1370be6c498149687cf6db3e3b00999 AlamofireNetworkActivityIndicator: 9acc3de3ca6645bf0efed462396b0df13dd3e7b8 AppCenter: a4070ec3d4418b5539067a51f57155012e486ebd - Automattic-Tracks-iOS: eb06b138cd65453dc565ab047f55fbb759aca133 + Automattic-Tracks-iOS: baa126f98d2ce26fd54ee2534bef6e2d46480a5c CocoaLumberjack: 78abfb691154e2a9df8ded4350d504ee19d90732 CropViewController: a5c143548a0fabcd6cc25f2d26e40460cfb8c78c Down: 71bf4af3c04fa093e65dffa25c4b64fa61287373 FSInteractiveMap: a396f610f48b76cb540baa87139d056429abda86 Gifu: 416d4e38c4c2fed012f019e0a1d3ffcb58e5b842 Gridicons: 4455b9f366960121430e45997e32112ae49ffe1d - Gutenberg: 46edef6239e4fa8c032aee57f67c9a258edf0cc1 + Gutenberg: 0e64ef7d9c46ba0a681c82f5969622f3db9bf033 JTAppleCalendar: 16c6501b22cb27520372c28b0a2e0b12c8d0cd73 Kanvas: cc027f8058de881a4ae2b5aa5f05037b6d054d08 MediaEditor: d08314cfcbfac74361071a306b4bc3a39b3356ae @@ -216,9 +208,8 @@ SPEC CHECKSUMS: OCMock: 43565190abc78977ad44a61c0d20d7f0784d35ab OHHTTPStubs: 90eac6d8f2c18317baeca36698523dc67c513831 Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96 - SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d - Sentry: 790330d900f10169ef0faeeceadc9c698bcef857 - SentryPrivate: 03298c944ad388dc3d95183e62c7e46ebf5009f3 + Sentry: 8984a4ffb2b9bd2894d74fb36e6f5833865bc18e + SentryPrivate: 2f0c9ba4c3fc993f70eab6ca95673509561e0085 Sodium: 23d11554ecd556196d313cf6130d406dfe7ac6da Starscream: ef3ece99d765eeccb67de105bfa143f929026cf5 SVProgressHUD: 1428aafac632c1f86f62aa4243ec12008d7a51d6 @@ -226,11 +217,10 @@ SPEC CHECKSUMS: UIDeviceIdentifier: 442b65b4ff1832d4ca9c2a157815cb29ad981b17 WordPress-Aztec-iOS: fbebd569c61baa252b3f5058c0a2a9a6ada686bb WordPress-Editor-iOS: bda9f7f942212589b890329a0cb22547311749ef - WordPressAuthenticator: ed6f3e3b1a3f720761df62ca361d0152b366abac - WordPressKit: 33a0571389da6b40c765398a8c84da72f5514a6f + WordPressAuthenticator: fd2e1d340680faffffd9d675fc2df5ed19e26ea2 + WordPressKit: 23d0ffb43f2ccdad2debd6799e62d39790a5ffad WordPressShared: 87f3ee89b0a3e83106106f13a8b71605fb8eb6d2 WordPressUI: a491454affda3b0fb812812e637dc5e8f8f6bd06 - WPMediaPicker: 332812329cbdc672cdb385b8ac3a389f668d3012 wpxmlrpc: 68db063041e85d186db21f674adf08d9c70627fd ZendeskCommonUISDK: 5f0a83f412e07ae23701f18c412fe783b3249ef5 ZendeskCoreSDK: 19a18e5ef2edcb18f4dbc0ea0d12bd31f515712a @@ -241,6 +231,6 @@ SPEC CHECKSUMS: ZendeskSupportSDK: 3a8e508ab1d9dd22dc038df6c694466414e037ba ZIPFoundation: d170fa8e270b2a32bef9dcdcabff5b8f1a5deced -PODFILE CHECKSUM: aa4d15c9fb591659b821430e53acf989a4f85f1b +PODFILE CHECKSUM: 9567ce349333fd257fea44c201727b4180efb961 -COCOAPODS: 1.12.1 +COCOAPODS: 1.14.2 diff --git a/README.md b/README.md index 94abc77b03bb..57ff7820799b 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,20 @@ # WordPress for iOS # -[![Build status](https://badge.buildkite.com/2f3fbb17bfbb5bba508efd80f1ea8d640db5ca2465a516a457.svg)](https://buildkite.com/automattic/wordpress-ios) -[![Reviewed by Hound](https://img.shields.io/badge/Reviewed_by-Hound-8E64B0.svg)](https://houndci.com) - ## Build Instructions -Please refer to the sections below for more detailed information. The instructions assume the work is performed from a command line. - -> Please note – these setup instructions only apply to Intel-based machines. M1-based Mac support is coming, but isn't yet supported by our tooling. +Please refer to the sections below for more detailed information. The instructions assume the work is performed from a command line inside the repository. ### Getting Started -1. [Download](https://developer.apple.com/downloads/index.action) and install Xcode. *WordPress for iOS* requires Xcode 11.2.1 or newer. -1. From a command line, run `git clone git@github.com:wordpress-mobile/WordPress-iOS.git` in the folder of your preference. -1. Now, run `cd WordPress-iOS` to enter the working directory. +1. [Download](https://developer.apple.com/downloads/index.action) and install Xcode. Refer to the [.xcode-version](./.xcode-version) file for the minimum required version. +1. [Clone this repository](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository) in the folder of your preference. #### Create WordPress.com API Credentials 1. Create a WordPress.com account at https://wordpress.com/start/user (if you don't already have one). 1. Create an application at https://developer.wordpress.com/apps/. -1. Set "Redirect URLs"= `https://localhost` and "Type" = `Native` and click "Create" then "Update". +1. Set **Website URL** to any valid host, **Redirect URLs** to `https://localhost`, and **Type** to `Native`. +1. Click "Create" then "Update". 1. Copy the `Client ID` and `Client Secret` from the OAuth Information. #### Configure Your WordPress App Development Environment diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 9a908cffc083..1082c73dc706 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -1,6 +1,107 @@ +24.1 +----- + + +24.0 +----- +* [**] [internal] A minor refactor in authentication flow, including but not limited to social sign-in and two factor authentication. [#22086] +* [***] [Jetpack-only] Plans: Upgrade to a WPCOM plan from domains dashboard in Jetpack app. [#22261] +* [**] [internal] Refactor domain selection flows to use the same domain selection UI. [22254] +* [**] Re-enable the support for using Security Keys as a second factor during login [#22258] +* [*] Fix crash in editor that sometimes happens after modifying tags or categories [#22265] +* [**] Updated login screen's colors to highlight WordPress - Jetpack brand relationship +* [*] Add defensive code to make sure the retain cycles in the editor don't lead to crashes [#22252] +* [*] [Jetpack-only] Updated Site Domains screen to make domains management more convenient [#22294, #22311] +* [**] [internal] [Jetpack-only] Adds support for dynamic dashboard cards driven by the backend [#22326] +* [**] [internal] Add support for the Phase One Fast Media Uploads banner [#22330] +* [*] [internal] Remove personalizeHomeTab feature flag [#22280] +* [*] Fix a rare crash in post search related to tags [#22275] +* [*] Fix a rare crash when deleting posts [#22277] +* [*] Fix a rare crash in Site Media prefetching cancellation [#22278] +* [*] Fix an issue with BlogDashboardPersonalizationService being used on the background thread [#22335] +* [***] Block Editor: Avoid keyboard dismiss when interacting with text blocks [https://github.com/WordPress/gutenberg/pull/57070] +* [**] Block Editor: Auto-scroll upon block insertion [https://github.com/WordPress/gutenberg/pull/57273] + +23.9 +----- +* [**] Updates the My Site header to show site actions in a context menu [#22151] +* [**] Add media fitlers to the Site Media screen [#22096] +* [*] The "aspect ratio" mode on the Site Media screen is now also available on iPhone via the new title menu [#22096] +* [**] Update the classic editor to use the new Photos and Site Media pickers [#22060] +* [**] [internal] Remove WPMediaPicker dependency [#22103] +* [*] [internal] Rework Tenor (Free GIF) and Stock Photos (Free Photos) pickers [#22066, #22074] +* [*] [internal] Remove MediaThumbnailService and reduce the size of the large thumbnails, reducing disk usage [#22106] +* [*] [Jetpack-only] Fix an occasional crash when changing Reader comment status [#22155] +* [*] [Jetpack-only] Fix an occasional crash when logging out after interacting with Reader [#22147] +* [*] Fix an issue where the Compliance Popover breaks other screens presentation. [#22085] +* [*] [Jetpack-only] Fix an occassional crash when logging out after interacting with Reader [#22147] +* [*] [Jetpack-only] Fix an issue where VoiceOver could lose focus when liking posts in Reader. [#22232] + +23.8 +----- +* [**] Add Optimize Images setting for image uploads and enable it by default [#21981] +* [*] Fix the media item details screen layout on iPad [#22042] +* [*] Improve the performance of loading thumbnails and original images in the Site Media screen [#22043] +* [*] Integrate native photos picker (`PHPickerViewController`) in Story Editor [#22059] +* [*] Fix an issue [#21959] where WordPress → Jetpack migration was not working for accounts with no sites. These users are now presented with a shortened migration flow. The "Uninstall WordPress" prompt will now also appear only as a card on the Dashboard. [#22064] +* [*] Add Select and Deselect button to previews in Site Media picker [#22078] +* [*] [internal] Fix an issue with scheduling of posts not working on iOS 17 with Xcode 15 [#22012] +* [*] [internal] Remove SDWebImage dependency from the app and improve cache cost calculation for GIFs [#21285] +* [*] Stats: Fix an issue where sites for clicked URLs do not open [#22061] +* [*] Improve pages list performance when there are hundreds of pages in the site [#22070] +* [*] Fix an issue with local thumbnails for GIFs inserted to Site Media not being animated [#22083] +* [*] [internal] Make Reader web views inspectable on iOS 16.4 and higher [#22077] +* [*] [internal] Add workarounds for large emoji on P2. [#22080] +* [*] [Jetpack-only] Block Editor: Ensure text is always visible within Contact Info block [https://github.com/Automattic/jetpack/pull/33873] +* [*] Block Editor: Ensure uploaded audio is always visible within Audio block [https://github.com/WordPress/gutenberg/pull/55627] +* [*] Block Editor: In the deeply nested block warning, only display the ungroup option for blocks that support it [https://github.com/WordPress/gutenberg/pull/56445] +* [**] Refactor deleting media [#21748] +* [*] [Jetpack-only] Add a dashboard card for Bloganuary. [https://github.com/wordpress-mobile/WordPress-iOS/pull/22136] +* [*] Fix an issue where the Compliance Popover breaks other screens presentation. [#22085] + +23.7 +----- +* [**] Posts & Pages: Redesigned the posts and pages screen. We’ve consolidated the “default” and “compact” display options (#21804, #21842) +* [*] Posts & Pages: Moved actions to a context menu and added new actions such as “Comments”, “Settings”, and “Set as regular page”. Made the context menu available via long-press (#21886, #21965, #21963, #21967) +* [*] Posts & Pages: Added swipe actions - left to view, right to share and/or delete (#21917) +* [***] Posts & Pages: Added paging, full-text search, and searching via “author” / “tag” filters (#21789) +* [*] Posts & Pages: Search now works across all authors unless you explicitly add the author filter (#21966) +* [*] Posts & Pages: Fix an issue with the Pages list not refreshing when the pages are added or modified +* [*] Posts & Pages: Fix an issue where “View” action was available for Trashed posts (#21958) +* [*] Posts & Pages: Fix rare crashes in Posts & Pages (#21298) +* [***] Site Media: Update the design of the Site Media screen with an improved selection mode, updated context menus, a way to share more than one item at a time, better support for animated GIFs, fix a couple of visual issues with state views and search, and more [#21457] +* [**] Site Media: Improve performance by moving the work to the background, reducing memory usage, prefetching images, decompressing jpegs in the background, canceling unneeded requests, and more [#21470], [#21615], [#21664] +* [**] Site Media: Add support for selecting site media with a pan gesture [#21702] +* [*] Site Media: Add storage quota shown proactively in the context menu when adding media [#22013] +* [*] Site Media: Add aspect ratio mode to Site Media on iPad, which is a new default [#22009] +* [**] Site Media: Update the design of the Site Media details view that now allows swiping between photos, makes it easier to modify metadata, and delete items [#22008] +* [*] Site Media: Fix an issue with blank image placeholders on the Site Media screen [#21457] +* [*] Site Media: Fix an issue with 'you have no media' appears just before the media does [#9922] [#21457] +* [*] Site Media: Fix an issue with media occasionally flashing white on the Site Media screen +* [*] Site Media: Fix rare crashes in the Site Media screen and media picker [#21572] +* [*] Site Media: Fix an issue with sharing PDF and other documents [#22021] +* [*] Bug fix: Reader now scrolls to the top when tapping the status bar. [#21914] +* [*] Fix an issue with incorrect description for "Hidden" post privacy status [#21955] +* [*] [internal] Refactor sending the API requests for searching posts and pages. [#21976] +* [*] Fix an issue in Menu screen where it fails to create default menu items. [#21949] +* [*] [internal] Refactor how site's pages are loaded in Site Settings -> Homepage Settings. [#21974] +* [*] Block Editor: Fix error when pasting deeply nested structure content [https://github.com/WordPress/gutenberg/pull/55613] +* [*] Block Editor: Fix crash related to accessing undefined value in `TextColorEdit` [https://github.com/WordPress/gutenberg/pull/55664] +* [***] [Jetpack-only] Added the All Domains screen enabling the users to manage their domains from within the app [#22033] + 23.6 ----- - +* [***] Added support for logging in with security keys [#22001] +* [***] [Jetpack-only] Added paid domain selection, plan selection, and checkout screens in site creation flow [#21688] +* [**] When moving a post to trash, show a toast message with undo action instead of an inline undo row. [#21724] +* [*] Site Domains: Fixed an issue where the message shared while adding a domain was inaccurate. [#21827] +* [*] Fix an issue where login with site address is blocked after failing the first attempt. [#21848] +* [*] Fix an issue with an issue [#16999] with HTML not being stripped from post titles [#21846] +* [*] Fix an issue that leads to an ambiguous error message when an incorrect SMS 2FA code is submitted. [#21863] +* [*] Fix an issue where two 2FA controllers were being opened at the same time when logging in. [#21865] +* [*] Block Editor Social Icons: Fix visibility of inactive icons when used with block based themes in dark mode [https://github.com/WordPress/gutenberg/pull/55398] +* [*] Block Editor Classic block: Add option to convert to blocks [https://github.com/WordPress/gutenberg/pull/55461] +* [*] Block Editor Synced Patterns: Fix visibility of heading section when used with block based themes in dark mode [https://github.com/WordPress/gutenberg/pull/55399] 23.5 ----- @@ -11,7 +112,6 @@ * [*] (Internal) Remove .nativePhotoPicker feature flag and the disabled code [#21681](https://github.com/wordpress-mobile/WordPress-iOS/pull/21681) * [*] [WordPress-only] fixes an issue where users attempting to create a .com site in the post-sign-up flow are presented with two consecutive overlays. [#21752] * [**] [Jetpack-only] Reader: Improvement of core UI elements, including feed cards, tag and site headers, buttons and recommendation sections. [#21772] -* [***] [internal][Jetpack-only] [***] Added paid domain selection, plan selection, and checkout screens in site creation flow [#21688] 23.4 ----- diff --git a/WordPress.xcworkspace/contents.xcworkspacedata b/WordPress.xcworkspace/contents.xcworkspacedata index eca405337189..72ba573a45b5 100644 --- a/WordPress.xcworkspace/contents.xcworkspacedata +++ b/WordPress.xcworkspace/contents.xcworkspacedata @@ -4,6 +4,9 @@ + + diff --git a/WordPress.xcworkspace/xcshareddata/swiftpm/Package.resolved b/WordPress.xcworkspace/xcshareddata/swiftpm/Package.resolved index bd99391cc337..792e92a50c88 100644 --- a/WordPress.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/WordPress.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -11,12 +11,12 @@ } }, { - "package": "Charts", + "package": "DGCharts", "repositoryURL": "https://github.com/danielgindi/Charts", "state": { "branch": null, - "revision": "07b23476ad52b926be772f317d8f1d4511ee8d02", - "version": "4.1.0" + "revision": "0a229f8c914b0ec93798cee058cf75b339297513", + "version": "5.0.0" } }, { @@ -64,24 +64,6 @@ "version": "0.2.3" } }, - { - "package": "swift-algorithms", - "repositoryURL": "https://github.com/apple/swift-algorithms", - "state": { - "branch": null, - "revision": "b14b7f4c528c942f121c8b860b9410b2bf57825e", - "version": "1.0.0" - } - }, - { - "package": "swift-numerics", - "repositoryURL": "https://github.com/apple/swift-numerics", - "state": { - "branch": null, - "revision": "0a5bc04095a675662cf24757cc0640aa2204253b", - "version": "1.0.2" - } - }, { "package": "BuildkiteTestCollector", "repositoryURL": "https://github.com/buildkite/test-collector-swift", diff --git a/WordPress/Classes/Categories/Media+WPMediaAsset.h b/WordPress/Classes/Categories/Media+Extensions.h similarity index 53% rename from WordPress/Classes/Categories/Media+WPMediaAsset.h rename to WordPress/Classes/Categories/Media+Extensions.h index 3ef121b83b02..b027f57f573e 100644 --- a/WordPress/Classes/Categories/Media+WPMediaAsset.h +++ b/WordPress/Classes/Categories/Media+Extensions.h @@ -1,13 +1,15 @@ #import "Media.h" -#import +#import +#import -/** - Provides an implementation of Media conforming to the WPMediaAsset protocol. - Note: Doesn't play nicely with Swift, see @property filename below. - */ -@interface Media (WPMediaAsset) +@interface Media (Extensions) + +- (void)videoAssetWithCompletionHandler:(void (^ _Nonnull)(AVAsset * _Nullable asset, NSError * _Nullable error))completionHandler; -/** +- (CGSize)pixelSize; +- (NSTimeInterval)duration; + +/** Note: Redefine the filename property of Media to keep Swift happy. Otherwise, currently, Swift will only see the protocol method of filename() available, and not the (getter, setter) properity itself on Media. diff --git a/WordPress/Classes/Categories/Media+WPMediaAsset.m b/WordPress/Classes/Categories/Media+Extensions.m similarity index 64% rename from WordPress/Classes/Categories/Media+WPMediaAsset.m rename to WordPress/Classes/Categories/Media+Extensions.m index 27f519d678c8..27cb0ce9364f 100644 --- a/WordPress/Classes/Categories/Media+WPMediaAsset.m +++ b/WordPress/Classes/Categories/Media+Extensions.m @@ -1,41 +1,27 @@ -#import "Media+WPMediaAsset.h" +#import "Media+Extensions.h" #import "MediaService.h" #import "Blog.h" #import "CoreDataStack.h" #import "WordPress-Swift.h" -@implementation Media(WPMediaAsset) +@implementation Media (Extensions) -- (WPMediaRequestID)imageWithSize:(CGSize)size completionHandler:(WPMediaImageBlock)completionHandler -{ - [MediaThumbnailCoordinator.shared thumbnailFor:self with:size onCompletion:^(UIImage *image, NSError *error) { - dispatch_async(dispatch_get_main_queue(), ^{ - if (error) { - if (completionHandler) { - completionHandler(nil, error); - } - return; - } - if (completionHandler) { - completionHandler(image, nil); - } - }); - }]; - - return [self.mediaID intValue]; +- (NSError *)errorWithMessage:(NSString *)errorMessage { + return [NSError errorWithDomain:@"MediaErrorDomain" + code:4 + userInfo:@{NSLocalizedDescriptionKey: errorMessage}]; } -- (WPMediaRequestID)videoAssetWithCompletionHandler:(WPMediaAssetBlock)completionHandler -{ +- (void)videoAssetWithCompletionHandler:(void (^ _Nonnull)(AVAsset * _Nullable asset, NSError * _Nullable error))completionHandler { if (!completionHandler) { - return 0; + return; } // Check if asset being used is a video, if not this method fails - if (!(self.assetType == WPMediaTypeVideo || self.assetType == WPMediaTypeAudio)) { + if (!(self.mediaType == MediaTypeVideo || self.mediaType == MediaTypeAudio)) { NSString *errorMessage = NSLocalizedString(@"Selected media is not a video.", @"Error message when user tries to preview an image media like a video"); completionHandler(nil, [self errorWithMessage:errorMessage]); - return 0; + return; } NSURL *url = nil; @@ -45,7 +31,7 @@ - (WPMediaRequestID)videoAssetWithCompletionHandler:(WPMediaAssetBlock)completio } if (!url && self.videopressGUID.length > 0 ){ - id mediaServiceRemote = [[MediaServiceRemoteFactory new] remoteForBlog:self.blog]; + id mediaServiceRemote = [[MediaServiceRemoteFactory new] remoteForBlog:self.blog error:nil]; [mediaServiceRemote getMetadataFromVideoPressID:self.videopressGUID isSitePrivate:self.blog.isPrivate success:^(RemoteVideoPressVideo *metadata) { // Let see if can create an asset with this url NSURL *originalURL = metadata.originalURL; @@ -66,7 +52,7 @@ - (WPMediaRequestID)videoAssetWithCompletionHandler:(WPMediaAssetBlock)completio } failure:^(NSError *error) { completionHandler(nil, error); }]; - return 0; + return; } // Do we have a local url, or remote url to use for the video if (!url && self.remoteURL) { @@ -76,7 +62,7 @@ - (WPMediaRequestID)videoAssetWithCompletionHandler:(WPMediaAssetBlock)completio if (!url) { NSString *errorMessage = NSLocalizedString(@"Selected media is unavailable.", @"Error message when user tries a no longer existent video media object."); completionHandler(nil, [self errorWithMessage:errorMessage]); - return 0; + return; } // Let see if can create an asset with this url @@ -84,17 +70,10 @@ - (WPMediaRequestID)videoAssetWithCompletionHandler:(WPMediaAssetBlock)completio if (!asset) { NSString *errorMessage = NSLocalizedString(@"Selected media is unavailable.", @"Error message when user tries a no longer existent video media object."); completionHandler(nil, [self errorWithMessage:errorMessage]); - return 0; + return; } completionHandler(asset, nil); - return [self.mediaID intValue]; -} - -- (NSError *)errorWithMessage:(NSString *)errorMessage { - return [NSError errorWithDomain:WPMediaPickerErrorDomain - code:WPMediaPickerErrorCodeVideoURLNotAvailable - userInfo:@{NSLocalizedDescriptionKey:errorMessage}]; } - (CGSize)pixelSize @@ -102,24 +81,6 @@ - (CGSize)pixelSize return CGSizeMake([self.width floatValue], [self.height floatValue]); } -- (void)cancelImageRequest:(WPMediaRequestID)requestID -{ - -} - -- (WPMediaType)assetType -{ - if (self.mediaType == MediaTypeImage) { - return WPMediaTypeImage; - } else if (self.mediaType == MediaTypeVideo) { - return WPMediaTypeVideo; - } else if (self.mediaType == MediaTypeAudio) { - return WPMediaTypeAudio; - } else { - return WPMediaTypeOther; - } -} - - (NSTimeInterval)duration { if (!(self.mediaType == MediaTypeVideo || self.mediaType == MediaTypeAudio)) { @@ -139,28 +100,4 @@ - (NSTimeInterval)duration return CMTimeGetSeconds(duration); } -- (NSDate *)date -{ - return self.creationDate; -} - -- (id)baseAsset -{ - return self; -} - -- (NSString *)identifier -{ - return [[self.objectID URIRepresentation] absoluteString]; -} - -- (NSString *)UTTypeIdentifier -{ - NSString *extension = [self fileExtension]; - if (!extension.length) { - return nil; - } - return [UTType typeWithFilenameExtension:extension].identifier; -} - @end diff --git a/WordPress/Classes/Categories/WPStyleGuide+ReadableMargins.h b/WordPress/Classes/Categories/WPStyleGuide+ReadableMargins.h deleted file mode 100644 index dd72f8f51d04..000000000000 --- a/WordPress/Classes/Categories/WPStyleGuide+ReadableMargins.h +++ /dev/null @@ -1,8 +0,0 @@ -#import - -@interface WPStyleGuide (ReadableMargins) - - -+ (void)resetReadableMarginsForTableView:(UITableView *)tableView __deprecated_msg("Follow readable margins via constraints or instead explicitly set setCellLayoutMarginsFollowReadableWidth on UITableView."); - -@end diff --git a/WordPress/Classes/Categories/WPStyleGuide+ReadableMargins.m b/WordPress/Classes/Categories/WPStyleGuide+ReadableMargins.m deleted file mode 100644 index 28a35b2fd588..000000000000 --- a/WordPress/Classes/Categories/WPStyleGuide+ReadableMargins.m +++ /dev/null @@ -1,14 +0,0 @@ -#import "WPStyleGuide+ReadableMargins.h" - -@implementation WPStyleGuide (ReadableMargins) - -+ (void)resetReadableMarginsForTableView:(UITableView *)tableView -{ - // By default, iOS 9 sets cellLayoutMarginsFollowReadableWidth = YES. - // This conflicts with our desired layout margins, so set it to NO. - if ([tableView respondsToSelector:@selector(setCellLayoutMarginsFollowReadableWidth:)]) { - [tableView setCellLayoutMarginsFollowReadableWidth:NO]; - } -} - -@end diff --git a/WordPress/Classes/Extensions/Colors and Styles/UIColor+MurielColors.swift b/WordPress/Classes/Extensions/Colors and Styles/UIColor+MurielColors.swift index 1d5817c6324c..e42bfb8d7fa1 100644 --- a/WordPress/Classes/Extensions/Colors and Styles/UIColor+MurielColors.swift +++ b/WordPress/Classes/Extensions/Colors and Styles/UIColor+MurielColors.swift @@ -43,13 +43,9 @@ extension UIColor { /// Muriel brand color static var brand = muriel(color: .brand) - class func brand(_ shade: MurielColorShade) -> UIColor { - return muriel(color: .brand, shade) - } /// Muriel error color static var error = muriel(color: .error) - static var errorDark = muriel(color: .error, .shade70) class func error(_ shade: MurielColorShade) -> UIColor { return muriel(color: .error, shade) } @@ -64,15 +60,9 @@ extension UIColor { /// Muriel editor primary color static var editorPrimary = muriel(color: .editorPrimary) - class func editorPrimary(_ shade: MurielColorShade) -> UIColor { - return muriel(color: .editorPrimary, shade) - } /// Muriel success color static var success = muriel(color: .success) - class func success(_ shade: MurielColorShade) -> UIColor { - return muriel(color: .success, shade) - } /// Muriel warning color static var warning = muriel(color: .warning) @@ -197,10 +187,6 @@ extension UIColor { return .separator } - static var primaryButtonBorder: UIColor { - return .opaqueSeparator - } - /// WP color for table foregrounds (cells, etc) static var listForeground: UIColor { return .secondarySystemGroupedBackground @@ -232,10 +218,6 @@ extension UIColor { return .systemGray } - static var buttonIcon: UIColor { - return .systemGray2 - } - /// For icons that are present in a toolbar or similar view static var toolbarInactive: UIColor { return .secondaryLabel diff --git a/WordPress/Classes/Extensions/Colors and Styles/WPStyleGuide+Aztec.swift b/WordPress/Classes/Extensions/Colors and Styles/WPStyleGuide+Aztec.swift index 1113a7253722..eaf58fd5cc42 100644 --- a/WordPress/Classes/Extensions/Colors and Styles/WPStyleGuide+Aztec.swift +++ b/WordPress/Classes/Extensions/Colors and Styles/WPStyleGuide+Aztec.swift @@ -11,8 +11,6 @@ extension WPStyleGuide { static let aztecFormatBarDividerColor: UIColor = .divider - static let aztecFormatBarBackgroundColor = UIColor.basicBackground - static var aztecFormatPickerSelectedCellBackgroundColor: UIColor { get { return (UIDevice.isPad()) ? .neutral(.shade0) : .neutral(.shade5) diff --git a/WordPress/Classes/Extensions/Colors and Styles/WPStyleGuide+Search.swift b/WordPress/Classes/Extensions/Colors and Styles/WPStyleGuide+Search.swift index ebad5bb1c5a6..b9a854a84ff0 100644 --- a/WordPress/Classes/Extensions/Colors and Styles/WPStyleGuide+Search.swift +++ b/WordPress/Classes/Extensions/Colors and Styles/WPStyleGuide+Search.swift @@ -4,8 +4,6 @@ import UIKit extension WPStyleGuide { - fileprivate static let barTintColor: UIColor = .neutral(.shade10) - public class func configureSearchBar(_ searchBar: UISearchBar, backgroundColor: UIColor, returnKeyType: UIReturnKeyType) { searchBar.accessibilityIdentifier = "Search" searchBar.autocapitalizationType = .none diff --git a/WordPress/Classes/Extensions/Interpolation.swift b/WordPress/Classes/Extensions/Interpolation.swift index ae019dca6a7c..775eb498017e 100644 --- a/WordPress/Classes/Extensions/Interpolation.swift +++ b/WordPress/Classes/Extensions/Interpolation.swift @@ -1,10 +1,6 @@ import Foundation extension CGFloat { - static func interpolated(from fromValue: CGFloat, to toValue: CGFloat, progress: CGFloat) -> CGFloat { - return fromValue.interpolated(to: toValue, with: progress) - } - /// Interpolates a CGFloat /// - Parameters: /// - toValue: The to value diff --git a/WordPress/Classes/Extensions/Media+Sync.swift b/WordPress/Classes/Extensions/Media+Sync.swift index 6fac636fcfa7..5fa4ef7b7a93 100644 --- a/WordPress/Classes/Extensions/Media+Sync.swift +++ b/WordPress/Classes/Extensions/Media+Sync.swift @@ -48,7 +48,7 @@ extension Media { } // If they failed to upload themselfs because no local copy exists then we need to delete this media object // This scenario can happen when media objects were created based on an asset that failed to import to the WordPress App. - // For example a PHAsset that is stored on the iCloud storage and because of the network connection failed the import process. + // For example a that is stored on the iCloud storage and because of the network connection failed the import process. if media.remoteStatus == .failed, let error = media.error as NSError?, error.domain == MediaServiceErrorDomain && error.code == MediaServiceError.fileDoesNotExist.rawValue { context.delete(media) diff --git a/WordPress/Classes/Extensions/NSAttributedString+Helpers.swift b/WordPress/Classes/Extensions/NSAttributedString+Helpers.swift index 721518d8acaa..5bd00571e47e 100644 --- a/WordPress/Classes/Extensions/NSAttributedString+Helpers.swift +++ b/WordPress/Classes/Extensions/NSAttributedString+Helpers.swift @@ -31,7 +31,7 @@ public extension NSAttributedString { // When displaying an animated gif pass the gif data instead of the image if displayAnimatedGifs, - let animatedImage = image as? AnimatedImageWrapper, + let animatedImage = image as? AnimatedImage, animatedImage.gifData != nil { imageAttachment.contents = animatedImage.gifData diff --git a/WordPress/Classes/Extensions/PHAsset+Exporters.swift b/WordPress/Classes/Extensions/PHAsset+Exporters.swift deleted file mode 100644 index 359b0c3f8932..000000000000 --- a/WordPress/Classes/Extensions/PHAsset+Exporters.swift +++ /dev/null @@ -1,19 +0,0 @@ -import Foundation -import Photos -import MobileCoreServices -import AVFoundation - -extension PHAsset: ExportableAsset { - - public var assetMediaType: MediaType { - get { - if self.mediaType == .image { - return .image - } else if self.mediaType == .video { - return .video - } - return .document - } - } - -} diff --git a/WordPress/Classes/Extensions/PHAsset+Metadata.swift b/WordPress/Classes/Extensions/PHAsset+Metadata.swift deleted file mode 100644 index d914e80e11e9..000000000000 --- a/WordPress/Classes/Extensions/PHAsset+Metadata.swift +++ /dev/null @@ -1,40 +0,0 @@ -import Foundation -import Photos -import MobileCoreServices -import AVFoundation - -extension PHAsset { - - public var uniformTypeIdentifier: String? { - let resources = PHAssetResource.assetResources(for: self) - var types: [PHAssetResourceType.RawValue] = [] - if mediaType == PHAssetMediaType.image { - types = [PHAssetResourceType.photo.rawValue] - } else if mediaType == PHAssetMediaType.video { - types = [PHAssetResourceType.video.rawValue] - } - for resource in resources { - if types.contains(resource.type.rawValue) { - return resource.uniformTypeIdentifier - } - } - return nil - } - - public var originalFilename: String? { - let resources = PHAssetResource.assetResources(for: self) - var types: [PHAssetResourceType.RawValue] = [] - if mediaType == PHAssetMediaType.image { - types = [PHAssetResourceType.photo.rawValue] - } else if mediaType == PHAssetMediaType.video { - types = [PHAssetResourceType.video.rawValue] - } - for resource in resources { - if types.contains(resource.type.rawValue) { - return resource.originalFilename - } - } - return nil - } - -} diff --git a/WordPress/Classes/Extensions/Post+BloggingPrompts.swift b/WordPress/Classes/Extensions/Post+BloggingPrompts.swift index a0d40783249a..a2057afacde5 100644 --- a/WordPress/Classes/Extensions/Post+BloggingPrompts.swift +++ b/WordPress/Classes/Extensions/Post+BloggingPrompts.swift @@ -15,6 +15,13 @@ extension Post { } tags?.append(", \(Strings.promptTag)-\(prompt.promptID)") + + // add any additional tags for the prompt. + if let additionalPostTags = prompt.additionalPostTags, !additionalPostTags.isEmpty { + additionalPostTags + .map { ", \($0)" } + .forEach { self.tags?.append($0) } + } } private func promptContent(withPromptText promptText: String) -> String { diff --git a/WordPress/Classes/Extensions/UIBarButtonItem+Extensions.swift b/WordPress/Classes/Extensions/UIBarButtonItem+Extensions.swift new file mode 100644 index 000000000000..e3a9d26d01fa --- /dev/null +++ b/WordPress/Classes/Extensions/UIBarButtonItem+Extensions.swift @@ -0,0 +1,11 @@ +import UIKit + +extension UIBarButtonItem { + /// Returns a bar button item with a spinner activity indicator. + static var activityIndicator: UIBarButtonItem { + let activityIndicator = UIActivityIndicatorView(style: .medium) + activityIndicator.sizeToFit() + activityIndicator.startAnimating() + return UIBarButtonItem(customView: activityIndicator) + } +} diff --git a/WordPress/Classes/Extensions/UIButton+Extensions.swift b/WordPress/Classes/Extensions/UIButton+Extensions.swift new file mode 100644 index 000000000000..b1a281abfc60 --- /dev/null +++ b/WordPress/Classes/Extensions/UIButton+Extensions.swift @@ -0,0 +1,27 @@ +import UIKit + +extension UIButton { + /// Creates a bar button item that looks like the native title menu + /// (see `navigationItem.titleMenuProvider`, iOS 16+). + static func makeMenu(title: String, menu: UIMenu) -> UIButton { + let button = UIButton(configuration: { + var configuration = UIButton.Configuration.plain() + configuration.title = title + configuration.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { + var attributes = $0 + attributes.font = WPStyleGuide.fontForTextStyle(.headline) + return attributes + } + configuration.image = UIImage(systemName: "chevron.down.circle.fill")?.withBaselineOffset(fromBottom: 4) + configuration.preferredSymbolConfigurationForImage = UIImage.SymbolConfiguration(paletteColors: [.secondaryLabel, .secondarySystemFill]) + .applying(UIImage.SymbolConfiguration(font: WPStyleGuide.fontForTextStyle(.subheadline, fontWeight: .semibold))) + configuration.imagePlacement = .trailing + configuration.imagePadding = 4 + configuration.baseForegroundColor = .label + return configuration + }()) + button.menu = menu + button.showsMenuAsPrimaryAction = true + return button + } +} diff --git a/WordPress/Classes/Extensions/UINavigationBar+Appearance.swift b/WordPress/Classes/Extensions/UINavigationBar+Appearance.swift deleted file mode 100644 index 45cc4b0d84a9..000000000000 --- a/WordPress/Classes/Extensions/UINavigationBar+Appearance.swift +++ /dev/null @@ -1,7 +0,0 @@ -import UIKit - -extension UINavigationBar { - class func standardTitleTextAttributes() -> [NSAttributedString.Key: Any] { - return appearance().standardAppearance.titleTextAttributes - } -} diff --git a/WordPress/Classes/Extensions/UINavigationController+Helpers.swift b/WordPress/Classes/Extensions/UINavigationController+Helpers.swift index c7462b5c40e8..13a3e306f6e6 100644 --- a/WordPress/Classes/Extensions/UINavigationController+Helpers.swift +++ b/WordPress/Classes/Extensions/UINavigationController+Helpers.swift @@ -51,3 +51,18 @@ extension UINavigationController { self.pushViewController(viewController, animated: animated) } } + +extension UIViewController { + func configureDefaultNavigationBarAppearance() { + let standardAppearance = UINavigationBarAppearance() + standardAppearance.configureWithDefaultBackground() + + let scrollEdgeAppearance = UINavigationBarAppearance() + scrollEdgeAppearance.configureWithTransparentBackground() + + navigationItem.standardAppearance = standardAppearance + navigationItem.compactAppearance = standardAppearance + navigationItem.scrollEdgeAppearance = scrollEdgeAppearance + navigationItem.compactScrollEdgeAppearance = scrollEdgeAppearance + } +} diff --git a/WordPress/Classes/Extensions/URL+Helpers.swift b/WordPress/Classes/Extensions/URL+Helpers.swift index d06cc4a78ec2..28e4da54833c 100644 --- a/WordPress/Classes/Extensions/URL+Helpers.swift +++ b/WordPress/Classes/Extensions/URL+Helpers.swift @@ -108,6 +108,10 @@ extension URL { return components.count == 4 && isHostedAtWPCom } + var isWPComEmoji: Bool { + absoluteString.contains(".wp.com/i/emojis") + } + /// Handle the common link protocols. /// - tel: open a prompt to call the phone number diff --git a/WordPress/Classes/Models/AbstractPost+HashHelpers.m b/WordPress/Classes/Models/AbstractPost+HashHelpers.m index 4c573886cbdd..6c130bca4cdd 100644 --- a/WordPress/Classes/Models/AbstractPost+HashHelpers.m +++ b/WordPress/Classes/Models/AbstractPost+HashHelpers.m @@ -1,5 +1,5 @@ #import "AbstractPost+HashHelpers.h" -#import "Media+WPMediaAsset.h" +#import "Media+Extensions.h" #import "WordPress-Swift.h" @implementation AbstractPost (HashHelpers) @@ -21,7 +21,7 @@ - (NSString *)calculateConfirmedChangesContentHash { [self hashForString:self.password], [self hashForString:self.author], [self hashForNSInteger:self.authorID.integerValue], - [self hashForString:self.featuredImage.identifier], + [self hashForString:self.featuredImage.objectID.URIRepresentation.absoluteString], [self hashForString:self.wp_slug]]; diff --git a/WordPress/Classes/Models/AbstractPost.h b/WordPress/Classes/Models/AbstractPost.h index eb780151c39e..571a7c62181a 100644 --- a/WordPress/Classes/Models/AbstractPost.h +++ b/WordPress/Classes/Models/AbstractPost.h @@ -36,10 +36,6 @@ typedef NS_ENUM(NSUInteger, AbstractPostRemoteStatus) { // These are primarily used as helpers sorting fetchRequests. @property (nonatomic, assign) BOOL metaIsLocal; @property (nonatomic, assign) BOOL metaPublishImmediately; -/** - Used to store the post's status before its sent to the trash. - */ -@property (nonatomic, strong) NSString *restorableStatus; /** This array will contain a list of revision IDs. */ diff --git a/WordPress/Classes/Models/AbstractPost.m b/WordPress/Classes/Models/AbstractPost.m index 1e62605b84c2..a17a917c4ab2 100644 --- a/WordPress/Classes/Models/AbstractPost.m +++ b/WordPress/Classes/Models/AbstractPost.m @@ -39,8 +39,6 @@ @implementation AbstractPost @dynamic autosaveModifiedDate; @dynamic autosaveIdentifier; -@synthesize restorableStatus; - + (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key { NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key]; diff --git a/WordPress/Classes/Models/Blog+JetpackSocial.swift b/WordPress/Classes/Models/Blog+JetpackSocial.swift index 18cb9ffb0a20..5a5737e2c712 100644 --- a/WordPress/Classes/Models/Blog+JetpackSocial.swift +++ b/WordPress/Classes/Models/Blog+JetpackSocial.swift @@ -7,8 +7,8 @@ extension Blog { /// Whether the blog has Social auto-sharing limited. /// Note that sites hosted at WP.com has no Social sharing limitations. var isSocialSharingLimited: Bool { - let features = planActiveFeatures ?? [] - return !isHostedAtWPcom && !features.contains(Constants.socialSharingFeature) + let hasUnlimitedSharing = (planActiveFeatures ?? []).contains(Constants.unlimitedSharingFeatureKey) + return !(isHostedAtWPcom || isAtomic() || hasUnlimitedSharing) } /// The auto-sharing limit information for the blog. @@ -26,6 +26,6 @@ extension Blog { private enum Constants { /// The feature key listed in the blog's plan's features. At the moment, `social-shares-1000` means unlimited /// sharing, but in the future we might introduce a proper differentiation between 1000 and unlimited. - static let socialSharingFeature = "social-shares-1000" + static let unlimitedSharingFeatureKey = "social-shares-1000" } } diff --git a/WordPress/Classes/Models/Blog+Media.swift b/WordPress/Classes/Models/Blog+Media.swift deleted file mode 100644 index f0aae5188094..000000000000 --- a/WordPress/Classes/Models/Blog+Media.swift +++ /dev/null @@ -1,37 +0,0 @@ -import Foundation - -extension Blog { - - /// Get the number of items in a blog media library that are of a certain type. - /// - /// - Parameter mediaTypes: set of media type values to be considered in the counting. - /// - Returns: Number of media assets matching the criteria. - @objc(mediaLibraryCountForTypes:) - func mediaLibraryCount(types mediaTypes: NSSet) -> Int { - guard let context = managedObjectContext else { - return 0 - } - - var count = 0 - context.performAndWait { - var predicate = NSPredicate(format: "blog == %@", self) - - if mediaTypes.count > 0 { - let types = mediaTypes - .map { obj in - guard let rawValue = (obj as? NSNumber)?.uintValue, - let type = MediaType(rawValue: rawValue) else { - fatalError("Can't convert \(obj) to MediaType") - } - return Media.string(from: type) - } - let filterPredicate = NSPredicate(format: "mediaTypeString IN %@", types) - predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate, filterPredicate]) - } - - count = context.countObjects(ofType: Media.self, matching: predicate) - } - return count - } - -} diff --git a/WordPress/Classes/Models/Blog.h b/WordPress/Classes/Models/Blog.h index 3dd4386d7a9d..0efb24da843f 100644 --- a/WordPress/Classes/Models/Blog.h +++ b/WordPress/Classes/Models/Blog.h @@ -236,7 +236,7 @@ typedef NS_ENUM(NSInteger, SiteVisibility) { @property (readonly) BOOL hasIcon; /** Determine timezone for blog from blog options. If no timezone information is stored on the device, then assume GMT+0 is the default. */ -@property (readonly) NSTimeZone *timeZone; +@property (readonly, nullable) NSTimeZone *timeZone; #pragma mark - Blog information diff --git a/WordPress/Classes/Models/Blog.m b/WordPress/Classes/Models/Blog.m index c876f6dabcbb..12db38b9b6dd 100644 --- a/WordPress/Classes/Models/Blog.m +++ b/WordPress/Classes/Models/Blog.m @@ -326,7 +326,7 @@ - (BOOL)hasIcon return self.icon.length > 0 ? [NSURL URLWithString:self.icon].pathComponents.count > 1 : NO; } -- (NSTimeZone *)timeZone +- (nullable NSTimeZone *)timeZone { CGFloat const OneHourInSeconds = 60.0 * 60.0; diff --git a/WordPress/Classes/Models/BloggingPrompt+CoreDataClass.swift b/WordPress/Classes/Models/BloggingPrompt+CoreDataClass.swift index 7b9e0ccd615c..5ed06390c336 100644 --- a/WordPress/Classes/Models/BloggingPrompt+CoreDataClass.swift +++ b/WordPress/Classes/Models/BloggingPrompt+CoreDataClass.swift @@ -21,22 +21,25 @@ public class BloggingPrompt: NSManagedObject { BloggingPromptsAttribution(rawValue: attribution.lowercased()) } - /// Convenience method to map properties from `RemoteBloggingPrompt`. + /// Convenience method to map properties from `BloggingPromptRemoteObject`. /// /// - Parameters: /// - remotePrompt: The remote prompt model to convert /// - siteID: The ID of the site that the prompt is intended for - func configure(with remotePrompt: RemoteBloggingPrompt, for siteID: Int32) { + func configure(with remotePrompt: BloggingPromptRemoteObject, for siteID: Int32) { self.promptID = Int32(remotePrompt.promptID) self.siteID = siteID self.text = remotePrompt.text - self.title = remotePrompt.title - self.content = remotePrompt.content self.attribution = remotePrompt.attribution self.date = remotePrompt.date self.answered = remotePrompt.answered self.answerCount = Int32(remotePrompt.answeredUsersCount) self.displayAvatarURLs = remotePrompt.answeredUserAvatarURLs + self.additionalPostTags = [String]() // reset previously additional tags. + + if let brandContext = BrandContext(with: remotePrompt) { + brandContext.configure(self) + } } func textForDisplay() -> String { @@ -71,6 +74,36 @@ extension BloggingPrompt { private extension BloggingPrompt { + enum Constants { + static let bloganuaryTag = "bloganuary" + } + + enum BrandContext { + case bloganuary(String) + + init?(with remotePrompt: BloggingPromptRemoteObject) { + // Bloganuary context + if let bloganuaryId = remotePrompt.bloganuaryId, + bloganuaryId.contains(Constants.bloganuaryTag) { + self = .bloganuary(bloganuaryId) + return + } + + return nil + } + + /// Configures the given prompt with additional data based on the brand context. + /// + /// - Parameter prompt: The `BloggingPrompt` instance to configure. + func configure(_ prompt: BloggingPrompt) { + switch self { + case .bloganuary(let id): + prompt.additionalPostTags = [Constants.bloganuaryTag, id] + prompt.attribution = BloggingPromptsAttribution.bloganuary.rawValue + } + } + } + struct DateFormatters { static let local: DateFormatter = { let formatter = DateFormatter() diff --git a/WordPress/Classes/Models/BloggingPrompt+CoreDataProperties.swift b/WordPress/Classes/Models/BloggingPrompt+CoreDataProperties.swift index a40e60d39968..47fac71d4113 100644 --- a/WordPress/Classes/Models/BloggingPrompt+CoreDataProperties.swift +++ b/WordPress/Classes/Models/BloggingPrompt+CoreDataProperties.swift @@ -11,12 +11,6 @@ extension BloggingPrompt { /// The prompt content to be displayed at entry points. @NSManaged public var text: String - /// Template title for the draft post. - @NSManaged public var title: String - - /// Template content for the draft post. - @NSManaged public var content: String - /// The attribution source for the prompt. @NSManaged public var attribution: String @@ -31,4 +25,7 @@ extension BloggingPrompt { /// Contains avatar URLs of some users that have answered the prompt. @NSManaged public var displayAvatarURLs: [URL] + + /// Contains additional tags that should be appended to the post for this prompt's answer. + @NSManaged public var additionalPostTags: [String]? } diff --git a/WordPress/Classes/Models/BloggingPromptSettings+CoreDataClass.swift b/WordPress/Classes/Models/BloggingPromptSettings+CoreDataClass.swift index 14694a2917f3..9cccb837b720 100644 --- a/WordPress/Classes/Models/BloggingPromptSettings+CoreDataClass.swift +++ b/WordPress/Classes/Models/BloggingPromptSettings+CoreDataClass.swift @@ -48,12 +48,13 @@ public class BloggingPromptSettings: NSManagedObject { } private func updatePromptSettingsIfNecessary(siteID: Int, enabled: Bool) { - let service = BlogDashboardPersonalizationService(siteID: siteID) - if !service.hasPreference(for: .prompts) { - service.setEnabled(enabled, for: .prompts) + DispatchQueue.main.async { + let service = BlogDashboardPersonalizationService(siteID: siteID) + if !service.hasPreference(for: .prompts) { + service.setEnabled(enabled, for: .prompts) + } } } - } extension RemoteBloggingPromptsSettings { diff --git a/WordPress/Classes/Models/Comment+CoreDataClass.swift b/WordPress/Classes/Models/Comment+CoreDataClass.swift index 39ab676683a3..8bc04ae781fc 100644 --- a/WordPress/Classes/Models/Comment+CoreDataClass.swift +++ b/WordPress/Classes/Models/Comment+CoreDataClass.swift @@ -58,10 +58,6 @@ public class Comment: NSManagedObject { return Int(likeCount) } - func hasAuthorUrl() -> Bool { - return !author_url.isEmpty - } - func canEditAuthorData() -> Bool { // If the authorID is zero, the user is unregistered. Therefore, the data can be edited. return authorID == 0 diff --git a/WordPress/Classes/Models/Media.h b/WordPress/Classes/Models/Media.h index 0f7703fea6c1..061d5b0142f0 100644 --- a/WordPress/Classes/Models/Media.h +++ b/WordPress/Classes/Models/Media.h @@ -105,4 +105,13 @@ typedef NS_ENUM(NSUInteger, MediaType) { @end +// TODO: Remove it; it was added for compatibility during the WPMediaPicker removal +typedef NS_OPTIONS(NSInteger, WPMediaType){ + WPMediaTypeImage = 1, + WPMediaTypeVideo = 1 << 1, + WPMediaTypeAudio = 1 << 2, + WPMediaTypeOther = 1 << 3, + WPMediaTypeAll= 0XFF +}; + NS_ASSUME_NONNULL_END diff --git a/WordPress/Classes/Models/Media.swift b/WordPress/Classes/Models/Media.swift index 9323a0653e5f..2ea86c81278f 100644 --- a/WordPress/Classes/Models/Media.swift +++ b/WordPress/Classes/Models/Media.swift @@ -2,7 +2,6 @@ import Foundation import UniformTypeIdentifiers extension Media { - // MARK: - AutoUpload Failure Count static let maxAutoUploadFailureCount = 3 diff --git a/WordPress/Classes/Models/Notifications/NotificationSettings.swift b/WordPress/Classes/Models/Notifications/NotificationSettings.swift index d03d8caaf613..b3c4c6832490 100644 --- a/WordPress/Classes/Models/Notifications/NotificationSettings.swift +++ b/WordPress/Classes/Models/Notifications/NotificationSettings.swift @@ -144,8 +144,6 @@ open class NotificationSettings { return NSLocalizedString("Push Notifications", comment: "Mobile Push Notifications") } } - - static let allValues = [ Timeline, Email, Device ] } } diff --git a/WordPress/Classes/Models/Post.swift b/WordPress/Classes/Models/Post.swift index fbd15cd797fa..50c8bd28acd8 100644 --- a/WordPress/Classes/Models/Post.swift +++ b/WordPress/Classes/Models/Post.swift @@ -364,7 +364,9 @@ class Post: AbstractPost { override func titleForDisplay() -> String { var title = postTitle?.trimmingCharacters(in: CharacterSet.whitespaces) ?? "" - title = title.stringByDecodingXMLCharacters() + title = title + .stringByDecodingXMLCharacters() + .strippingHTML() if title.count == 0 && contentPreviewForDisplay().count == 0 && !hasRemote() { title = NSLocalizedString("(no title)", comment: "Lets a user know that a local draft does not have a title.") diff --git a/WordPress/Classes/Models/ReaderCard+CoreDataClass.swift b/WordPress/Classes/Models/ReaderCard+CoreDataClass.swift index 2787ce87cb49..8542d85561f8 100644 --- a/WordPress/Classes/Models/ReaderCard+CoreDataClass.swift +++ b/WordPress/Classes/Models/ReaderCard+CoreDataClass.swift @@ -25,6 +25,15 @@ public class ReaderCard: NSManagedObject { return .unknown } + var isRecommendationCard: Bool { + switch type { + case .topics, .sites: + return true + default: + return false + } + } + var topicsArray: [ReaderTagTopic] { topics?.array as? [ReaderTagTopic] ?? [] } diff --git a/WordPress/Classes/Models/Revisions/Revision.swift b/WordPress/Classes/Models/Revisions/Revision.swift index ae6636c9bd08..e4e31a44f98d 100644 --- a/WordPress/Classes/Models/Revisions/Revision.swift +++ b/WordPress/Classes/Models/Revisions/Revision.swift @@ -31,10 +31,6 @@ class Revision: NSManagedObject { return revisionFormatter.date(from: postDateGmt ?? "") ?? Date() } - var revisionModifiedDate: Date { - return revisionFormatter.date(from: postModifiedGmt ?? "") ?? Date() - } - @objc var revisionDateForSection: String { return revisionDate.longUTCStringWithoutTime() } diff --git a/WordPress/Classes/Networking/MediaHost+ReaderPostContentProvider.swift b/WordPress/Classes/Networking/MediaHost+ReaderPostContentProvider.swift index 7af84a15507b..f1794ca90b5d 100644 --- a/WordPress/Classes/Networking/MediaHost+ReaderPostContentProvider.swift +++ b/WordPress/Classes/Networking/MediaHost+ReaderPostContentProvider.swift @@ -5,7 +5,6 @@ import Foundation /// extension MediaHost { enum ReaderPostContentProviderError: Swift.Error { - case noDefaultWordPressComAccount case baseInitializerError(error: Error, readerPostContentProvider: ReaderPostContentProvider) } diff --git a/WordPress/Classes/Networking/MediaRequestAuthenticator.swift b/WordPress/Classes/Networking/MediaRequestAuthenticator.swift index 68c74ad84a6b..5132eab8fd04 100644 --- a/WordPress/Classes/Networking/MediaRequestAuthenticator.swift +++ b/WordPress/Classes/Networking/MediaRequestAuthenticator.swift @@ -22,7 +22,6 @@ class MediaRequestAuthenticator { /// Errors conditions that this class can find. /// enum Error: Swift.Error { - case cannotFindSiteIDForSiteAvailableThroughWPCom(blog: Blog) case cannotBreakDownURLIntoComponents(url: URL) case cannotCreateAtomicURL(components: URLComponents) case cannotCreateAtomicProxyURL(components: URLComponents) diff --git a/WordPress/Classes/Services/AccountService.m b/WordPress/Classes/Services/AccountService.m index f21c638a970a..235eafd50ee5 100644 --- a/WordPress/Classes/Services/AccountService.m +++ b/WordPress/Classes/Services/AccountService.m @@ -353,6 +353,7 @@ - (void)updateAccountWithID:(NSManagedObjectID *)objectID withUserDetails:(Remot [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { WPAccount *account = [context existingObjectWithID:objectID error:nil]; [self updateDefaultBlogIfNeeded:account inContext:context]; + [[NSNotificationCenter defaultCenter] postNotificationName:WPAccountEmailAndDefaultBlogUpdatedNotification object:nil]; }]; } diff --git a/WordPress/Classes/Services/AccountSettingsService.swift b/WordPress/Classes/Services/AccountSettingsService.swift index 927854ca06cb..bf6184a1e617 100644 --- a/WordPress/Classes/Services/AccountSettingsService.swift +++ b/WordPress/Classes/Services/AccountSettingsService.swift @@ -38,7 +38,6 @@ class AccountSettingsService { struct Defaults { static let stallTimeout = 4.0 static let maxRetries = 3 - static let pollingInterval = 60.0 } let remote: AccountSettingsRemoteInterface diff --git a/WordPress/Classes/Services/BlogService+Domains.swift b/WordPress/Classes/Services/BlogService+Domains.swift index cc1039a90799..06326fc3fe4c 100644 --- a/WordPress/Classes/Services/BlogService+Domains.swift +++ b/WordPress/Classes/Services/BlogService+Domains.swift @@ -18,17 +18,15 @@ extension BlogService { return } - guard account.wordPressComRestApi != nil else { - failure?(BlogServiceDomainError.noWordPressComRestApi(blog: blog)) - return - } - guard let siteID = blog.dotComID?.intValue else { failure?(BlogServiceDomainError.noSiteIDForSpecifiedBlog(blog: blog)) return } - let service = DomainsService(coreDataStack: coreDataStack, account: account) + guard let service = DomainsService(coreDataStack: coreDataStack, wordPressComRestApi: account.wordPressComRestApi) else { + failure?(BlogServiceDomainError.noWordPressComRestApi(blog: blog)) + return + } service.refreshDomains(siteID: siteID) { result in switch result { diff --git a/WordPress/Classes/Services/BloggingPrompts/BloggingPromptRemoteObject.swift b/WordPress/Classes/Services/BloggingPrompts/BloggingPromptRemoteObject.swift new file mode 100644 index 000000000000..200536f59347 --- /dev/null +++ b/WordPress/Classes/Services/BloggingPrompts/BloggingPromptRemoteObject.swift @@ -0,0 +1,77 @@ +/// Encapsulates a single blogging prompt object from the v3 API. +struct BloggingPromptRemoteObject { + let promptID: Int + let text: String + let attribution: String + let date: Date + let answered: Bool + let answeredUsersCount: Int + let answeredUserAvatarURLs: [URL] + let answeredLink: URL? + let answeredLinkText: String + let bloganuaryId: String? +} + +// MARK: - Decodable + +extension BloggingPromptRemoteObject: Decodable { + enum CodingKeys: String, CodingKey { + case id + case text + case attribution + case date + case answered + case answeredUsersCount = "answered_users_count" + case answeredUserAvatarURLs = "answered_users_sample" + case answeredLink = "answered_link" + case answeredLinkText = "answered_link_text" + case bloganuaryId = "bloganuary_id" + } + + /// meta structure to simplify decoding logic for user avatar objects. + /// this is intended to be private. + private struct UserAvatar: Codable { + var avatar: String + } + + /// Used to format the fetched object's date string to a date. + private static var dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.locale = .init(identifier: "en_US_POSIX") + formatter.timeZone = .init(secondsFromGMT: 0) + formatter.dateFormat = "yyyy-MM-dd" + return formatter + }() + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.promptID = try container.decode(Int.self, forKey: .id) + self.text = try container.decode(String.self, forKey: .text) + self.attribution = try container.decode(String.self, forKey: .attribution) + self.answered = try container.decode(Bool.self, forKey: .answered) + self.date = Self.dateFormatter.date(from: try container.decode(String.self, forKey: .date)) ?? Date() + self.answeredUsersCount = try container.decode(Int.self, forKey: .answeredUsersCount) + + let userAvatars = try container.decode([UserAvatar].self, forKey: .answeredUserAvatarURLs) + self.answeredUserAvatarURLs = userAvatars.compactMap { URL(string: $0.avatar) } + + self.answeredLink = { + guard let linkURLString = try? container.decode(String.self, forKey: .answeredLink), + let answeredLinkURL = URL(string: linkURLString) else { + return nil + } + return answeredLinkURL + }() + + self.answeredLinkText = try container.decode(String.self, forKey: .answeredLinkText) + + self.bloganuaryId = { + guard let remoteBloganuaryId = try? container.decode(String.self, forKey: .bloganuaryId), + !remoteBloganuaryId.isEmpty else { + return nil + } + return remoteBloganuaryId + }() + } +} diff --git a/WordPress/Classes/Services/BloggingPromptsService.swift b/WordPress/Classes/Services/BloggingPrompts/BloggingPromptsService.swift similarity index 69% rename from WordPress/Classes/Services/BloggingPromptsService.swift rename to WordPress/Classes/Services/BloggingPrompts/BloggingPromptsService.swift index 6fc0cf603d5d..09f626e14a3b 100644 --- a/WordPress/Classes/Services/BloggingPromptsService.swift +++ b/WordPress/Classes/Services/BloggingPrompts/BloggingPromptsService.swift @@ -2,9 +2,11 @@ import CoreData import WordPressKit class BloggingPromptsService { - private let contextManager: CoreDataStackSwift let siteID: NSNumber - private let remote: BloggingPromptsServiceRemote + + private let contextManager: CoreDataStackSwift + private let remote: BloggingPromptsServiceRemote // TODO: Remove once the settings logic is ported. + private let api: WordPressComRestApi private let calendar: Calendar = .autoupdatingCurrent private let maxListPrompts = 11 @@ -27,6 +29,14 @@ class BloggingPromptsService { return formatter }() + /// A JSON decoder that can parse date strings that matches `JSONDecoder.DateDecodingStrategy.DateFormat` into `Date`. + private static var jsonDecoder: JSONDecoder = { + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = JSONDecoder.DateDecodingStrategy.supportMultipleDateFormats + + return decoder + }() + /// Convenience computed variable that returns today's prompt from local store. /// var localTodaysPrompt: BloggingPrompt? { @@ -48,7 +58,8 @@ class BloggingPromptsService { success: (([BloggingPrompt]) -> Void)? = nil, failure: ((Error?) -> Void)? = nil) { let fromDate = startDate ?? defaultStartDate - remote.fetchPrompts(for: siteID, number: number, fromDate: fromDate) { result in + + fetchRemotePrompts(number: number, fromDate: fromDate, ignoresYear: true) { result in switch result { case .success(let remotePrompts): self.upsert(with: remotePrompts) { innerResult in @@ -181,11 +192,14 @@ class BloggingPromptsService { /// Otherwise, a remote service with the default account's credentials will be used. /// - blog: When supplied, the service will perform blogging prompts requests for this specified blog. /// Otherwise, this falls back to the default account's primary blog. + /// - api: When supplied, the WordPressComRestApi instance to use to fetch the prompts. + /// Otherwise, an default or anonymous instance will be computed based on whether there is an account available. required init?(contextManager: CoreDataStackSwift = ContextManager.shared, + api: WordPressComRestApi? = nil, remote: BloggingPromptsServiceRemote? = nil, blog: Blog? = nil) { let blogObjectID = blog?.objectID - let (siteID, remoteInstance) = contextManager.performQuery { mainContext in + let (siteID, remoteInstance, api) = contextManager.performQuery { mainContext in // if a blog exists, then try to use the blog's ID. var blogInContext: Blog? = nil if let blogObjectID { @@ -194,12 +208,18 @@ class BloggingPromptsService { // fetch the default account and fall back to default values as needed. guard let account = try? WPAccount.lookupDefaultWordPressComAccount(in: mainContext) else { - return (blogInContext?.dotComID, remote) + return ( + blogInContext?.dotComID, + remote, + api ?? WordPressComRestApi.anonymousApi(userAgent: WPUserAgent.wordPress(), + localeKey: WordPressComRestApi.LocaleKeyV2) + ) } return ( blogInContext?.dotComID ?? account.primaryBlogID, - remote ?? .init(wordPressComRestApi: account.wordPressComRestV2Api) + remote ?? .init(wordPressComRestApi: api ?? account.wordPressComRestV2Api), + api ?? account.wordPressComRestV2Api ) } @@ -211,6 +231,7 @@ class BloggingPromptsService { self.contextManager = contextManager self.siteID = siteID self.remote = remoteInstance + self.api = api } } @@ -260,6 +281,64 @@ private extension BloggingPromptsService { return Self.utcDateFormatter.date(from: dateString) } + // MARK: Prompts + + /// Fetches a number of blogging prompts for the specified site from the v3 endpoint. + /// + /// - Parameters: + /// - number: The number of prompts to query. When not specified, this will default to remote implementation. + /// - fromDate: When specified, this will fetch prompts from the given date. When not specified, this will default to remote implementation. + /// - ignoresYear: When set to true, this will convert the date to a custom format that ignores the year part. Defaults to true. + /// - forceYear: Forces the year value on the prompt's date to the specified value. Defaults to the current year. + /// - completion: A closure that will be called when the fetch request completes. + func fetchRemotePrompts(number: Int? = nil, + fromDate: Date? = nil, + ignoresYear: Bool = true, + forceYear: Int? = nil, + completion: @escaping (Result<[BloggingPromptRemoteObject], Error>) -> Void) { + let path = "wpcom/v3/sites/\(siteID)/blogging-prompts" + let requestParameter: [String: AnyHashable] = { + var params = [String: AnyHashable]() + + if let number, number > 0 { + params["per_page"] = number + } + + if let fromDate { + // convert to yyyy-MM-dd format in local timezone so users would see the same prompt throughout their day. + var dateString = Self.localDateFormatter.string(from: fromDate) + + // when the year needs to be ignored, we'll transform the dateString to match the "--mm-dd" format. + if ignoresYear, !dateString.isEmpty { + dateString = "-" + dateString.dropFirst(4) + } + + params["after"] = dateString + } + + if let forceYear = forceYear ?? fromDate?.dateAndTimeComponents().year { + params["force_year"] = forceYear + } + + return params + }() + + api.GET(path, parameters: requestParameter as [String: AnyObject]) { result, _ in + switch result { + case .success(let responseObject): + do { + let data = try JSONSerialization.data(withJSONObject: responseObject, options: []) + let remotePrompts = try Self.jsonDecoder.decode([BloggingPromptRemoteObject].self, from: data) + completion(.success(remotePrompts)) + } catch { + completion(.failure(error)) + } + case .failure(let error): + completion(.failure(error)) + } + } + } + /// Loads local prompts based on the given parameters. /// /// - Parameters: @@ -292,45 +371,64 @@ private extension BloggingPromptsService { /// - Parameters: /// - remotePrompts: An array containing prompts obtained from remote. /// - completion: Closure to be called after the process completes. Returns an array of prompts when successful. - func upsert(with remotePrompts: [RemoteBloggingPrompt], completion: @escaping (Result) -> Void) { + func upsert(with remotePrompts: [BloggingPromptRemoteObject], completion: @escaping (Result) -> Void) { if remotePrompts.isEmpty { completion(.success(())) return } - let remoteIDs = Set(remotePrompts.map { Int32($0.promptID) }) - let remotePromptsDictionary = remotePrompts.reduce(into: [Int32: RemoteBloggingPrompt]()) { partialResult, remotePrompt in - partialResult[Int32(remotePrompt.promptID)] = remotePrompt + // incoming remote prompts should have unique dates. + // fetch requests require the date to be `NSDate` specifically, hence the cast. + let incomingDates = Set(remotePrompts.map(\.date)) + let promptsByDate = remotePrompts.reduce(into: [Date: BloggingPromptRemoteObject]()) { partialResult, remotePrompt in + partialResult[remotePrompt.date] = remotePrompt } - let predicate = NSPredicate(format: "\(#keyPath(BloggingPrompt.siteID)) = %@ AND \(#keyPath(BloggingPrompt.promptID)) IN %@", siteID, remoteIDs) + let predicate = NSPredicate(format: "\(#keyPath(BloggingPrompt.siteID)) = %@ AND \(#keyPath(BloggingPrompt.date)) IN %@", + siteID, + incomingDates.map { $0 as NSDate }) let fetchRequest = BloggingPrompt.fetchRequest() fetchRequest.predicate = predicate contextManager.performAndSave({ derivedContext in - var foundExistingIDs = [Int32]() + /// Try to overwrite prompts that have the same dates. + /// + /// Perf. notes: since we're at most updating 25 entries, it should be acceptable to update them one by one. + /// However, if requirements change and we need to work through a larger data set, consider switching to + /// a drop-and-replace strategy with `NSBatchDeleteRequest` as it's more performant. + var updatedExistingDates = Set() let results = try derivedContext.fetch(fetchRequest) results.forEach { prompt in - guard let remotePrompt = remotePromptsDictionary[prompt.promptID] else { + guard let incoming = promptsByDate[prompt.date] else { return } - foundExistingIDs.append(prompt.promptID) - prompt.configure(with: remotePrompt, for: self.siteID.int32Value) + // ensure that there's only one prompt for each date. + // if the prompt with this date has been updated before, then it's a duplicate. Let's delete it. + if updatedExistingDates.contains(prompt.date) { + derivedContext.deleteObject(prompt) + return + } + + // otherwise, we can update the prompt matching the date with the incoming prompt. + prompt.configure(with: incoming, for: self.siteID.int32Value) + updatedExistingDates.insert(incoming.date) } - // Insert new prompts - let newPromptIDs = remoteIDs.subtracting(foundExistingIDs) - newPromptIDs.forEach { newPromptID in - guard let remotePrompt = remotePromptsDictionary[newPromptID], + // process the remaining new prompts. + let datesToInsert = incomingDates.subtracting(updatedExistingDates) + datesToInsert.forEach { date in + guard let incoming = promptsByDate[date], let newPrompt = BloggingPrompt.newObject(in: derivedContext) else { return } - newPrompt.configure(with: remotePrompt, for: self.siteID.int32Value) + newPrompt.configure(with: incoming, for: self.siteID.int32Value) } }, completion: completion, on: .main) } + // MARK: Prompt Settings + /// Updates existing settings or creates new settings from the remote prompt settings. /// /// - Parameters: diff --git a/WordPress/Classes/Services/Domains/DomainsService+AllDomains.swift b/WordPress/Classes/Services/Domains/DomainsService+AllDomains.swift new file mode 100644 index 000000000000..8b092afdd25a --- /dev/null +++ b/WordPress/Classes/Services/Domains/DomainsService+AllDomains.swift @@ -0,0 +1,37 @@ +import Foundation +import WordPressKit + +protocol DomainsServiceAllDomainsFetching { + func fetchAllDomains(resolveStatus: Bool, noWPCOM: Bool, completion: @escaping (DomainsServiceRemote.AllDomainsEndpointResult) -> Void) +} + +extension DomainsService: DomainsServiceAllDomainsFetching { + + /// Makes a GET request to `/v1.1/all-domains` endpoint and returns a list of domain objects. + /// + /// - Parameters: + /// - resolveStatus: Boolean indicating whether the backend should resolve domain status. + /// - noWPCOM: Boolean indicating whether the backend should include `wpcom` domains. + /// - completion: The closure to be executed when the API request is complete. + func fetchAllDomains(resolveStatus: Bool, noWPCOM: Bool, completion: @escaping (AllDomainsEndpointResult) -> Void) { + var params = AllDomainsEndpointParams() + params.resolveStatus = resolveStatus + params.noWPCOM = noWPCOM + params.locale = Locale.current.identifier + remote.fetchAllDomains(params: params, completion: completion) + } + + typealias AllDomainsEndpointResult = DomainsServiceRemote.AllDomainsEndpointResult + typealias AllDomainsEndpointParams = DomainsServiceRemote.AllDomainsEndpointParams + typealias AllDomainsListItem = DomainsServiceRemote.AllDomainsListItem +} + +extension DomainsService.AllDomainsListItem { + + func matches(searchQuery: String) -> Bool { + return domain.localizedStandardContains(searchQuery) + || siteSlug.localizedStandardContains(searchQuery) + || blogName.localizedStandardContains(searchQuery) + || (status?.value.localizedStandardContains(searchQuery) ?? false) + } +} diff --git a/WordPress/Classes/Services/DomainsService.swift b/WordPress/Classes/Services/Domains/DomainsService.swift similarity index 97% rename from WordPress/Classes/Services/DomainsService.swift rename to WordPress/Classes/Services/Domains/DomainsService.swift index 0c0f0a1128a2..c7f84d29c61d 100644 --- a/WordPress/Classes/Services/DomainsService.swift +++ b/WordPress/Classes/Services/Domains/DomainsService.swift @@ -225,8 +225,9 @@ struct DomainsService { } extension DomainsService { - init(coreDataStack: CoreDataStack, account: WPAccount) { - self.init(coreDataStack: coreDataStack, remote: DomainsServiceRemote(wordPressComRestApi: account.wordPressComRestApi)) + init?(coreDataStack: CoreDataStack, wordPressComRestApi: WordPressComRestApi?) { + guard let wordPressComRestApi = wordPressComRestApi else { return nil } + self.init(coreDataStack: coreDataStack, remote: DomainsServiceRemote(wordPressComRestApi: wordPressComRestApi)) } } diff --git a/WordPress/Classes/Services/MediaCoordinator.swift b/WordPress/Classes/Services/MediaCoordinator.swift index 4916bfee0308..4bffc4622698 100644 --- a/WordPress/Classes/Services/MediaCoordinator.swift +++ b/WordPress/Classes/Services/MediaCoordinator.swift @@ -154,7 +154,7 @@ class MediaCoordinator: NSObject { addMedia(from: asset, post: post, coordinator: coordinator(for: post), analyticsInfo: analyticsInfo) } - /// Create a `Media` instance from the main context and upload the asset to the Meida Library. + /// Create a `Media` instance from the main context and upload the asset to the Media Library. /// /// - Warning: This function must be called from the main thread. /// @@ -186,7 +186,7 @@ class MediaCoordinator: NSObject { return media } - /// Create a `Media` instance and upload the asset to the Meida Library. + /// Create a `Media` instance and upload the asset to the Media Library. /// /// - SeeAlso: `MediaImportService.createMedia(with:blog:post:receiveUpdate:thumbnailCallback:completion:)` private func addMedia(from asset: ExportableAsset, blog: Blog, post: AbstractPost?, coordinator: MediaProgressCoordinator, analyticsInfo: MediaAnalyticsInfo? = nil) { @@ -300,13 +300,12 @@ class MediaCoordinator: NSObject { /// the upload will be canceled. /// /// - Parameter media: The media object to delete - /// - Parameter onProgress: Optional progress block, called after each media item is deleted /// - Parameter success: Optional block called after all media items are deleted successfully /// - Parameter failure: Optional block called if deletion failed for any media items, /// after attempted deletion of all media items /// - func delete(_ media: Media, onProgress: ((Progress?) -> Void)? = nil, success: (() -> Void)? = nil, failure: (() -> Void)? = nil) { - delete(media: [media], onProgress: onProgress, success: success, failure: failure) + func delete(_ media: Media, success: (() -> Void)? = nil, failure: (() -> Void)? = nil) { + delete(media: [media], success: success, failure: failure) } /// Deletes media objects. If the objects are currently being uploaded, @@ -321,12 +320,42 @@ class MediaCoordinator: NSObject { func delete(media: [Media], onProgress: ((Progress?) -> Void)? = nil, success: (() -> Void)? = nil, failure: (() -> Void)? = nil) { media.forEach({ self.cancelUpload(of: $0) }) - coreDataStack.performAndSave { context in - let service = self.mediaServiceFactory.create(context) - service.deleteMedia(media, - progress: { onProgress?($0) }, - success: success, - failure: failure) + let mediaIDs = media.map { TaggedManagedObjectID($0) } + Task { @MainActor in + let failures = await self.delete(mediaIDs, onProgress: onProgress) + if failures.isEmpty { + success?() + } else { + failure?() + } + } + } + + /// Delete media from WordPress Media Library concurrently. + /// + /// - Returns: Media objects that are failed to be deleted. + private func delete(_ mediaIDs: [TaggedManagedObjectID], onProgress: ((Progress?) -> Void)?) async -> [TaggedManagedObjectID] { + let progress = Progress.discreteProgress(totalUnitCount: Int64(mediaIDs.count)) + onProgress?(progress) + let mediaRepository = MediaRepository(coreDataStack: coreDataStack) + return await withTaskGroup(of: (TaggedManagedObjectID, Bool).self) { group in + for id in mediaIDs { + group.addTask { + do { + try await mediaRepository.delete(id) + progress.completedUnitCount += 1 + return (id, true) + } catch { + return (id, false) + } + } + } + return await group.reduce(into: [TaggedManagedObjectID]()) { partialResult, deletion in + let (id, success) = deletion + if !success { + partialResult.append(id) + } + } } } @@ -427,15 +456,6 @@ class MediaCoordinator: NSObject { return cachedCoordinator(for: post)?.totalProgress ?? 0 } - /// Returns the error associated to media if any - /// - /// - Parameter media: the media object from where to fetch the associated error. - /// - Returns: the error associated to media if any - /// - func error(for media: Media) -> NSError? { - return coordinator(for: media).error(forMediaID: media.uploadID) - } - /// Returns the media object for the specified uploadID. /// /// - Parameter uploadID: the identifier for an ongoing upload diff --git a/WordPress/Classes/Services/MediaHelper.swift b/WordPress/Classes/Services/MediaHelper.swift index 4beb471de0c3..7c582c6005ba 100644 --- a/WordPress/Classes/Services/MediaHelper.swift +++ b/WordPress/Classes/Services/MediaHelper.swift @@ -67,6 +67,38 @@ class MediaHelper: NSObject { } } + + static func advertiseImageOptimization(completion: @escaping (() -> Void)) { + guard MediaSettings().advertiseImageOptimization else { + completion() + return + } + + let title = NSLocalizedString("appSettings.optimizeImagesPopup.title", value: "Keep optimizing images?", + comment: "Title of an alert informing users to enable image optimization in uploads.") + let message = NSLocalizedString("appSettings.optimizeImagesPopup.message", value: "Image optimization shrinks images for faster uploading.\n\nThis option is enabled by default, but you can change it in the app settings at any time.", + comment: "Message of an alert informing users to enable image optimization in uploads.") + let turnOffTitle = NSLocalizedString("appSettings.optimizeImagesPopup.turnOff", value: "No, turn off", comment: "Title of button for turning off image optimization, displayed in the alert informing users to enable image optimization in uploads.") + let leaveOnTitle = NSLocalizedString("appSettings.optimizeImagesPopup.turnOn", value: "Yes, leave on", comment: "Title of button for leaving on image optimization, displayed in the alert informing users to enable image optimization in uploads.") + + let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) + let turnOffAction = UIAlertAction(title: turnOffTitle, style: .default) { _ in + MediaSettings().imageOptimizationEnabled = false + WPAnalytics.track(.appSettingsOptimizeImagesPopupTapped, properties: ["option": "off"]) + completion() + } + let leaveOnAction = UIAlertAction(title: leaveOnTitle, style: .default) { _ in + MediaSettings().imageOptimizationEnabled = true + WPAnalytics.track(.appSettingsOptimizeImagesPopupTapped, properties: ["option": "on"]) + completion() + } + alert.addAction(turnOffAction) + alert.addAction(leaveOnAction) + alert.preferredAction = leaveOnAction + alert.presentFromRootViewController() + + MediaSettings().advertiseImageOptimization = false + } } extension Media { diff --git a/WordPress/Classes/Services/MediaImageService.swift b/WordPress/Classes/Services/MediaImageService.swift index cf13aa7bd3ad..ff9d9ce3411b 100644 --- a/WordPress/Classes/Services/MediaImageService.swift +++ b/WordPress/Classes/Services/MediaImageService.swift @@ -1,30 +1,28 @@ import UIKit import CoreData -/// A service for retrieval and caching of thumbnails for Media objects. -final class MediaImageService: NSObject { +/// A service for retrieval and caching of thumbnails for ``Media`` objects. +final class MediaImageService { static let shared = MediaImageService() - private let session: URLSession + private let cache: MemoryCache private let coreDataStack: CoreDataStackSwift private let mediaFileManager: MediaFileManager - private let ioQueue = DispatchQueue(label: "org.automattic.MediaImageService") + private let downloader: ImageDownloader - init(coreDataStack: CoreDataStackSwift = ContextManager.shared, - mediaFileManager: MediaFileManager = MediaFileManager(directory: .cache)) { + init(cache: MemoryCache = .shared, + coreDataStack: CoreDataStackSwift = ContextManager.shared, + mediaFileManager: MediaFileManager = MediaFileManager(directory: .cache), + downloader: ImageDownloader = .shared) { + self.cache = cache self.coreDataStack = coreDataStack self.mediaFileManager = mediaFileManager - - let configuration = URLSessionConfiguration.default - // `MediaImageService` has its own disk cache, so it's important to - // disable the native url cache which is by default set to `URLCache.shared` - configuration.urlCache = nil - self.session = URLSession(configuration: configuration) + self.downloader = downloader } static func migrateCacheIfNeeded() { let didMigrateKey = "MediaImageService-didMigrateCacheKey" - guard Feature.enabled(.mediaModernization) && !UserDefaults.standard.bool(forKey: didMigrateKey) else { + guard !UserDefaults.standard.bool(forKey: didMigrateKey) else { return } UserDefaults.standard.set(true, forKey: didMigrateKey) @@ -33,22 +31,109 @@ final class MediaImageService: NSObject { } } - // MARK: - Thumbnails + enum Error: Swift.Error { + case unsupportedMediaType(_ type: MediaType) + case unsupportedThumbnailSize(_ size: ImageSize) + case missingImageURL + } - /// Returns a thumbnail for the given media asset. The images are decompressed - /// (or bitmapped) and are ready to be displayed. + /// Returns an image for the given media asset. + /// + /// **Performance Characteristics** + /// + /// The returned images are decompressed (or bitmapped) and are ready to be + /// displayed even during scrolling. + /// + /// The thumbnails (``ImageSize/small`` or ``ImageSize/medium``) don't take + /// a lot of space or memory and are used often. The app often displays + /// multiple thumbnails on the screen at the same time. This is why the + /// thumbnails are stored in both disk and memory cache. The disk cache + /// has no size or time limit. + /// + /// The original images (``ImageSize/original``) are rarely displayed by the + /// app and you usually preview only one image at a time. The original images + /// are _not_ stored in the memory cache as they may take up too much space. + /// They are stored in a custom `URLCache` instance that automatically evicts + /// images if it reaches the size limit. @MainActor - func thumbnail(for media: Media, size: ThumbnailSize = .small) async throws -> UIImage { - guard media.remoteStatus != .stub else { - let media = try await fetchStubMedia(for: media) - return try await _thumbnail(for: media, size: size) + func image(for media: Media, size: ImageSize) async throws -> UIImage { + let media = try await getSafeMedia(for: media) + switch size { + case .small, .large: + return try await thumbnail(for: media, size: size) + case .original: + return try await originalImage(for: media) } - return try await _thumbnail(for: media, size: size) } + /// Returns a thread-safe media object and materializes a stub if needed. @MainActor - private func _thumbnail(for media: Media, size: ThumbnailSize) async throws -> UIImage { - if let image = await cachedThumbnail(for: media.objectID, size: size) { + private func getSafeMedia(for media: Media) async throws -> SafeMedia { + guard media.remoteStatus != .stub else { + guard let mediaID = media.mediaID else { + throw URLError(.unknown) // This should never happen + } + let blogID = TaggedManagedObjectID(media.blog) + return try await fetchStubMedia(for: mediaID, blogID: blogID) + } + return SafeMedia(media) + } + + // MARK: - Media (Original) + + /// Returns a full-size image for the given media asset. + /// + /// The app rarely loads full-size images, and they make take a significant + /// amount of space and memory, so they are cached only in `URLCache`. + private func originalImage(for media: SafeMedia) async throws -> UIImage { + guard media.mediaType == .image else { + assertionFailure("Unsupported media type: \(media.mediaType)") + throw Error.unsupportedMediaType(media.mediaType) + } + if let localURL = media.absoluteLocalURL, + let image = try? await ImageDecoder.makeImage(from: localURL) { + return image + } + if let info = await getFullsizeImageInfo(for: media) { + let data = try await data(for: info, isCached: true) + return try await ImageDecoder.makeImage(from: data) + } + // The media has no local or remote URL – should never happen + throw Error.missingImageURL + } + + private func getFullsizeImageInfo(for media: SafeMedia) async -> RemoteImageInfo? { + guard let remoteURL = media.remoteURL.flatMap(URL.init) else { + return nil + } + return try? await coreDataStack.performQuery { context in + let blog = try context.existingObject(with: media.blogID) + return RemoteImageInfo(imageURL: remoteURL, host: MediaHost(with: blog)) + } + } + + // MARK: - Media (Thumbnails) + + private func thumbnail(for media: SafeMedia, size: ImageSize) async throws -> UIImage { + guard media.mediaType == .image || media.mediaType == .video else { + assertionFailure("Unsupported thubmnail media type: \(media.mediaType)") + throw Error.unsupportedMediaType(media.mediaType) + } + guard size != .original else { + assertionFailure("Unsupported thumbnail size: \(size)") + throw Error.unsupportedThumbnailSize(size) + } + + if let image = cache.getImage(forKey: makeCacheKey(for: media.mediaID, size: size)) { + return image + } + let image = try await actuallyLoadThumbnail(for: media, size: size) + cache.setImage(image, forKey: makeCacheKey(for: media.mediaID, size: size)) + return image + } + + private func actuallyLoadThumbnail(for media: SafeMedia, size: ImageSize) async throws -> UIImage { + if let image = await cachedThumbnail(for: media.mediaID, size: size) { return image } if let image = await localThumbnail(for: media, size: size) { @@ -57,70 +142,61 @@ final class MediaImageService: NSObject { return try await remoteThumbnail(for: media, size: size) } - // MARK: - Cached Thumbnail + // MARK: - Thumbnails (Memory Cache) - /// Returns a local thumbnail for the given media object (if available). - private func cachedThumbnail(for mediaID: NSManagedObjectID, size: ThumbnailSize) async -> UIImage? { - return try? await Task.detached { - let imageURL = try self.getCachedThumbnailURL(for: mediaID, size: size) - let data = try Data(contentsOf: imageURL) - return try makeImage(from: data) - }.value + /// Returns cached image for the given thumbnail. + nonisolated func getCachedThumbnail(for mediaID: TaggedManagedObjectID, size: ImageSize = .small) -> UIImage? { + cache.getImage(forKey: makeCacheKey(for: mediaID, size: size)) } - // The save is performed asynchronously to eliminate any delays. It's - // exceedingly unlikely it will result in any duplicated work thanks to the - // memore caches. - private func saveThumbnail(for mediaID: NSManagedObjectID, size: ThumbnailSize, _ closure: @escaping (URL) throws -> Void) { - ioQueue.async { - if let targetURL = try? self.getCachedThumbnailURL(for: mediaID, size: size) { - try? closure(targetURL) - } - } + // MARK: - Thumbnails (Disk Cache) + + /// Returns a local thumbnail for the given media object (if available). + private func cachedThumbnail(for mediaID: TaggedManagedObjectID, size: ImageSize) async -> UIImage? { + guard let fileURL = getCachedThumbnailURL(for: mediaID, size: size) else { return nil } + return try? await ImageDecoder.makeImage(from: fileURL) } - private func getCachedThumbnailURL(for mediaID: NSManagedObjectID, size: ThumbnailSize) throws -> URL { - let mediaID = mediaID.uriRepresentation().lastPathComponent - return try mediaFileManager.makeLocalMediaURL( + private func getCachedThumbnailURL(for mediaID: TaggedManagedObjectID, size: ImageSize) -> URL? { + let mediaID = mediaID.objectID.uriRepresentation().lastPathComponent + return try? mediaFileManager.makeLocalMediaURL( withFilename: "\(mediaID)-\(size.rawValue)-thumbnail", fileExtension: nil, // We don't know ahead of time incremented: false ) } - /// Flushes all pending I/O changes to disk. - /// - /// - warning: For testing purposes only. - func flush() { - ioQueue.sync {} - } - // MARK: - Local Thumbnail /// Generates a thumbnail from a local asset and saves it in cache. - @MainActor - private func localThumbnail(for media: Media, size: ThumbnailSize) async -> UIImage? { + private func localThumbnail(for media: SafeMedia, size: ImageSize) async -> UIImage? { + guard let url = await generateLocalThumbnail(for: media, size: size) else { + return nil + } + return try? await ImageDecoder.makeImage(from: url) + } + + private func generateLocalThumbnail(for media: SafeMedia, size: ImageSize) async -> URL? { guard let sourceURL = media.absoluteLocalURL else { return nil } - let exporter = makeThumbnailExporter(for: media, size: size) + let exporter = await makeThumbnailExporter(for: media, size: size) + if sourceURL.isGif { + exporter.options.thumbnailImageType = UTType.gif.identifier + } guard exporter.supportsThumbnailExport(forFile: sourceURL), let (_, export) = try? await exporter.exportThumbnail(forFileURL: sourceURL), - let image = try? await makeImage(from: export.url) + let thumbnailURL = getCachedThumbnailURL(for: media.mediaID, size: size) else { return nil } - - // The order is important to ensure `export.url` still exists when creating an image - saveThumbnail(for: media.objectID, size: size) { targetURL in - try FileManager.default.moveItem(at: export.url, to: targetURL) - } - - return image + try? FileManager.default.moveItem(at: export.url, to: thumbnailURL) + return thumbnailURL } - private func makeThumbnailExporter(for media: Media, size: ThumbnailSize) -> MediaThumbnailExporter { + @MainActor + private func makeThumbnailExporter(for media: SafeMedia, size: ImageSize) -> MediaThumbnailExporter { let exporter = MediaThumbnailExporter() exporter.mediaDirectoryType = .cache exporter.options.preferredSize = MediaImageService.getThumbnailSize(for: media, size: size) @@ -128,13 +204,22 @@ final class MediaImageService: NSObject { return exporter } + /// - warning: This method was added only for backward-compatability with + /// the editor that relies on using URLs for displaying the preview thumbnail + /// while the image is loaded. + public func getThumbnailURL(for media: Media, _ completion: @escaping (URL?) -> Void) { + let media = SafeMedia(media) + Task { + let url = await generateLocalThumbnail(for: media, size: .large) + completion(url) + } + } + // MARK: - Remote Thumbnail /// Downloads a remote thumbnail and saves it in cache. - @MainActor - private func remoteThumbnail(for media: Media, size: ThumbnailSize) async throws -> UIImage { - let targetSize = MediaImageService.getThumbnailSize(for: media, size: size) - guard let imageURL = media.getRemoteThumbnailURL(targetSize: targetSize) else { + private func remoteThumbnail(for media: SafeMedia, size: ImageSize) async throws -> UIImage { + guard let info = await getRemoteThumbnailInfo(for: media, size: size) else { // Self-hosted WordPress sites don't have `remoteThumbnailURL`, so // the app generates the thumbnail by itself. if media.mediaType == .video { @@ -142,58 +227,66 @@ final class MediaImageService: NSObject { } throw URLError(.badURL) } - - let blogID = TaggedManagedObjectID(media.blog) - let host = try await coreDataStack.performQuery { context in - MediaHost(with: try context.existingObject(with: blogID)) - } - let request = try await MediaRequestAuthenticator() - .authenticatedRequest(for: imageURL, host: host) - guard !Task.isCancelled else { - throw CancellationError() - } - let (data, response) = try await session.data(for: request) - guard let statusCode = (response as? HTTPURLResponse)?.statusCode, - (200..<400).contains(statusCode) else { - throw URLError(.unknown) - } - let image = try await Task.detached { - try makeImage(from: data) - }.value - saveThumbnail(for: media.objectID, size: size) { targetURL in - try data.write(to: targetURL) + // The service has a custom disk cache for thumbnails, so it's important to + // disable the native url cache which is by default set to `URLCache.shared` + let data = try await data(for: info, isCached: false) + let image = try await ImageDecoder.makeImage(from: data) + if let fileURL = getCachedThumbnailURL(for: media.mediaID, size: size) { + try? data.write(to: fileURL) } return image } + // There are two reasons why these operations are performed in the background: + // performance and making sure the subsystem is thread-safe and can be used + // from the background. + private func getRemoteThumbnailInfo(for media: SafeMedia, size: ImageSize) async -> RemoteImageInfo? { + let targetSize = await MediaImageService.getThumbnailSize(for: media, size: size) + return try? await coreDataStack.performQuery { context in + let blog = try context.existingObject(with: media.blogID) + guard let imageURL = media.getRemoteThumbnailURL(targetSize: targetSize, blog: blog) else { return nil } + return RemoteImageInfo(imageURL: imageURL, host: MediaHost(with: blog)) + } + } + + // MARK: - Networking + + private func data(for info: RemoteImageInfo, isCached: Bool) async throws -> Data { + let options = ImageRequestOptions(isDiskCacheEnabled: isCached) + return try await downloader.data(from: info.imageURL, host: info.host, options: options) + } + + private struct RemoteImageInfo { + let imageURL: URL + let host: MediaHost + } + // MARK: - Thubmnail for Video - @MainActor - private func generateThumbnailForVideo(for media: Media, size: ThumbnailSize) async throws -> UIImage { + private func generateThumbnailForVideo(for media: SafeMedia, size: ImageSize) async throws -> UIImage { guard let videoURL = media.remoteURL.flatMap(URL.init) else { throw URLError(.badURL) } - let exporter = makeThumbnailExporter(for: media, size: size) + let exporter = await makeThumbnailExporter(for: media, size: size) let (_, export) = try await exporter.exportThumbnail(forVideoURL: videoURL) - let image = try await makeImage(from: export.url) + let image = try await ImageDecoder.makeImage(from: export.url) // The order is important to ensure `export.url` exists when making an image - saveThumbnail(for: media.objectID, size: size) { targetURL in - try FileManager.default.moveItem(at: export.url, to: targetURL) + if let fileURL = getCachedThumbnailURL(for: media.mediaID, size: size) { + try? FileManager.default.moveItem(at: export.url, to: fileURL) } return image } // MARK: - Stubs - @MainActor - private func fetchStubMedia(for media: Media) async throws -> Media { - guard let mediaID = media.mediaID else { - throw MediaThumbnailExporter.ThumbnailExportError.failedToGenerateThumbnailFileURL - } + private func fetchStubMedia(for mediaID: NSNumber, blogID: TaggedManagedObjectID) async throws -> SafeMedia { let mediaRepository = MediaRepository(coreDataStack: coreDataStack) - let objectID = try await mediaRepository.getMedia(withID: mediaID, in: .init(media.blog)) - return try coreDataStack.mainContext.existingObject(with: objectID) + let objectID = try await mediaRepository.getMedia(withID: mediaID, in: blogID) + return try await coreDataStack.performQuery { context in + let media = try context.existingObject(with: objectID) + return SafeMedia(media) + } } } @@ -201,19 +294,30 @@ final class MediaImageService: NSObject { extension MediaImageService { - enum ThumbnailSize: String { + enum ImageSize: String { /// The small thumbnail that can be used in collection view cells and /// similar situations. case small + + /// A large thumbnail thumbnail that can typically be used to fit + /// the entire screen on iPhone or a large portion of the sreen on iPad. + case large + + /// Loads an original image. + case original + } + + @MainActor + fileprivate static func getThumbnailSize(for media: SafeMedia, size: ImageSize) -> CGSize { + let mediaSize = media.size ?? CGSize(width: 1024, height: 1024) // rhs should never happen + return MediaImageService.getThumbnailSize(for: mediaSize, size: size) + } /// Returns an optimal target size in pixels for a thumbnail of the given /// size for the given media asset. - static func getThumbnailSize(for media: Media, size: ThumbnailSize) -> CGSize { - let mediaSize = CGSize( - width: CGFloat(media.width?.floatValue ?? 0), - height: CGFloat(media.height?.floatValue ?? 0) - ) + @MainActor + static func getThumbnailSize(for mediaSize: CGSize, size: ImageSize) -> CGSize { let targetSize = MediaImageService.getPreferredThumbnailSize(for: size) return MediaImageService.targetSize(forMediaSize: mediaSize, targetSize: targetSize) } @@ -223,7 +327,9 @@ extension MediaImageService { /// - important: It makes sure the app uses the same thumbnails across /// different screens and presentation modes to avoid fetching and caching /// more than one version of the same image. - private static func getPreferredThumbnailSize(for thumbnail: ThumbnailSize) -> CGSize { + @MainActor + private static func getPreferredThumbnailSize(for thumbnail: ImageSize) -> CGSize { + let minScreenSide = min(UIScreen.main.bounds.width, UIScreen.main.bounds.height) switch thumbnail { case .small: /// The size is calculated to fill a collection view cell, assuming the app @@ -231,12 +337,17 @@ extension MediaImageService { /// on whether the device is in landscape or portrait mode, but the thumbnail size is /// guaranteed to always be the same across app launches and optimized for /// a portraint (dominant) mode. - let screenSide = min(UIScreen.main.bounds.width, UIScreen.main.bounds.height) let itemPerRow = UIDevice.current.userInterfaceIdiom == .pad ? 5 : 4 - let availableWidth = screenSide - SiteMediaCollectionViewController.spacing * CGFloat(itemPerRow - 1) + let availableWidth = minScreenSide - SiteMediaCollectionViewController.spacing * CGFloat(itemPerRow - 1) let targetSide = (availableWidth / CGFloat(itemPerRow)).rounded(.down) let targetSize = CGSize(width: targetSide, height: targetSide) return targetSize.scaled(by: UIScreen.main.scale) + case .large: + let side = min(1024, minScreenSide * UIScreen.main.scale) + return CGSize(width: side, height: side) + case .original: + assertionFailure("Unsupported thumbnail size") + return CGSize(width: 2048, height: 2048) } } @@ -269,14 +380,37 @@ extension MediaImageService { } } -// MARK: - Helpers (RemoteURL) +// MARK: - SafeMedia + +/// A thread-safe media wrapper for use by `MediaImageService`. +private final class SafeMedia { + let mediaID: TaggedManagedObjectID + let blogID: TaggedManagedObjectID + let mediaType: MediaType + let absoluteLocalURL: URL? + let remoteThumbnailURL: String? + let remoteURL: String? + let size: CGSize? + + init(_ media: Media) { + self.mediaID = TaggedManagedObjectID(media) + self.blogID = TaggedManagedObjectID(media.blog) + self.mediaType = media.mediaType + self.absoluteLocalURL = media.absoluteLocalURL + self.remoteURL = media.remoteURL + self.remoteThumbnailURL = media.remoteThumbnailURL + if let width = media.width?.floatValue, let height = media.height?.floatValue { + self.size = CGSize(width: CGFloat(width), height: CGFloat(height)) + } else { + self.size = nil + } + } -private extension Media { /// Returns the thumbnail remote URL with a given target size. It uses /// Image CDN (formerly Photon) if available. /// /// - parameter targetSize: Target size in pixels. - func getRemoteThumbnailURL(targetSize: CGSize) -> URL? { + func getRemoteThumbnailURL(targetSize: CGSize, blog: Blog) -> URL? { switch mediaType { case .image: guard let remoteURL = remoteURL.flatMap(URL.init) else { @@ -291,7 +425,7 @@ private extension Media { .scaled(by: 1.0 / scale) .scaled(by: min(2, scale)) } - if !isEligibleForPhoton { + if !blog.isEligibleForPhoton { return WPImageURLHelper.imageURLWithSize(targetSize, forImageURL: remoteURL) } else { let targetSize = targetSize.scaled(by: 1.0 / UIScreen.main.scale) @@ -301,60 +435,14 @@ private extension Media { return remoteThumbnailURL.flatMap(URL.init) } } - - var isEligibleForPhoton: Bool { - !(blog.isPrivateAtWPCom() || (!blog.isHostedAtWPcom && blog.isBasicAuthCredentialStored())) - } } -// MARK: - Helpers (Decompression) - -private func makeImage(from fileURL: URL) async throws -> UIImage { - try await Task.detached { - let data = try Data(contentsOf: fileURL) - return try makeImage(from: data) - }.value -} - -// Forces decompression (or bitmapping) to happen in the background. -// It's very expensive for some image formats, such as JPEG. -private func makeImage(from data: Data) throws -> UIImage { - guard let image = UIImage(data: data) else { - throw URLError(.cannotDecodeContentData) - } - if data.isMatchingMagicNumbers(Data.gifMagicNumbers) { - return AnimatedImageWrapper(gifData: data) ?? image - } - guard isDecompressionNeeded(for: data) else { - return image +private extension Blog { + var isEligibleForPhoton: Bool { + !(isPrivateAtWPCom() || (!isHostedAtWPcom && isBasicAuthCredentialStored())) } - return image.preparingForDisplay() ?? image -} - -private func isDecompressionNeeded(for data: Data) -> Bool { - // This check is required to avoid the following error messages when - // using `preparingForDisplay`: - // - // [Decompressor] Error -17102 decompressing image -- possibly corrupt - // - // More info: https://github.com/SDWebImage/SDWebImage/issues/3365 - data.isMatchingMagicNumbers(Data.jpegMagicNumbers) } -private extension Data { - // JPEG magic numbers https://en.wikipedia.org/wiki/JPEG - static let jpegMagicNumbers: [UInt8] = [0xFF, 0xD8, 0xFF] - - // GIF magic numbers https://en.wikipedia.org/wiki/GIF - static let gifMagicNumbers: [UInt8] = [0x47, 0x49, 0x46] - - func isMatchingMagicNumbers(_ numbers: [UInt8?]) -> Bool { - guard self.count >= numbers.count else { - return false - } - return zip(numbers.indices, numbers).allSatisfy { index, number in - guard let number = number else { return true } - return self[index] == number - } - } +private func makeCacheKey(for mediaID: TaggedManagedObjectID, size: MediaImageService.ImageSize) -> String { + "\(mediaID.objectID)-\(size.rawValue)" } diff --git a/WordPress/Classes/Services/MediaImportService.swift b/WordPress/Classes/Services/MediaImportService.swift index 7f550abd29ef..2a166aa4b673 100644 --- a/WordPress/Classes/Services/MediaImportService.swift +++ b/WordPress/Classes/Services/MediaImportService.swift @@ -2,7 +2,7 @@ import Foundation import CocoaLumberjack import PhotosUI -/// Encapsulates importing assets such as PHAssets, images, videos, or files at URLs to Media objects. +/// Encapsulates importing assets such as images, videos, or files at URLs to Media objects. /// /// - Note: Methods with escaping closures will call back via the configured managedObjectContext /// method and its corresponding thread. @@ -232,7 +232,7 @@ class MediaImportService: NSObject { return self.import(exportable, to: media, options: options, completion: completion) } - /// Imports media from a PHAsset to the Media object, asynchronously. + /// Imports media from exportable assets to the Media object, asynchronously. /// /// - Parameters: /// - exportable: the exportable resource where data will be read from. @@ -279,12 +279,6 @@ class MediaImportService: NSObject { private func makeExporter(for exportable: ExportableAsset, options: ExportOptions) -> MediaExporter? { switch exportable { - case let asset as PHAsset: - let exporter = MediaAssetExporter(asset: asset) - exporter.imageOptions = options.imageOptions - exporter.videoOptions = options.videoOptions - exporter.allowableFileExtensions = options.allowableFileExtensions.isEmpty ? MediaImportService.defaultAllowableFileExtensions : options.allowableFileExtensions - return exporter case let provider as NSItemProvider: let exporter = ItemProviderMediaExporter(provider: provider) exporter.imageOptions = options.imageOptions @@ -312,7 +306,7 @@ class MediaImportService: NSObject { } /// Generate a thumbnail image for the `Media` so that consumers of the `absoluteThumbnailLocalURL` property - /// will have an image ready to load, without using the async methods provided via `MediaThumbnailService`. + /// will have an image ready to load, without using the async methods provided via `MediaImageService`. /// /// This is primarily used as a placeholder image throughout the code-base, particulary within the editors. /// @@ -321,8 +315,7 @@ class MediaImportService: NSObject { /// via the new thumbnail service methods is much preferred, but would indeed take a good bit of refactoring away from /// using `absoluteThumbnailLocalURL`. func exportPlaceholderThumbnail(for media: Media, completion: ((URL?) -> Void)?) { - let thumbnailService = MediaThumbnailService(coreDataStack: coreDataStack) - thumbnailService.thumbnailURL(forMedia: media, preferredSize: .zero) { url in + MediaImageService.shared.getThumbnailURL(for: media) { url in self.coreDataStack.performAndSave({ context in let mediaInContext = try context.existingObject(with: media.objectID) as! Media // Set the absoluteThumbnailLocalURL with the generated thumbnail's URL. @@ -330,8 +323,6 @@ class MediaImportService: NSObject { }, completion: { _ in completion?(url) }, on: .main) - } onError: { error in - DDLogError("Error occurred exporting placeholder thumbnail: \(error)") } } @@ -341,8 +332,6 @@ class MediaImportService: NSObject { // Write an error logging message to help track specific sources of export errors. var errorLogMessage = "Error occurred importing to Media" switch error { - case is MediaAssetExporter.AssetExportError: - errorLogMessage.append(" with asset export error") case is MediaImageExporter.ImageExportError: errorLogMessage.append(" with image export error") case is MediaURLExporter.URLExportError: @@ -378,7 +367,7 @@ class MediaImportService: NSObject { var options = MediaImageExporter.Options() options.maximumImageSize = self.exporterMaximumImageSize() options.stripsGeoLocationIfNeeded = MediaSettings().removeLocationSetting - options.imageCompressionQuality = MediaImportService.preferredImageCompressionQuality + options.imageCompressionQuality = MediaSettings().imageQualityForUpload.doubleValue return options } diff --git a/WordPress/Classes/Services/MediaRepository.swift b/WordPress/Classes/Services/MediaRepository.swift index 786666814e09..78eafc876446 100644 --- a/WordPress/Classes/Services/MediaRepository.swift +++ b/WordPress/Classes/Services/MediaRepository.swift @@ -18,14 +18,7 @@ final class MediaRepository { /// Get the Media object from the server using the blog and the mediaID as the identifier of the resource func getMedia(withID mediaID: NSNumber, in blogID: TaggedManagedObjectID) async throws -> TaggedManagedObjectID { - let remote = try await coreDataStack.performQuery { [remoteFactory] context in - let blog = try context.existingObject(with: blogID) - return remoteFactory.remote(for: blog) - } - guard let remote else { - throw MediaRepository.Error.remoteAPIUnavailable - } - + let remote = try await remote(for: blogID) let remoteMedia: RemoteMedia? = try await withCheckedThrowingContinuation { continuation in remote.getMediaWithID( mediaID, success: continuation.resume(returning:), @@ -43,12 +36,57 @@ final class MediaRepository { } } + /// Deletes the Media object from the server. Note the Media is deleted, not trashed. + func delete(_ mediaID: TaggedManagedObjectID) async throws { + // Delete the media from WordPress Media Library + let queryResult: (MediaServiceRemote, RemoteMedia)? = try await coreDataStack.performQuery { [remoteFactory] context in + guard let media = try? context.existingObject(with: mediaID) else { + return nil + } + // No need to delete the media from Media Library if it's not synced + if media.remoteStatus != .sync { + return nil + } + let remote = try remoteFactory.remote(for: media.blog) + return (remote, RemoteMedia.from(media)) + } + if let queryResult { + let (remote, remoteMedia) = queryResult + + try await withCheckedThrowingContinuation { [remote] continuation in + remote.delete( + remoteMedia, + success: { continuation.resume(returning: ()) }, + failure: { continuation.resume(throwing: $0!) } + ) + } + } + + // Delete the media locally from Core Data. + // + // Considering the intent of calling this method is to delete the media object, + // when it doesn't exist, we can treat the flow as success, since the intent is fulfilled. + try? await coreDataStack.performAndSave { context in + let media = try context.existingObject(with: mediaID) + context.delete(media) + } + } + +} + +private extension MediaRepository { + func remote(for blogID: TaggedManagedObjectID) async throws -> MediaServiceRemote { + try await coreDataStack.performQuery { [remoteFactory] context in + let blog = try context.existingObject(with: blogID) + return try remoteFactory.remote(for: blog) + } + } } @objc class MediaServiceRemoteFactory: NSObject { - @objc(remoteForBlog:) - func remote(for blog: Blog) -> MediaServiceRemote? { + @objc(remoteForBlog:error:) + func remote(for blog: Blog) throws -> MediaServiceRemote { if blog.supports(.wpComRESTAPI), let dotComID = blog.dotComID, let api = blog.wordPressComRestApi() { return MediaServiceRemoteREST(wordPressComRestApi: api, siteID: dotComID) } @@ -57,7 +95,7 @@ final class MediaRepository { return MediaServiceRemoteXMLRPC(api: api, username: username, password: password) } - return nil + throw MediaRepository.Error.remoteAPIUnavailable } } @@ -87,3 +125,31 @@ extension MediaServiceRemote { } } + +extension RemoteMedia { + + @objc(remoteMediaWithMedia:) + static func from(_ media: Media) -> RemoteMedia { + let remoteMedia = RemoteMedia() + remoteMedia.mediaID = media.mediaID + remoteMedia.url = media.remoteURL.flatMap(URL.init(string:)) + remoteMedia.largeURL = media.remoteLargeURL.flatMap(URL.init(string:)) + remoteMedia.mediumURL = media.remoteMediumURL.flatMap(URL.init(string:)) + remoteMedia.date = media.creationDate + remoteMedia.file = media.filename + remoteMedia.`extension` = media.fileExtension() ?? "unknown" + remoteMedia.title = media.title + remoteMedia.caption = media.caption + remoteMedia.descriptionText = media.desc + remoteMedia.alt = media.alt + remoteMedia.height = media.height + remoteMedia.width = media.width + remoteMedia.localURL = media.absoluteLocalURL + remoteMedia.mimeType = media.mimeType + remoteMedia.videopressGUID = media.videopressGUID + remoteMedia.remoteThumbnailURL = media.remoteThumbnailURL + remoteMedia.postID = media.postID + return remoteMedia + } + +} diff --git a/WordPress/Classes/Services/MediaService.h b/WordPress/Classes/Services/MediaService.h index 7b547e72f4ba..1187138f5748 100644 --- a/WordPress/Classes/Services/MediaService.h +++ b/WordPress/Classes/Services/MediaService.h @@ -59,7 +59,7 @@ typedef NS_ERROR_ENUM(MediaServiceErrorDomain, MediaServiceError) { @failure a block that will be invoked when there is upload error. */ - (void)updateMedia:(nonnull Media *)media - fieldsToUpdate:(NSArray *)fieldsToUpdate + fieldsToUpdate:(nonnull NSArray *)fieldsToUpdate success:(nullable void (^)(void))success failure:(nullable void (^)(NSError * _Nullable error))failure; @@ -88,34 +88,10 @@ typedef NS_ERROR_ENUM(MediaServiceErrorDomain, MediaServiceError) { @param success */ - (void)updateMedia:(nonnull NSArray *)mediaObjects - fieldsToUpdate:(NSArray *)fieldsToUpdate + fieldsToUpdate:(nonnull NSArray *)fieldsToUpdate overallSuccess:(nullable void (^)(void))overallSuccess failure:(nullable void (^)(NSError * _Nullable error))failure; -/** - Deletes the Media object from the server. Note the Media is deleted, not trashed. - - @param media object to delete. - @param success a block that will be invoked when the media deletion finished with success - @param failure a block that will be invoked when there is an error. - */ -- (void)deleteMedia:(nonnull Media *)media - success:(nullable void (^)(void))success - failure:(nullable void (^)(NSError * _Nonnull error))failure; - -/** - Deletes multiple Media objects from the server. Note the Media objects are deleted, not trashed. - - @param mediaObjects An array of media objects to delete. - @param progress a block that will be invoked after each media item is deleted successfully - @param success a block that will be invoked when the media deletion finished with success - @param failure a block that will be invoked when there is an error. - */ -- (void)deleteMedia:(nonnull NSArray *)mediaObjects - progress:(nullable void (^)(NSProgress *_Nonnull progress))progress - success:(nullable void (^)(void))success - failure:(nullable void (^)(void))failure; - /** * Sync all Media objects from the server to local database diff --git a/WordPress/Classes/Services/MediaService.m b/WordPress/Classes/Services/MediaService.m index e39474454f4f..355b7c9adac3 100644 --- a/WordPress/Classes/Services/MediaService.m +++ b/WordPress/Classes/Services/MediaService.m @@ -72,7 +72,7 @@ - (void)uploadMedia:(Media *)media { Blog *blog = media.blog; id remote = [self remoteForBlog:blog]; - RemoteMedia *remoteMedia = [self remoteMediaFromMedia:media]; + RemoteMedia *remoteMedia = [RemoteMedia remoteMediaWithMedia:media]; // Even though jpeg is a valid extension, use jpg instead for the widest possible // support. Some third-party image related plugins prefer the .jpg extension. // See https://github.com/wordpress-mobile/WordPress-iOS/issues/4663 @@ -82,7 +82,7 @@ - (void)uploadMedia:(Media *)media void (^failureBlock)(NSError *error) = ^(NSError *error) { [self.managedObjectContext performBlock:^{ if (error) { - [self trackUploadError:error]; + [self trackUploadError:error blog:blog]; DDLogError(@"Error uploading media: %@", error); } NSError *customError = [self customMediaUploadError:error remote:remote]; @@ -165,11 +165,12 @@ - (void)uploadMedia:(Media *)media #pragma mark - Private helpers - (void)trackUploadError:(NSError *)error + blog:(Blog *)blog { if (error.code == NSURLErrorCancelled) { - [WPAppAnalytics track:WPAnalyticsStatMediaServiceUploadCanceled]; + [WPAppAnalytics track:WPAnalyticsStatMediaServiceUploadCanceled withBlog:blog]; } else { - [WPAppAnalytics track:WPAnalyticsStatMediaServiceUploadFailed error:error]; + [WPAppAnalytics track:WPAnalyticsStatMediaServiceUploadFailed error:error withBlogID:blog.dotComID]; } } @@ -184,7 +185,7 @@ - (void)updateMedia:(Media *)media if (fieldsToUpdate != nil && [fieldsToUpdate count] > 0) { remoteMedia = [self remoteMediaFromMedia:media fieldsToUpdate:fieldsToUpdate]; } else { - remoteMedia = [self remoteMediaFromMedia:media]; + remoteMedia = [RemoteMedia remoteMediaWithMedia:media]; } id remote = [self remoteForBlog:media.blog]; @@ -366,7 +367,7 @@ - (void)deleteMedia:(nonnull Media *)media } id remote = [self remoteForBlog:media.blog]; - RemoteMedia *remoteMedia = [self remoteMediaFromMedia:media]; + RemoteMedia *remoteMedia = [RemoteMedia remoteMediaWithMedia:media]; [remote deleteMedia:remoteMedia success:successBlock @@ -528,30 +529,6 @@ - (void)mergeMedia:(NSArray *)media } } -- (RemoteMedia *)remoteMediaFromMedia:(Media *)media -{ - RemoteMedia *remoteMedia = [[RemoteMedia alloc] init]; - remoteMedia.mediaID = media.mediaID; - remoteMedia.url = [NSURL URLWithString:media.remoteURL]; - remoteMedia.largeURL = [NSURL URLWithString:media.remoteLargeURL]; - remoteMedia.mediumURL = [NSURL URLWithString:media.remoteMediumURL]; - remoteMedia.date = media.creationDate; - remoteMedia.file = media.filename; - remoteMedia.extension = [media fileExtension] ?: @"unknown"; - remoteMedia.title = media.title; - remoteMedia.caption = media.caption; - remoteMedia.descriptionText = media.desc; - remoteMedia.alt = media.alt; - remoteMedia.height = media.height; - remoteMedia.width = media.width; - remoteMedia.localURL = media.absoluteLocalURL; - remoteMedia.mimeType = media.mimeType; - remoteMedia.videopressGUID = media.videopressGUID; - remoteMedia.remoteThumbnailURL = media.remoteThumbnailURL; - remoteMedia.postID = media.postID; - return remoteMedia; -} - - (RemoteMedia *)remoteMediaFromMedia:(Media *)media fieldsToUpdate:(NSArray *)fieldsToUpdate { RemoteMedia *remoteMedia = [[RemoteMedia alloc] init]; diff --git a/WordPress/Classes/Services/MediaSettings.swift b/WordPress/Classes/Services/MediaSettings.swift index d20aec639a94..8b01674c3d3b 100644 --- a/WordPress/Classes/Services/MediaSettings.swift +++ b/WordPress/Classes/Services/MediaSettings.swift @@ -3,14 +3,55 @@ import AVFoundation class MediaSettings: NSObject { // MARK: - Constants + fileprivate let imageOptimizationKey = "SavedImageOptimizationSetting" fileprivate let maxImageSizeKey = "SavedMaxImageSizeSetting" + fileprivate let imageQualityKey = "SavedImageQualitySetting" fileprivate let removeLocationKey = "SavedRemoveLocationSetting" fileprivate let maxVideoSizeKey = "SavedMaxVideoSizeSetting" + fileprivate let advertiseImageOptimizationKey = "SavedAdvertiseImageOptimization" + fileprivate let defaultImageOptimization = true + fileprivate let defaultMaxImageDimension = 2000 + fileprivate let defaultImageQuality: ImageQuality = .medium + fileprivate let defaultMaxVideoSize: VideoResolution = .sizeOriginal + fileprivate let defaultRemoveLocation = true fileprivate let minImageDimension = 150 fileprivate let maxImageDimension = 3000 + enum ImageQuality: String { + case maximum = "MaximumQuality100" + case high = "HighQuality90" + case medium = "MediumQuality80" + case low = "LowQuality70" + + var doubleValue: Double { + switch self { + case .maximum: + return 1.0 + case .high: + return 0.9 + case .medium: + return 0.8 + case .low: + return 0.7 + } + } + + var description: String { + switch self { + case .maximum: + return NSLocalizedString("appSettings.media.imageQuality.maximum", value: "Maximum", comment: "Indicates an image will use maximum quality when uploaded.") + case .high: + return NSLocalizedString("appSettings.media.imageQuality.high", value: "High", comment: "Indicates an image will use high quality when uploaded.") + case .medium: + return NSLocalizedString("appSettings.media.imageQuality.medium", value: "Medium", comment: "Indicates an image will use medium quality when uploaded.") + case(.low): + return NSLocalizedString("appSettings.media.imageQuality.low", value: "Low", comment: "Indicates an image will use low quality when uploaded.") + } + } + } + enum VideoResolution: String { case size640x480 = "AVAssetExportPreset640x480" case size1280x720 = "AVAssetExportPreset1280x720" @@ -62,23 +103,6 @@ class MediaSettings: NSObject { return 5 } } - - static func videoResolution(from value: Int) -> MediaSettings.VideoResolution { - switch value { - case 1: - return .size640x480 - case 2: - return .size1280x720 - case 3: - return .size1920x1080 - case 4: - return .size3840x2160 - case 5: - return .sizeOriginal - default: - return .sizeOriginal - } - } } // MARK: - Internal variables @@ -110,13 +134,19 @@ class MediaSettings: NSObject { /// - Note: if the image doesn't need to be resized, it returns `Int.max` /// @objc var imageSizeForUpload: Int { - if maxImageSizeSetting >= maxImageDimension { + // When image optimization is enabled, setting the max image size setting to + // the maximum value will be considered as to using the original size. + if !imageOptimizationEnabled || maxImageSizeSetting >= maxImageDimension { return Int.max } else { return maxImageSizeSetting } } + var imageQualityForUpload: ImageQuality { + return imageOptimizationEnabled ? imageQualitySetting : .high + } + /// The stored value for the maximum size images can have before uploading. /// If you set this to `maxImageDimension` or higher, it means images won't /// be resized on upload. @@ -134,7 +164,7 @@ class MediaSettings: NSObject { database.set(newSize, forKey: maxImageSizeKey) return Int(newSize) } else { - return maxImageDimension + return defaultMaxImageDimension } } set { @@ -148,7 +178,7 @@ class MediaSettings: NSObject { if let savedRemoveLocation = database.object(forKey: removeLocationKey) as? Bool { return savedRemoveLocation } else { - return true + return defaultRemoveLocation } } set { @@ -160,7 +190,7 @@ class MediaSettings: NSObject { get { guard let savedSize = database.object(forKey: maxVideoSizeKey) as? String, let videoSize = VideoResolution(rawValue: savedSize) else { - return .sizeOriginal + return defaultMaxVideoSize } return videoSize } @@ -168,4 +198,48 @@ class MediaSettings: NSObject { database.set(newValue.rawValue, forKey: maxVideoSizeKey) } } + + var imageOptimizationEnabled: Bool { + get { + if let savedImageOptimization = database.object(forKey: imageOptimizationKey) as? Bool { + return savedImageOptimization + } else { + return defaultImageOptimization + } + } + set { + database.set(newValue, forKey: imageOptimizationKey) + + // If the user changes this setting manually, we disable the image optimization popup. + if advertiseImageOptimization { + advertiseImageOptimization = false + } + } + } + + var imageQualitySetting: ImageQuality { + get { + guard let savedQuality = database.object(forKey: imageQualityKey) as? String, + let imageQuality = ImageQuality(rawValue: savedQuality) else { + return defaultImageQuality + } + return imageQuality + } + set { + database.set(newValue.rawValue, forKey: imageQualityKey) + } + } + + var advertiseImageOptimization: Bool { + get { + if let savedAdvertiseImageOptimization = database.object(forKey: advertiseImageOptimizationKey) as? Bool { + return savedAdvertiseImageOptimization + } else { + return true + } + } + set { + database.set(newValue, forKey: advertiseImageOptimizationKey) + } + } } diff --git a/WordPress/Classes/Services/MediaThumbnailCoordinator.swift b/WordPress/Classes/Services/MediaThumbnailCoordinator.swift deleted file mode 100644 index 4b01aae2c07d..000000000000 --- a/WordPress/Classes/Services/MediaThumbnailCoordinator.swift +++ /dev/null @@ -1,94 +0,0 @@ -import Foundation -import UIKit - -/// MediaThumbnailCoordinator is responsible for generating thumbnails for media -/// items, independently of a specific view controller. It should be accessed -/// via the `shared` singleton. -/// -class MediaThumbnailCoordinator: NSObject { - - @objc static let shared = MediaThumbnailCoordinator() - - private var coreDataStack: CoreDataStackSwift { - ContextManager.shared - } - - private let queue = DispatchQueue(label: "org.wordpress.media_thumbnail_coordinator", qos: .default) - - typealias ThumbnailBlock = (UIImage?, Error?) -> Void - typealias LoadStubMediaCompletionBlock = (Media?, Error?) -> Void - - /// Tries to generate a thumbnail for the specified media object with the size requested - /// - /// - Parameters: - /// - media: The media object to generate the thumbnail representation. - /// - size: The size of the thumbnail in pixels. - /// - onCompletion: a block that is invoked when the thumbnail generation is completed with success or failure. - @objc func thumbnail(for media: Media, with size: CGSize, onCompletion: @escaping ThumbnailBlock) { - if media.remoteStatus == .stub { - fetchThumbnailForMediaStub(for: media, with: size, onCompletion: onCompletion) - return - } - - let success: (URL?) -> Void = { (url) in - guard let imageURL = url else { - DispatchQueue.main.async { - onCompletion(nil, MediaThumbnailExporter.ThumbnailExportError.failedToGenerateThumbnailFileURL) - } - return - } - let image = UIImage(contentsOfFile: imageURL.path) - DispatchQueue.main.async { - onCompletion(image, nil) - } - } - let failure: (Error?) -> Void = { (error) in - DispatchQueue.main.async { - onCompletion(nil, error) - } - } - - let mediaThumbnailService = MediaThumbnailService(coreDataStack: coreDataStack) - mediaThumbnailService.exportQueue = self.queue - mediaThumbnailService.thumbnailURL(forMedia: media, preferredSize: size, onCompletion: success, onError: failure) - } - - /// Tries to generate a thumbnail for the specified media object that is stub with the size requested - /// - /// - Parameters: - /// - media: the media object to generate the thumbnail representation - /// - size: The size of the thumbnail in pixels. - /// - onCompletion: a block that is invoked when the thumbnail generation is completed with success or failure. - func fetchThumbnailForMediaStub(for media: Media, with size: CGSize, onCompletion: @escaping ThumbnailBlock) { - fetchStubMedia(for: media) { [weak self] (fetchedMedia, error) in - if let fetchedMedia = fetchedMedia { - self?.thumbnail(for: fetchedMedia, with: size, onCompletion: onCompletion) - } - } - } - - /// Fetch a media from a stub media - /// - /// - Parameters: - /// - media: the media object to fetch - /// - onCompletion: a block that is invoked when the media is loaded and fetched with success or failure. - func fetchStubMedia(for media: Media, onCompletion: @escaping LoadStubMediaCompletionBlock) { - guard let mediaID = media.mediaID else { - onCompletion(nil, MediaThumbnailExporter.ThumbnailExportError.failedToGenerateThumbnailFileURL) - return - } - - let mediaRepository = MediaRepository(coreDataStack: coreDataStack) - let blogID = TaggedManagedObjectID(media.blog) - Task { @MainActor in - do { - let mediaID = try await mediaRepository.getMedia(withID: mediaID, in: blogID) - // FIXME: Pass media object identifier to the completion block instead. - let loadedMedia = try coreDataStack.mainContext.existingObject(with: mediaID) - onCompletion(loadedMedia, nil) - } catch { - onCompletion(nil, error) - } - } - } -} diff --git a/WordPress/Classes/Services/MediaThumbnailService.swift b/WordPress/Classes/Services/MediaThumbnailService.swift deleted file mode 100644 index 20105ad4792b..000000000000 --- a/WordPress/Classes/Services/MediaThumbnailService.swift +++ /dev/null @@ -1,225 +0,0 @@ -import Foundation - -/// A service for handling the process of retrieving and generating thumbnail images -/// for existing Media objects, whether remote or locally available. -/// -class MediaThumbnailService: NSObject { - - /// Completion handler for a thumbnail URL. - /// - public typealias OnThumbnailURL = (URL?) -> Void - - /// Error handler. - /// - public typealias OnError = (Error) -> Void - - private static let defaultExportQueue: DispatchQueue = DispatchQueue(label: "org.wordpress.mediaThumbnailService", autoreleaseFrequency: .workItem) - - @objc public lazy var exportQueue: DispatchQueue = { - return MediaThumbnailService.defaultExportQueue - }() - - private let coreDataStack: CoreDataStackSwift - - /// The initialiser for Objective-C code. - /// - /// Using `ContextManager` as the argument becuase `CoreDataStackSwift` is not accessible from Objective-C code. - @objc - convenience init(contextManager: ContextManager) { - self.init(coreDataStack: contextManager) - } - - init(coreDataStack: CoreDataStackSwift) { - self.coreDataStack = coreDataStack - } - - /// Generate a URL to a thumbnail of the Media, if available. - /// - /// - Parameters: - /// - media: The Media object the URL should be a thumbnail of. - /// - preferredSize: An ideal size of the thumbnail in points. If `zero`, the maximum dimension of the UIScreen is used. - /// - onCompletion: Completion handler passing the URL once available, or nil if unavailable. This closure is called on the `exportQueue`. - /// - onError: Error handler. This closure is called on the `exportQueue`. - /// - /// - Note: Images may be downloaded and resized if required, avoid requesting multiple explicit preferredSizes - /// as several images could be downloaded, resized, and cached, if there are several variations in size. - /// - @objc func thumbnailURL(forMedia media: Media, preferredSize: CGSize, onCompletion: @escaping OnThumbnailURL, onError: OnError?) { - // We can use the main context here because we only read the `Media` instance, without changing it, and all - // the time consuming work is done in background queues. - let context = coreDataStack.mainContext - context.perform { - var objectInContext: NSManagedObject? - do { - objectInContext = try context.existingObject(with: media.objectID) - } catch { - self.exportQueue.async { - onError?(error) - } - return - } - guard let mediaInContext = objectInContext as? Media else { - return - } - // Configure a thumbnail exporter. - let exporter = MediaThumbnailExporter() - exporter.mediaDirectoryType = .cache - if preferredSize == CGSize.zero { - // When using a zero size, default to the maximum screen dimension. - let screenSize = UIScreen.main.bounds - let screenSizeMax = max(screenSize.width, screenSize.height) - exporter.options.preferredSize = CGSize(width: screenSizeMax, height: screenSizeMax) - } else { - exporter.options.preferredSize = preferredSize - } - - // Check if there is already an exported thumbnail available. - if let identifier = mediaInContext.localThumbnailIdentifier, let availableThumbnail = exporter.availableThumbnail(with: identifier) { - self.exportQueue.async { - onCompletion(availableThumbnail) - } - return - } - - // If we already set an identifier before let's reuse it - if let identifier = mediaInContext.localThumbnailIdentifier { - exporter.options.identifier = identifier - } else { - exporter.options.identifier = media.objectID.uriRepresentation().lastPathComponent - } - - // Configure a handler for any thumbnail exports - let onThumbnailExport: MediaThumbnailExporter.OnThumbnailExport = { (identifier, export) in - self.handleThumbnailExport(media: mediaInContext, - identifier: identifier, - export: export, - onCompletion: onCompletion) - } - // Configure an error handler - let onThumbnailExportError: OnExportError = { (error) in - self.handleExportError(error, errorHandler: onError) - } - - // Configure an attempt to download a remote thumbnail and export it as a thumbnail. - let attemptDownloadingThumbnail: () -> Void = { - self.downloadThumbnail(forMedia: mediaInContext, preferredSize: preferredSize, callbackQueue: self.exportQueue, onCompletion: { (image) in - guard let image = image else { - onError?(MediaThumbnailExporter.ThumbnailExportError.failedToGenerateThumbnailFileURL) - return - } - exporter.exportThumbnail(forImage: image, onCompletion: onThumbnailExport, onError: onThumbnailExportError) - }, onError: { (error) in - onError?(error) - }) - } - - // If the Media asset is available locally, export thumbnails from the local asset. - if let localAssetURL = mediaInContext.absoluteLocalURL, - exporter.supportsThumbnailExport(forFile: localAssetURL) { - self.exportQueue.async { - exporter.exportThumbnail(forFile: localAssetURL, - onCompletion: onThumbnailExport, - onError: onThumbnailExportError) - } - return - } - - // If the Media item is a video and has a remote video URL, try and export from the remote video URL. - if mediaInContext.mediaType == .video, let remoteURLStr = mediaInContext.remoteURL, let videoURL = URL(string: remoteURLStr) { - self.exportQueue.async { - exporter.exportThumbnail(forVideoURL: videoURL, - onCompletion: onThumbnailExport, - onError: { (error) in - // If an error occurred with the remote video URL, try and download the Media's - // remote thumbnail instead. - context.perform { - attemptDownloadingThumbnail() - } - }) - } - return - } - - // Try and download a remote thumbnail, if available. - attemptDownloadingThumbnail() - } - } - - /// Download a thumbnail image for a Media item, if available. - /// - /// - Parameters: - /// - media: The Media object. - /// - preferredSize: The preferred size of the image, in points, to configure remote URLs for. - /// - callbackQueue: The queue to execute the `onCompletion` or the `onError` callback. - /// - onCompletion: Completes if everything was successful, but nil if no image is available. - /// - onError: An error was encountered either from the server or locally, depending on the Media object or blog. - /// - /// - Note: based on previous implementation in MediaService.m. - /// - private func downloadThumbnail( - forMedia media: Media, - preferredSize: CGSize, - callbackQueue: DispatchQueue, - onCompletion: @escaping (UIImage?) -> Void, - onError: @escaping (Error) -> Void - ) { - var remoteURL: URL? - // Check if the Media item is a video or image. - if media.mediaType == .video { - // If a video, ensure there is a remoteThumbnailURL - if let remoteThumbnailURL = media.remoteThumbnailURL { - remoteURL = URL(string: remoteThumbnailURL) - } - } else { - // Check if a remote URL for the media itself is available. - if let remoteAssetURLStr = media.remoteURL, let remoteAssetURL = URL(string: remoteAssetURLStr) { - // Get an expected WP URL, for sizing. - if media.blog.isPrivateAtWPCom() || (!media.blog.isHostedAtWPcom && media.blog.isBasicAuthCredentialStored()) { - remoteURL = WPImageURLHelper.imageURLWithSize(preferredSize, forImageURL: remoteAssetURL) - } else { - let scale = 1.0 / UIScreen.main.scale - let preferredSize = preferredSize.applying(CGAffineTransform(scaleX: scale, y: scale)) - remoteURL = PhotonImageURLHelper.photonURL(with: preferredSize, forImageURL: remoteAssetURL) - } - } - } - guard let imageURL = remoteURL else { - // No URL's available, no images available. - callbackQueue.async { - onCompletion(nil) - } - return - } - - let download = AuthenticatedImageDownload(url: imageURL, mediaHost: MediaHost(with: media.blog), callbackQueue: callbackQueue, onSuccess: onCompletion, onFailure: onError) - - download.start() - } - - // MARK: - Helpers - - private func handleThumbnailExport(media: Media, identifier: MediaThumbnailExporter.ThumbnailIdentifier, export: MediaExport, onCompletion: @escaping OnThumbnailURL) { - coreDataStack.performAndSave({ context in - let object = try context.existingObject(with: media.objectID) - // It's safe to force-unwrap here, since the `object`, if exists, must be a `Media` type. - let mediaInContext = object as! Media - mediaInContext.localThumbnailIdentifier = identifier - }, completion: { (result: Result) in - switch result { - case .success: - onCompletion(export.url) - case .failure: - onCompletion(nil) - } - }, on: exportQueue) - } - - /// Handle the OnError callback and logging any errors encountered. - /// - private func handleExportError(_ error: MediaExportError, errorHandler: OnError?) { - MediaImportService.logExportError(error) - exportQueue.async { - errorHandler?(error.toNSError()) - } - } -} diff --git a/WordPress/Classes/Services/MenusService.h b/WordPress/Classes/Services/MenusService.h index c1b78825d356..f9522ade7f90 100644 --- a/WordPress/Classes/Services/MenusService.h +++ b/WordPress/Classes/Services/MenusService.h @@ -72,16 +72,12 @@ typedef void(^MenusServiceFailureBlock)(NSError *error); failure:(nullable MenusServiceFailureBlock)failure; /** - * @brief Generate a list MenuItems from the blog's top-level pages. + * @brief Create a list MenuItems from the given page. * - * @param blog The blog to use for pages. Cannot be nil. - * @param success The success handler. Can be nil. - * @param failure The failure handler. Can be nil. + * @return A MenuItem instance for the page if it's a top-level page. Otherwise, nil. * */ -- (void)generateDefaultMenuItemsForBlog:(Blog *)blog - success:(nullable void(^)(NSArray * _Nullable defaultItems))success - failure:(nullable MenusServiceFailureBlock)failure; +- (nullable MenuItem *)createItemWithPageID:(NSManagedObjectID *)pageObjectID inContext:(NSManagedObjectContext *)context; @end diff --git a/WordPress/Classes/Services/MenusService.m b/WordPress/Classes/Services/MenusService.m index ce37c91fd105..838af168bcf7 100644 --- a/WordPress/Classes/Services/MenusService.m +++ b/WordPress/Classes/Services/MenusService.m @@ -9,8 +9,6 @@ #import "WordPress-Swift.h" @import WordPressKit; -NS_ASSUME_NONNULL_BEGIN - @implementation MenusService #pragma mark - Menus availability @@ -161,47 +159,21 @@ - (void)deleteMenu:(Menu *)menu failure:failure]; } -- (void)generateDefaultMenuItemsForBlog:(Blog *)blog - success:(nullable void(^)(NSArray * _Nullable defaultItems))success - failure:(nullable MenusServiceFailureBlock)failure -{ - // Get the latest list of Pages available to the site. - PostServiceSyncOptions *options = [[PostServiceSyncOptions alloc] init]; - options.statuses = @[PostStatusPublish]; - options.order = PostServiceResultsOrderAscending; - options.orderBy = PostServiceResultsOrderingByTitle; - PostService *postService = [[PostService alloc] initWithManagedObjectContext:self.managedObjectContext]; - [postService syncPostsOfType:PostServiceTypePage - withOptions:options - forBlog:blog - success:^(NSArray *pages) { - [self.managedObjectContext performBlock:^{ - - if (!pages.count) { - success(nil); - return; - } - NSMutableArray *items = [NSMutableArray arrayWithCapacity:pages.count]; - // Create menu items for the top parent pages. - for (Page *page in pages) { - if ([page.parentID integerValue] > 0) { - continue; - } - MenuItem *pageItem = [NSEntityDescription insertNewObjectForEntityForName:[MenuItem entityName] inManagedObjectContext:self.managedObjectContext]; - pageItem.contentID = page.postID; - pageItem.name = page.titleForDisplay; - pageItem.type = MenuItemTypePage; - [items addObject:pageItem]; - } - - [[ContextManager sharedInstance] saveContext:self.managedObjectContext withCompletionBlock:^{ - if (success) { - success(items); - } - } onQueue:dispatch_get_main_queue()]; - }]; - } - failure:failure]; +- (MenuItem *)createItemWithPageID:(NSManagedObjectID *)pageObjectID inContext:(NSManagedObjectContext *)context { + Page *page = [context existingObjectWithID:pageObjectID error:nil]; + if (page == nil) { + return nil; + } + + if ([page.parentID integerValue] > 0) { + return nil; + } + + MenuItem *pageItem = [NSEntityDescription insertNewObjectForEntityForName:[MenuItem entityName] inManagedObjectContext:self.managedObjectContext]; + pageItem.contentID = page.postID; + pageItem.name = page.titleForDisplay; + pageItem.type = MenuItemTypePage; + return pageItem; } #pragma mark - private @@ -435,5 +407,3 @@ - (RemoteMenuItem *)remoteItemFromItem:(MenuItem *)item withItems:(NSOrderedSet< } @end - -NS_ASSUME_NONNULL_END diff --git a/WordPress/Classes/Services/PostCoordinator.swift b/WordPress/Classes/Services/PostCoordinator.swift index a26a3ba81441..fdabb87cd4ff 100644 --- a/WordPress/Classes/Services/PostCoordinator.swift +++ b/WordPress/Classes/Services/PostCoordinator.swift @@ -2,6 +2,10 @@ import Aztec import Foundation import WordPressFlux +protocol PostCoordinatorDelegate: AnyObject { + func postCoordinator(_ postCoordinator: PostCoordinator, promptForPasswordForBlog blog: Blog) +} + class PostCoordinator: NSObject { enum SavingError: Error { @@ -17,8 +21,11 @@ class PostCoordinator: NSObject { coreDataStack.mainContext } + weak var delegate: PostCoordinatorDelegate? + private let queue = DispatchQueue(label: "org.wordpress.postcoordinator") + private var pendingDeletionPostIDs: Set = [] private var observerUUIDs: [AbstractPost: UUID] = [:] private let mediaCoordinator: MediaCoordinator @@ -469,6 +476,93 @@ class PostCoordinator: NSObject { self.actionDispatcherFacade.dispatch(NoticeAction.post(model.notice)) } } + + // MARK: - Trash/Delete + + func isDeleting(_ post: AbstractPost) -> Bool { + pendingDeletionPostIDs.contains(post.objectID) + } + + /// Moves the post to trash or delets it permanently in case it's already in trash. + @MainActor + func delete(_ post: AbstractPost) async { + assert(post.managedObjectContext == mainContext) + + WPAnalytics.track(.postListTrashAction, withProperties: propertiesForAnalytics(for: post)) + + setPendingDeletion(true, post: post) + + let trashed = (post.status == .trash) + + let repository = PostRepository(coreDataStack: ContextManager.shared) + do { + try await repository.trash(TaggedManagedObjectID(post)) + + if trashed { + cancelAnyPendingSaveOf(post: post) + MediaCoordinator.shared.cancelUploadOfAllMedia(for: post) + } + + // Remove the trashed post from spotlight + SearchManager.shared.deleteSearchableItem(post) + + let message: String + switch post { + case _ as Post: + message = trashed ? Strings.deletePost : Strings.movePostToTrash + case _ as Page: + message = trashed ? Strings.deletePage : Strings.movePageToTrash + default: + fatalError("Unsupported item: \(type(of: post))") + } + + let notice = Notice(title: message) + ActionDispatcher.dispatch(NoticeAction.dismiss) + ActionDispatcher.dispatch(NoticeAction.post(notice)) + + // No need to notify as the object gets deleted + setPendingDeletion(false, post: post, notify: false) + } catch { + if let error = error as NSError?, error.code == Constants.httpCodeForbidden { + delegate?.postCoordinator(self, promptForPasswordForBlog: post.blog) + } else { + WPError.showXMLRPCErrorAlert(error) + } + + setPendingDeletion(false, post: post) + } + } + + private func setPendingDeletion(_ isDeleting: Bool, post: AbstractPost, notify: Bool = true) { + if isDeleting { + pendingDeletionPostIDs.insert(post.objectID) + } else { + pendingDeletionPostIDs.remove(post.objectID) + } + if notify { + NotificationCenter.default.post(name: .postCoordinatorDidUpdate, object: self, userInfo: [ + NSUpdatedObjectsKey: Set([post]) + ]) + } + } + + private func propertiesForAnalytics(for post: AbstractPost) -> [String: AnyObject] { + var properties = [String: AnyObject]() + properties["type"] = ((post is Post) ? "post" : "page") as AnyObject + if let dotComID = post.blog.dotComID { + properties[WPAppAnalyticsKeyBlogID] = dotComID + } + return properties + } +} + +private struct Constants { + static let httpCodeForbidden = 403 +} + +extension Foundation.Notification.Name { + /// Contains a set of updated objects under the `NSUpdatedObjectsKey` key + static let postCoordinatorDidUpdate = Foundation.Notification.Name("org.automattic.postCoordinatorDidUpdate") } // MARK: - Automatic Uploads @@ -531,3 +625,10 @@ extension PostCoordinator { } } } + +private enum Strings { + static let movePostToTrash = NSLocalizedString("postsList.movePostToTrash.message", value: "Post moved to trash", comment: "A short message explaining that a post was moved to the trash bin.") + static let deletePost = NSLocalizedString("postsList.deletePost.message", value: "Post deleted permanently", comment: "A short message explaining that a post was deleted permanently.") + static let movePageToTrash = NSLocalizedString("postsList.movePageToTrash.message", value: "Page moved to trash", comment: "A short message explaining that a page was moved to the trash bin.") + static let deletePage = NSLocalizedString("postsList.deletePage.message", value: "Page deleted permanently", comment: "A short message explaining that a page was deleted permanently.") +} diff --git a/WordPress/Classes/Services/PostRepository.swift b/WordPress/Classes/Services/PostRepository.swift index c09d99d12b20..c38c9abdc044 100644 --- a/WordPress/Classes/Services/PostRepository.swift +++ b/WordPress/Classes/Services/PostRepository.swift @@ -81,6 +81,9 @@ final class PostRepository { return } + let status = try await coreDataStack.performQuery { try $0.existingObject(with: postID).status } + assert(status == .trash, "This function can only be used to delete trashed posts/pages.") + // First delete the post from local database. let (remote, remotePost) = try await coreDataStack.performAndSave { [remoteFactory] context in let post = try context.existingObject(with: postID) @@ -228,3 +231,283 @@ final class PostRepository { } } + +// MARK: - Posts/Pages List + +private final class PostRepositoryPostsSerivceRemoteOptions: NSObject, PostServiceRemoteOptions { + struct Options { + var statuses: [String]? + var number: Int = 100 + var offset: Int = 0 + var order: PostServiceResultsOrder = .descending + var orderBy: PostServiceResultsOrdering = .byDate + var authorID: NSNumber? + var search: String? + var meta: String? = "autosave" + var tag: String? + } + + var options: Options + + init(options: Options) { + self.options = options + } + + func statuses() -> [String]? { + options.statuses + } + + func number() -> NSNumber { + NSNumber(value: options.number) + } + + func offset() -> NSNumber { + NSNumber(value: options.offset) + } + + func order() -> PostServiceResultsOrder { + options.order + } + + func orderBy() -> PostServiceResultsOrdering { + options.orderBy + } + + func authorID() -> NSNumber? { + options.authorID + } + + func search() -> String? { + options.search + } + + func meta() -> String? { + options.meta + } + + func tag() -> String! { + options.tag + } +} + +private extension PostServiceRemote { + + func getPosts(ofType type: String, options: PostRepositoryPostsSerivceRemoteOptions) async throws -> [RemotePost] { + try await withCheckedThrowingContinuation { continuation in + self.getPostsOfType(type, options: self.dictionary(with: options), success: { + continuation.resume(returning: $0 ?? []) + }, failure: { + continuation.resume(throwing: $0!) + }) + } + } +} + +extension PostRepository { + + /// Fetch posts or pages from the given site page by page. All fetched posts are saved to the local database. + /// + /// - Parameters: + /// - type: `Post.self` and `Page.self` are the only acceptable types. + /// - statuses: Filter posts or pages with given status. + /// - authorUserID: Filter posts or pages that are authored by given user. + /// - offset: The position of the paginated request. Pass 0 for the first page and count of already fetched results for following pages. + /// - number: Number of posts or pages should be fetched. + /// - blogID: The blog from which to fetch posts or pages + /// - Returns: Object identifiers of the fetched posts. + /// - SeeAlso: https://developer.wordpress.com/docs/api/1.1/get/sites/%24site/posts/ + func paginate( + type: P.Type = P.self, + statuses: [BasePost.Status], + authorUserID: NSNumber? = nil, + offset: Int, + number: Int, + in blogID: TaggedManagedObjectID + ) async throws -> [TaggedManagedObjectID

] { + try await fetch( + type: type, + statuses: statuses, + authorUserID: authorUserID, + range: offset..<(offset + max(number, 0)), + orderBy: .byDate, + descending: true, + // Only delete other local posts if the current call is the first pagination request. + deleteOtherLocalPosts: offset == 0, + in: blogID + ) + } + + /// Search posts or pages in the given site. All fetched posts are saved to the local database. + /// + /// - Parameters: + /// - type: `Post.self` and `Page.self` are the only acceptable types. + /// - input: The text input from user. Or `nil` for searching all posts or pages. + /// - statuses: Filter posts or pages with given status. + /// - tag: Filter posts or pages with given tag. + /// - authorUserID: Filter posts or pages that are authored by given user. + /// - offset: The position of the paginated request. Pass 0 for the first page and count of already fetched results for following pages. + /// - limit: Number of posts or pages should be fetched. + /// - orderBy: The property by which to sort posts or pages. + /// - descending: Whether to sort the results in descending order. + /// - blogID: The blog from which to search posts or pages + /// - Returns: Object identifiers of the search result. + /// - SeeAlso: https://developer.wordpress.com/docs/api/1.1/get/sites/%24site/posts/ + func search( + type: P.Type = P.self, + input: String?, + statuses: [BasePost.Status], + tag: String?, + authorUserID: NSNumber? = nil, + offset: Int, + limit: Int, + orderBy: PostServiceResultsOrdering, + descending: Bool, + in blogID: TaggedManagedObjectID + ) async throws -> [TaggedManagedObjectID

] { + try await fetch( + type: type, + searchInput: input, + statuses: statuses, + tag: tag, + authorUserID: authorUserID, + range: offset..<(offset + max(limit, 0)), + orderBy: orderBy, + descending: descending, + deleteOtherLocalPosts: false, + in: blogID + ) + } + + /// Fetch all pages of the given site. + /// + /// It's higly recommended to cancel the returned task at an appropriate timing. + /// + /// - Warning: As the function name suggests, calling this function makes many API requests to fetch the site's + /// _all pages_. Fetching all pages may be handy in some use cases, but also can be wasteful when user aborts + /// in the middle of fetching all pages, if the fetching is not cancelled. + /// + /// - Parameters: + /// - statuses: Only fetch pages whose status is included in the given statues. + /// - blogID: Object ID of the site. + /// - Returns: A `Task` instance representing the fetching. The fetch pages API requests will stop if the task is cancelled. + func fetchAllPages(statuses: [BasePost.Status], authorUserID: NSNumber? = nil, in blogID: TaggedManagedObjectID) -> Task<[TaggedManagedObjectID], Swift.Error> { + Task { + let pageSize = 100 + var allPages = [TaggedManagedObjectID]() + while true { + try Task.checkCancellation() + + let pageRange = allPages.count..<(allPages.count + pageSize) + let current = try await fetch( + type: Page.self, + statuses: statuses, + authorUserID: authorUserID, + range: pageRange, + deleteOtherLocalPosts: false, + in: blogID + ) + allPages.append(contentsOf: current) + + if current.isEmpty || current.count < pageSize { + break + } + } + + // Once all pages are fetched and saved, we need to purge local database + // to ensure when a database query with the same conditions that are passed + // to this function returns the same result as the `allPages` value. + // Of course, we can't delete locally modified pages if there are any. + try await coreDataStack.performAndSave { context in + let request = Page.fetchRequest() + // Delete posts that match _all of the following conditions_: + request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ + // belongs to the given blog + NSPredicate(format: "blog = %@", blogID.objectID), + // was fetched from the site + NSPredicate(format: "postID != NULL AND postID > 0"), + // doesn't have local edits + NSPredicate(format: "original = NULL AND revision = NULL"), + // doesn't have local status changes + NSPredicate(format: "remoteStatusNumber = %@", NSNumber(value: AbstractPostRemoteStatus.sync.rawValue)), + // is not included in the fetched page lists (i.e. it has been deleted from the site) + NSPredicate(format: "NOT (SELF IN %@)", allPages.map { $0.objectID }), + // we only need to deal with pages that match the filters passed to this function. + statuses.isEmpty ? nil : NSPredicate(format: "status IN %@", statuses), + ].compactMap { $0 }) + + try context.execute(NSBatchDeleteRequest(fetchRequest: request)) + } + + return allPages + } + } + + private func fetch( + type: P.Type, + searchInput: String? = nil, + statuses: [BasePost.Status]?, + tag: String? = nil, + authorUserID: NSNumber?, + range: Range, + orderBy: PostServiceResultsOrdering = .byDate, + descending: Bool = true, + deleteOtherLocalPosts: Bool, + in blogID: TaggedManagedObjectID + ) async throws -> [TaggedManagedObjectID

] { + assert(type == Post.self || type == Page.self, "Only support fetching Post or Page") + assert(range.lowerBound >= 0) + + let postType: String + if type == Post.self { + postType = "post" + } else if type == Page.self { + postType = "page" + } else { + // There is an assertion above to ensure the app doesn't fall into this case. + return [] + } + + let remote = try await coreDataStack.performQuery { [remoteFactory] context in + let blog = try context.existingObject(with: blogID) + return remoteFactory.forBlog(blog) + } + guard let remote else { + throw PostRepository.Error.remoteAPIUnavailable + } + + let options = PostRepositoryPostsSerivceRemoteOptions(options: .init( + statuses: statuses?.strings, + number: range.count, + offset: range.lowerBound, + order: descending ? .descending : .ascending, + orderBy: orderBy, + authorID: authorUserID, + search: searchInput, + tag: tag + )) + let remotePosts = try await remote.getPosts(ofType: postType, options: options) + + let updatedPosts = try await coreDataStack.performAndSave { context in + let updatedPosts = PostHelper.merge( + remotePosts, + ofType: postType, + withStatuses: statuses?.strings, + byAuthor: authorUserID, + for: try context.existingObject(with: blogID), + purgeExisting: deleteOtherLocalPosts, + in: context + ) + return updatedPosts.compactMap { aPost -> TaggedManagedObjectID

? in + guard let post = aPost as? P else { + // FIXME: This issue is tracked in https://github.com/wordpress-mobile/WordPress-iOS/issues/22255 + DDLogWarn("Expecting a \(postType) as \(type), but got \(aPost)") + return nil + } + return TaggedManagedObjectID(post) + } + } + + return updatedPosts + } + +} diff --git a/WordPress/Classes/Services/PostService+Revisions.swift b/WordPress/Classes/Services/PostService+Revisions.swift index 0b58259fa6c3..f5280746d0e4 100644 --- a/WordPress/Classes/Services/PostService+Revisions.swift +++ b/WordPress/Classes/Services/PostService+Revisions.swift @@ -29,6 +29,26 @@ extension PostService { }, failure: failure) } + /// Check if the given post matches the latest revision on the site and logs an error event if it doesn't. + @objc(checkLatestRevisionForPost:usingRemote:) + func checkLatestRevision(for post: AbstractPost, using remote: PostServiceRemote) -> Void { + guard let postID = post.postID, + postID.int64Value > 0, + let restApi = (remote as? PostServiceRemoteREST), + let localRevision = post.revisions?.first as? NSNumber + else { + return + } + + restApi.getPostLatestRevisionID(for: postID) { latestRemoteRevision in + if let latestRemoteRevision, latestRemoteRevision != localRevision { + DDLogError("The latest revision (\(latestRemoteRevision)) of the post (id: \(postID)) is about to be overwritten by an edit that's based on revision \(localRevision)") + WordPressAppDelegate.logError(NSError(domain: "PostEditor.OverwritePost", code: 1)) + } + } failure: { _ in + // Do nothing + } + } // MARK: Private methods diff --git a/WordPress/Classes/Services/PostService.h b/WordPress/Classes/Services/PostService.h index 0082dcf00f4b..7fff3d5d7c6b 100644 --- a/WordPress/Classes/Services/PostService.h +++ b/WordPress/Classes/Services/PostService.h @@ -125,41 +125,6 @@ forceDraftIfCreating:(BOOL)forceDraftIfCreating success:(nullable void (^)(AbstractPost *post, NSString *previewURL))success failure:(void (^)(NSError * _Nullable error))failure; -/** - Attempts to delete the specified post outright vs moving it to the - trash folder. - - @param post The post or page to delete - @param success A success block - @param failure A failure block - */ -- (void)deletePost:(AbstractPost *)post - success:(nullable void (^)(void))success - failure:(void (^)(NSError * _Nullable error))failure; - -/** - Moves the specified post into the trash bin. Does not delete - the post unless it was deleted on the server. - - @param post The post or page to trash - @param success A success block - @param failure A failure block - */ -- (void)trashPost:(AbstractPost *)post - success:(nullable nullable void (^)(void))success - failure:(void (^)(NSError * _Nullable error))failure; - -/** - Moves the specified post out of the trash bin. - - @param post The post or page to restore - @param success A success block - @param failure A failure block - */ -- (void)restorePost:(AbstractPost *)post - success:(nullable void (^)(void))success - failure:(void (^)(NSError * _Nullable error))failure; - @end NS_ASSUME_NONNULL_END diff --git a/WordPress/Classes/Services/PostService.m b/WordPress/Classes/Services/PostService.m index e9fb06a08bba..7449ed4d6c81 100644 --- a/WordPress/Classes/Services/PostService.m +++ b/WordPress/Classes/Services/PostService.m @@ -197,6 +197,30 @@ - (void)uploadPost:(AbstractPost *)post failure:(nullable void (^)(NSError * _Nullable error))failure { id remote = [self.postServiceRemoteFactory forBlog:post.blog]; + + // It's possible that the post has been updated while the user is making changes to the post in the app. + // In that case, this function here will overwrite the latest revision on the server. + // + // See also: https://github.com/wordpress-mobile/WordPress-iOS/issues/8111 + // + // Here we check (only for WP.com posts for now) and log events for such scenario to get a sense of how + // often it occurs. + // + // The updating post API is made in parallel with this get revision API request–we don't want to delay + // saving post. In theory, it's possible that the updating post API finishes before the server handles + // the get revision API request, which means we'll receive an incorrect revision. But that should be + // extremely rare and we'll ignore it for now. + [self checkLatestRevisionForPost:post usingRemote:remote]; + + [self uploadPost:post forceDraftIfCreating:forceDraftIfCreating usingRemote:remote success:success failure:failure]; +} + +- (void)uploadPost:(AbstractPost *)post +forceDraftIfCreating:(BOOL)forceDraftIfCreating + usingRemote:(id)remote + success:(nullable void (^)(AbstractPost * _Nullable post))success + failure:(nullable void (^)(NSError * _Nullable error))failure +{ RemotePost *remotePost = [PostHelper remotePostWithPost:post]; post.remoteStatus = AbstractPostRemoteStatusPushing; @@ -449,195 +473,6 @@ - (void)handleAutoSaveWithRestRemote:(PostServiceRemoteREST *)restRemote } -#pragma mark - Delete, Trashing, Restoring - -- (void)deletePost:(AbstractPost *)post - success:(void (^)(void))success - failure:(void (^)(NSError *error))failure -{ - void (^privateBlock)(void) = ^void() { - NSNumber *postID = post.postID; - if ([postID longLongValue] > 0) { - RemotePost *remotePost = [PostHelper remotePostWithPost:post]; - id remote = [self.postServiceRemoteFactory forBlog:post.blog]; - [remote deletePost:remotePost success:success failure:failure]; - } - [self.managedObjectContext deleteObject:post]; - [[ContextManager sharedInstance] saveContext:self.managedObjectContext]; - }; - - if ([post isRevision]) { - [self deletePost:post.original success:privateBlock failure:failure]; - } else { - privateBlock(); - } -} - -- (void)trashPost:(AbstractPost *)post - success:(void (^)(void))success - failure:(void (^)(NSError *error))failure -{ - if ([post.status isEqualToString:PostStatusTrash]) { - [self deletePost:post success:success failure:failure]; - return; - } - - void(^privateBodyBlock)(void) = ^void() { - post.restorableStatus = post.status; - - NSNumber *postID = post.postID; - - if ([post isRevision] || [postID longLongValue] <= 0) { - post.status = PostStatusTrash; - - if (success) { - success(); - } - - return; - } - - [self trashRemotePostWithPost:post - success:success - failure:failure]; - }; - - if ([post isRevision]) { - [self trashPost:post.original - success:privateBodyBlock - failure:failure]; - } else { - privateBodyBlock(); - } -} - -- (void)trashRemotePostWithPost:(AbstractPost*)post - success:(void (^)(void))success - failure:(void (^)(NSError *error))failure -{ - NSManagedObjectID *postObjectID = post.objectID; - - void (^successBlock)(RemotePost *post) = ^(RemotePost *remotePost) { - NSError *err; - Post *postInContext = (Post *)[self.managedObjectContext existingObjectWithID:postObjectID error:&err]; - if (err) { - DDLogError(@"%@", err); - } - if (postInContext) { - if (!remotePost || [remotePost.status isEqualToString:PostStatusDeleted]) { - [self.managedObjectContext deleteObject:post]; - } else { - [PostHelper updatePost:postInContext withRemotePost:remotePost inContext:self.managedObjectContext]; - postInContext.latest.statusAfterSync = postInContext.statusAfterSync; - postInContext.latest.status = postInContext.status; - } - [[ContextManager sharedInstance] saveContext:self.managedObjectContext]; - } - if (success) { - success(); - } - }; - - void (^failureBlock)(NSError *error) = ^(NSError *error) { - NSError *err; - Post *postInContext = (Post *)[self.managedObjectContext existingObjectWithID:postObjectID error:&err]; - if (err) { - DDLogError(@"%@", err); - } - if (postInContext) { - postInContext.restorableStatus = nil; - } - if (failure){ - failure(error); - } - }; - - RemotePost *remotePost = [PostHelper remotePostWithPost:post]; - id remote = [self.postServiceRemoteFactory forBlog:post.blog]; - [remote trashPost:remotePost success:successBlock failure:failureBlock]; -} - -- (void)restorePost:(AbstractPost *)post - success:(void (^)(void))success - failure:(void (^)(NSError *error))failure -{ - void (^privateBodyBlock)(void) = ^void() { - post.status = post.restorableStatus; - [[ContextManager sharedInstance] saveContext:self.managedObjectContext]; - - if (![post isRevision] && [post.postID longLongValue] > 0) { - [self restoreRemotePostWithPost:post success:success failure:failure]; - } else { - if (success) { - success(); - } - } - }; - - if (post.isRevision) { - [self restorePost:post.original - success:privateBodyBlock - failure:failure]; - - return; - } else { - privateBodyBlock(); - } -} - -- (void)restoreRemotePostWithPost:(AbstractPost*)post - success:(void (^)(void))success - failure:(void (^)(NSError *error))failure -{ - NSManagedObjectID *postObjectID = post.objectID; - - void (^successBlock)(RemotePost *post) = ^(RemotePost *remotePost) { - NSError *err; - Post *postInContext = (Post *)[self.managedObjectContext existingObjectWithID:postObjectID error:&err]; - postInContext.restorableStatus = nil; - if (err) { - DDLogError(@"%@", err); - } - if (postInContext) { - [PostHelper updatePost:postInContext withRemotePost:remotePost inContext:self.managedObjectContext]; - [[ContextManager sharedInstance] saveContext:self.managedObjectContext]; - } - if (success) { - success(); - } - }; - - void (^failureBlock)(NSError *error) = ^(NSError *error) { - NSError *err; - Post *postInContext = (Post *)[self.managedObjectContext existingObjectWithID:postObjectID error:&err]; - if (err) { - DDLogError(@"%@", err); - } - if (postInContext) { - // Put the post back in the trash bin. - postInContext.status = PostStatusTrash; - [[ContextManager sharedInstance] saveContext:self.managedObjectContext]; - } - if (failure){ - failure(error); - } - }; - - RemotePost *remotePost = [PostHelper remotePostWithPost:post]; - if (post.restorableStatus) { - remotePost.status = post.restorableStatus; - } else { - // Assign a status of draft to the remote post. The WordPress.com REST API will - // ignore this and should restore the post's previous status. The XML-RPC API - // needs a status assigned to move a post out of the trash folder. Draft is the - // safest option when we don't know what the status was previously. - remotePost.status = PostStatusDraft; - } - - id remote = [self.postServiceRemoteFactory forBlog:post.blog]; - [remote restorePost:remotePost success:successBlock failure:failureBlock]; -} - #pragma mark - Helpers - (NSDictionary *)remoteSyncParametersDictionaryForRemote:(nonnull id )remote diff --git a/WordPress/Classes/Services/PostServiceOptions.h b/WordPress/Classes/Services/PostServiceOptions.h index 76e4f3e03c0d..6871c9abf3d9 100644 --- a/WordPress/Classes/Services/PostServiceOptions.h +++ b/WordPress/Classes/Services/PostServiceOptions.h @@ -25,5 +25,6 @@ @property (nonatomic, strong) NSNumber *authorID; @property (nonatomic, copy) NSString *search; @property (nonatomic, copy) NSString *meta; +@property (nonatomic, copy) NSString *tag; @end diff --git a/WordPress/Classes/Services/Reader Post/ReaderPostService.m b/WordPress/Classes/Services/Reader Post/ReaderPostService.m index 20a1d48ef7ed..ee7a987591fe 100644 --- a/WordPress/Classes/Services/Reader Post/ReaderPostService.m +++ b/WordPress/Classes/Services/Reader Post/ReaderPostService.m @@ -13,7 +13,7 @@ @import WordPressKit; @import WordPressShared; -NSUInteger const ReaderPostServiceNumberToSync = 40; +NSUInteger const ReaderPostServiceNumberToSync = 7; // NOTE: The search endpoint is currently capped to max results of 20 and returns // a 500 error if more are requested. // For performance reasons, request fewer results. EJ 2016-05-13 diff --git a/WordPress/Classes/Services/SiteAddressService.swift b/WordPress/Classes/Services/SiteAddressService.swift index de5f6ad8b004..26e6fbd408e1 100644 --- a/WordPress/Classes/Services/SiteAddressService.swift +++ b/WordPress/Classes/Services/SiteAddressService.swift @@ -17,18 +17,13 @@ struct SiteAddressServiceResult { typealias SiteAddressServiceCompletion = (Result) -> Void protocol SiteAddressService { - func addresses(for query: String, segmentID: Int64, completion: @escaping SiteAddressServiceCompletion) - func addresses(for query: String, completion: @escaping SiteAddressServiceCompletion) + func addresses(for query: String, type: DomainsServiceRemote.DomainSuggestionType, completion: @escaping SiteAddressServiceCompletion) } // MARK: - MockSiteAddressService final class MockSiteAddressService: SiteAddressService { - func addresses(for query: String, segmentID: Int64, completion: @escaping SiteAddressServiceCompletion) { - completion(.success(SiteAddressServiceResult(hasExactMatch: true, domainSuggestions: mockAddresses))) - } - - func addresses(for query: String, completion: @escaping SiteAddressServiceCompletion) { + func addresses(for query: String, type: DomainsServiceRemote.DomainSuggestionType, completion: @escaping SiteAddressServiceCompletion) { completion(.success(SiteAddressServiceResult(hasExactMatch: true, domainSuggestions: mockAddresses))) } @@ -51,11 +46,6 @@ final class DomainsServiceAdapter: SiteAddressService { // MARK: Properties - /// Checks if the Domain Purchasing Feature Flag and AB Experiment are enabled - private var domainPurchasingEnabled: Bool { - RemoteFeatureFlag.plansInSiteCreation.enabled() - } - /** Corresponds to: @@ -98,38 +88,18 @@ final class DomainsServiceAdapter: SiteAddressService { // MARK: SiteAddressService - func addresses(for query: String, segmentID: Int64, completion: @escaping SiteAddressServiceCompletion) { - - domainsService.getDomainSuggestions(query: query, - segmentID: segmentID, - quantity: domainRequestQuantity, - success: { domainSuggestions in - completion(Result.success(self.sortSuggestions(for: query, suggestions: domainSuggestions))) - }, - failure: { error in - if (error as NSError).code == DomainsServiceAdapter.emptyResultsErrorCode { - completion(Result.success(SiteAddressServiceResult())) - return - } - - completion(Result.failure(error)) - }) - } - - func addresses(for query: String, completion: @escaping SiteAddressServiceCompletion) { - let domainSuggestionType: DomainsServiceRemote.DomainSuggestionType = domainPurchasingEnabled - ? .freeAndPaid - : .wordPressDotComAndDotBlogSubdomains + func addresses(for query: String, type: DomainsServiceRemote.DomainSuggestionType, completion: @escaping SiteAddressServiceCompletion) { domainsService.getDomainSuggestions(query: query, quantity: domainRequestQuantity, - domainSuggestionType: domainSuggestionType, + domainSuggestionType: type, success: { domainSuggestions in - if self.domainPurchasingEnabled { + switch type { + case .freeAndPaid: let hasExactMatch = domainSuggestions.contains { domain -> Bool in return domain.domainNameStrippingSubdomain.caseInsensitiveCompare(query) == .orderedSame } completion(Result.success(.init(hasExactMatch: hasExactMatch, domainSuggestions: domainSuggestions))) - } else { + default: completion(Result.success(self.sortSuggestions(for: query, suggestions: domainSuggestions))) } }, diff --git a/WordPress/Classes/Services/SiteVerticalsService.swift b/WordPress/Classes/Services/SiteVerticalsService.swift deleted file mode 100644 index 5e064b3a119f..000000000000 --- a/WordPress/Classes/Services/SiteVerticalsService.swift +++ /dev/null @@ -1,68 +0,0 @@ -import AutomatticTracks - -// MARK: - SiteVerticalsService - -/// Advises the caller of results related to requests for a specific site vertical. -/// -/// - success: the site vertical request succeeded with the accompanying result. -/// - failure: the site vertical request failed due to the accompanying error. -/// -public enum SiteVerticalRequestResult { - case success(SiteVertical) - case failure(SiteVerticalsError) -} - -typealias SiteVerticalRequestCompletion = (SiteVerticalRequestResult) -> () - -/// Abstracts retrieval of site verticals. -/// -protocol SiteVerticalsService { - func retrieveVertical(named verticalName: String, completion: @escaping SiteVerticalRequestCompletion) - func retrieveVerticals(request: SiteVerticalsRequest, completion: @escaping SiteVerticalsServiceCompletion) -} - -// MARK: - SiteCreationVerticalsService - -/// Retrieves candidate Site Verticals used to create a new site. -/// -final class SiteCreationVerticalsService: SiteVerticalsService { - - // MARK: Properties - - /// A facade for WPCOM services. - private let remoteService: WordPressComServiceRemote - - init(coreDataStack: CoreDataStack) { - let api = coreDataStack.performQuery({ context in - try? WPAccount.lookupDefaultWordPressComAccount(in: context)?.wordPressComRestV2Api - }) ?? WordPressComRestApi.anonymousApi(userAgent: WPUserAgent.wordPress(), localeKey: WordPressComRestApi.LocaleKeyV2) - self.remoteService = WordPressComServiceRemote(wordPressComRestApi: api) - } - - // MARK: SiteVerticalsService - - func retrieveVertical(named verticalName: String, completion: @escaping SiteVerticalRequestCompletion) { - let request = SiteVerticalsRequest(search: verticalName, limit: 1) - - remoteService.retrieveVerticals(request: request) { result in - switch result { - case .success(let verticals): - guard let vertical = verticals.first else { - WordPressAppDelegate.crashLogging?.logMessage("The verticals service should always return at least 1 match for the precise term queried.", level: .error) - completion(.failure(.serviceFailure)) - return - } - - completion(.success(vertical)) - case .failure(let error): - completion(.failure(error)) - } - } - } - - func retrieveVerticals(request: SiteVerticalsRequest, completion: @escaping SiteVerticalsServiceCompletion) { - remoteService.retrieveVerticals(request: request) { result in - completion(result) - } - } -} diff --git a/WordPress/Classes/Services/Stories/DisabledVideoOverlay.swift b/WordPress/Classes/Services/Stories/DisabledVideoOverlay.swift deleted file mode 100644 index 2bcdd2cba8e8..000000000000 --- a/WordPress/Classes/Services/Stories/DisabledVideoOverlay.swift +++ /dev/null @@ -1,16 +0,0 @@ -import UIKit - -/// An overlay for videos that exceed allowed duration -class DisabledVideoOverlay: UIView { - - static let overlayTransparency: CGFloat = 0.8 - - init() { - super.init(frame: .zero) - backgroundColor = .gray.withAlphaComponent(Self.overlayTransparency) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} diff --git a/WordPress/Classes/Services/Stories/StoryEditor.swift b/WordPress/Classes/Services/Stories/StoryEditor.swift index 337741d36369..3ecca9002f11 100644 --- a/WordPress/Classes/Services/Stories/StoryEditor.swift +++ b/WordPress/Classes/Services/Stories/StoryEditor.swift @@ -8,15 +8,6 @@ class StoryEditor: CameraController { private static let directoryName = "Stories" - /// A directory to temporarily hold imported media. - /// - Throws: Any errors resulting from URL or directory creation. - /// - Returns: A URL with the media cache directory. - static func mediaCacheDirectory() throws -> URL { - let storiesURL = try MediaFileManager.cache.directoryURL().appendingPathComponent(directoryName, isDirectory: true) - try FileManager.default.createDirectory(at: storiesURL, withIntermediateDirectories: true, attributes: nil) - return storiesURL - } - /// A directory to temporarily hold saved archives. /// - Throws: Any errors resulting from URL or directory creation. /// - Returns: A URL with the save directory. diff --git a/WordPress/Classes/Services/Stories/WPMediaPicker+MediaPicker.swift b/WordPress/Classes/Services/Stories/WPMediaPicker+MediaPicker.swift index 1eb162b8f276..96b5278b35bc 100644 --- a/WordPress/Classes/Services/Stories/WPMediaPicker+MediaPicker.swift +++ b/WordPress/Classes/Services/Stories/WPMediaPicker+MediaPicker.swift @@ -1,354 +1,201 @@ -import WPMediaPicker import Kanvas import Gridicons -import Combine +import Photos +import PhotosUI -class PortraitTabBarController: UITabBarController { - override var supportedInterfaceOrientations: UIInterfaceOrientationMask { - return .portrait - } - - override var shouldAutorotate: Bool { - return false - } -} - -class WPMediaPickerForKanvas: WPNavigationMediaPickerViewController, MediaPicker { - - private struct Constants { - static let photosTabBarTitle: String = NSLocalizedString("Photos", comment: "Tab bar title for the Photos tab in Media Picker") - static let photosTabBarIcon: UIImage? = .gridicon(.imageMultiple) - static let mediaPickerTabBarTitle: String = NSLocalizedString("Media", comment: "Tab bar title for the Media tab in Media Picker") - static let mediaPickerTabBarIcon: UIImage? = UIImage(named: "icon-wp")?.af_imageAspectScaled(toFit: CGSize(width: 30, height: 30)) - } - - static var pickerDataSource: MediaLibraryPickerDataSource? - - private let delegateHandler: MediaPickerDelegate - - init(options: WPMediaPickerOptions, delegate: MediaPickerDelegate) { - self.delegateHandler = delegate - super.init(options: options) - self.delegate = delegate - self.mediaPicker.mediaPickerDelegate = delegate - self.mediaPicker.registerClass(forReusableCellOverlayViews: DisabledVideoOverlay.self) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - public static func present(on: UIViewController, +final class WPMediaPickerForKanvas: MediaPicker { + public static func present(on presentingViewController: UIViewController, with settings: CameraSettings, delegate: KanvasMediaPickerViewControllerDelegate, completion: @escaping () -> Void) { - guard let blog = (on as? StoryEditor)?.post.blog else { + guard let blog = (presentingViewController as? StoryEditor)?.post.blog else { DDLogWarn("No blog for Kanvas Media Picker") return } - let tabBar = PortraitTabBarController() - - let mediaPickerDelegate = MediaPickerDelegate(kanvasDelegate: delegate, - presenter: tabBar, - blog: blog) - let options = WPMediaPickerOptions() - options.allowCaptureOfMedia = false + let pickerDelegate = MediaPickerDelegate(kanvasDelegate: delegate, blog: blog) - let photoPicker = WPMediaPickerForKanvas(options: options, delegate: mediaPickerDelegate) - photoPicker.dataSource = WPPHAssetDataSource.sharedInstance() - photoPicker.tabBarItem = UITabBarItem(title: Constants.photosTabBarTitle, image: Constants.photosTabBarIcon, tag: 0) - photoPicker.mediaPicker.registerClass(forCustomHeaderView: DeviceMediaPermissionsHeader.self) + let photosPicker = PHPickerViewController(configuration: { + var configuration = PHPickerConfiguration() + configuration.preferredAssetRepresentationMode = .current + configuration.selection = .ordered + configuration.selectionLimit = 0 + return configuration + }()) + photosPicker.delegate = pickerDelegate + presentingViewController.present(photosPicker, animated: true, completion: completion) - let mediaPicker = WPMediaPickerForKanvas(options: options, delegate: mediaPickerDelegate) - mediaPicker.startOnGroupSelector = false - mediaPicker.showGroupSelector = false - - pickerDataSource = MediaLibraryPickerDataSource(blog: blog) - mediaPicker.dataSource = pickerDataSource - mediaPicker.tabBarItem = UITabBarItem(title: Constants.mediaPickerTabBarTitle, image: Constants.mediaPickerTabBarIcon, tag: 0) - - tabBar.viewControllers = [ - photoPicker, - mediaPicker - ] - on.present(tabBar, animated: true, completion: completion) + objc_setAssociatedObject(presentingViewController, &WPMediaPickerForKanvas.delegateAssociatedKey, pickerDelegate, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } -} -class MediaPickerDelegate: NSObject, WPMediaPickerViewControllerDelegate { + private static var delegateAssociatedKey: UInt8 = 0 +} +final class MediaPickerDelegate: PHPickerViewControllerDelegate { private weak var kanvasDelegate: KanvasMediaPickerViewControllerDelegate? - private weak var presenter: UIViewController? private let blog: Blog - private var cancellables = Set() init(kanvasDelegate: KanvasMediaPickerViewControllerDelegate, - presenter: UIViewController, blog: Blog) { self.kanvasDelegate = kanvasDelegate - self.presenter = presenter self.blog = blog } - func mediaPickerControllerDidCancel(_ picker: WPMediaPickerViewController) { - presenter?.dismiss(animated: true, completion: nil) - } + // MARK: - PHPickerViewControllerDelegate - enum ExportErrors: Error { - case missingImage - case missingVideoURL - case failedVideoDownload - case unexpectedAssetType // `WPMediaAsset.assetType()` was not an image or video + func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { + guard !results.isEmpty else { + picker.presentingViewController?.dismiss(animated: true) + return + } + Task { + await process(results, picker: picker) + } } - private struct ExportOutput { - let index: Int - let media: PickedMedia - } + @MainActor + private func process(_ results: [PHPickerResult], picker: PHPickerViewController) async { + startLoading(in: picker) + defer { stopLoading() } - func mediaPickerController(_ picker: WPMediaPickerViewController, didFinishPicking assets: [WPMediaAsset]) { + do { + let selection = try await exportPickedMedia(from: results, blog: blog) + picker.presentingViewController?.dismiss(animated: true) + kanvasDelegate?.didPick(media: selection) + } catch { + if let error = error as? AssetExportError, + case .videoLengthLimitExceeded = error { + presentVideoLimitExceededFromPicker(on: picker) + } else { + showError(error, in: picker) + } + } + } - let selected = picker.selectedAssets - picker.clearSelectedAssets(false) - picker.reloadInputViews() // Reloads the bottom bar so it is hidden while loading + // MARK: - Helpers + private func startLoading(in viewController: UIViewController) { SVProgressHUD.setDefaultMaskType(.black) - SVProgressHUD.setContainerView(presenter?.view) + SVProgressHUD.setContainerView(viewController.view) SVProgressHUD.showProgress(-1) - - let mediaExports: [AnyPublisher<(Int, PickedMedia), Error>] = assets.enumerated().map { (index, asset) -> AnyPublisher<(Int, PickedMedia), Error> in - switch asset.assetType() { - case .image: - return asset.imagePublisher().map { (image, url) in - (index, PickedMedia.image(image, url)) - }.eraseToAnyPublisher() - case .video: - return asset.videoURLPublisher().map { url in - (index, PickedMedia.video(url)) - }.eraseToAnyPublisher() - default: - return Fail(outputType: (Int, PickedMedia).self, failure: ExportErrors.unexpectedAssetType).eraseToAnyPublisher() - } - } - - Publishers.MergeMany(mediaExports) - .collect(assets.count) // Wait for all assets to complete before receiving. - .map { media in - // Sort our media back into the original order since they may be mixed up after export. - return media.sorted { left, right in - return left.0 < right.0 - }.map { - return $0.1 - } - } - .receive(on: DispatchQueue.main).sink(receiveCompletion: { completion in - switch completion { - case .failure(let error): - picker.selectedAssets = selected - - let title = NSLocalizedString("Failed Media Export", comment: "Error title when picked media cannot be imported into stories.") - let message = NSLocalizedString("Your media could not be exported. If the problem persists you can contact us via the Me > Help & Support screen.", comment: "Error message when picked media cannot be imported into stories.") - let dismissTitle = NSLocalizedString( - "mediaPicker.failedMediaExportAlert.dismissButton", - value: "Dismiss", - comment: "The title of the button to dismiss the alert shown when the picked media cannot be imported into stories." - ) - let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) - let dismiss = UIAlertAction(title: dismissTitle, style: .default) { _ in - alert.dismiss(animated: true, completion: nil) - } - alert.addAction(dismiss) - picker.present(alert, animated: true, completion: nil) - - DDLogError("Failed to export picked Stories media: \(error)") - case .finished: - break - } - SVProgressHUD.dismiss() - }, receiveValue: { [weak self] media in - self?.presenter?.dismiss(animated: true, completion: nil) - self?.kanvasDelegate?.didPick(media: media) - }).store(in: &cancellables) } - func mediaPickerController(_ picker: WPMediaPickerViewController, shouldShowOverlayViewForCellFor asset: WPMediaAsset) -> Bool { - picker != self && !blog.canUploadAsset(asset) + private func stopLoading() { + SVProgressHUD.dismiss() } - func mediaPickerControllerShouldShowCustomHeaderView(_ picker: WPMediaPickerViewController) -> Bool { - guard picker.dataSource is WPPHAssetDataSource else { - return false + private func showError(_ error: Error, in viewController: UIViewController) { + let title = NSLocalizedString("mediaPicker.failedMediaExportAlert.title", value: "Failed Media Export", comment: "Error title when picked media cannot be imported into stories.") + let message = NSLocalizedString("mediaPicker.failedMediaExportAlert.message", value: "Your media could not be exported. If the problem persists you can contact us via the Me > Help & Support screen.", comment: "Error message when picked media cannot be imported into stories.") + let dismissTitle = NSLocalizedString( + "mediaPicker.failedMediaExportAlert.dismissButton", + value: "Dismiss", + comment: "The title of the button to dismiss the alert shown when the picked media cannot be imported into stories." + ) + let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) + let dismiss = UIAlertAction(title: dismissTitle, style: .default) { _ in + alert.dismiss(animated: true, completion: nil) } + alert.addAction(dismiss) + viewController.present(alert, animated: true, completion: nil) - return PHPhotoLibrary.authorizationStatus(for: .readWrite) == .limited + DDLogError("Failed to export picked Stories media: \(error)") } +} - func mediaPickerControllerReferenceSize(forCustomHeaderView picker: WPMediaPickerViewController) -> CGSize { - let header = DeviceMediaPermissionsHeader() - header.translatesAutoresizingMaskIntoConstraints = false - - return header.referenceSizeInView(picker.view) - } +// MARK: - Helpers - func mediaPickerController(_ picker: WPMediaPickerViewController, configureCustomHeaderView headerView: UICollectionReusableView) { - guard let headerView = headerView as? DeviceMediaPermissionsHeader else { - return +@MainActor +private func exportPickedMedia(from results: [PHPickerResult], blog: Blog) async throws -> [PickedMedia] { + try await withThrowingTaskGroup(of: PickedMedia.self) { group in + for result in results { + group.addTask { @MainActor in + try await exportPickedMedia(from: result.itemProvider, blog: blog) + } } - - headerView.presenter = picker + var selection: [PickedMedia] = [] + for try await media in group { + selection.append(media) + } + return selection } +} - func mediaPickerController(_ picker: WPMediaPickerViewController, shouldSelect asset: WPMediaAsset) -> Bool { - if picker != self, !blog.canUploadAsset(asset) { - presentVideoLimitExceededFromPicker(on: picker) - return false +// Isolating it on the @MainActor because NSItemProvider is non-Sendable. +@MainActor +private func exportPickedMedia(from provider: NSItemProvider, blog: Blog) async throws -> PickedMedia { + if provider.hasConformingType(.image) { + let image = try await NSItemProvider.image(for: provider) + let imageSize = image.size.scaled(by: image.scale) + let targetSize = getTargetSize(forImageSize: imageSize, targetSize: CGSize(width: 2048, height: 2048)) + let resized = await Task.detached { + image.resizedImage(targetSize, interpolationQuality: .default) + }.value + return PickedMedia.image(resized ?? image, nil) + } else if provider.hasConformingType(.movie) || provider.hasConformingType(.video) { + let videoURL = try await NSItemProvider.video(for: provider) + guard blog.canUploadVideo(from: videoURL) else { + throw AssetExportError.videoLengthLimitExceeded + } + let asset = AVAsset(url: videoURL) + // important: Kanvas doesn't support video orientation! + guard asset.tracks(withMediaType: .video).first?.preferredTransform != .identity else { + return PickedMedia.video(videoURL) } - return true + defer { try? FileManager.default.removeItem(at: videoURL) } + let exportURL = try await asset.exportFixingOrientation(to: videoURL + .deletingLastPathComponent() + .appendingPathComponent(UUID().uuidString)) + return PickedMedia.video(exportURL) + } else { + throw AssetExportError.unexpectedAssetType } } +/// - parameter imageSize: Image size in pixels. +private func getTargetSize(forImageSize imageSize: CGSize, targetSize originalTargetSize: CGSize) -> CGSize { + guard imageSize.width > 0 && imageSize.height > 0 else { + return originalTargetSize + } + // Scale image to fit the target size but avoid upscaling + let scale = min(1, min( + originalTargetSize.width / imageSize.width, + originalTargetSize.height / imageSize.height + )) + return imageSize.scaled(by: scale).rounded() +} + // MARK: - User messages for video limits allowances extension MediaPickerDelegate: VideoLimitsAlertPresenter {} // MARK: Media Export extensions -enum VideoURLErrors: Error { - case videoAssetExportFailed - case failedVideoDownload -} - -private extension PHAsset { - // TODO: Update MPMediaPicker with degraded image implementation. - func sizedImage(with size: CGSize, completionHandler: @escaping (UIImage?, Error?) -> Void) { - let options = PHImageRequestOptions() - options.isSynchronous = false - options.deliveryMode = .opportunistic - options.resizeMode = .fast - options.isNetworkAccessAllowed = true - PHImageManager.default().requestImage(for: self, targetSize: size, contentMode: .aspectFit, options: options, resultHandler: { (result, info) in - let error = info?[PHImageErrorKey] as? Error - let cancelled = info?[PHImageCancelledKey] as? Bool - if let error = error, cancelled != true { - completionHandler(nil, error) - } - // Wait for resized image instead of thumbnail - if let degraded = info?[PHImageResultIsDegradedKey] as? Bool, degraded == false { - completionHandler(result, nil) - } - }) - } -} - -extension WPMediaAsset { - - private func fit(size: CGSize) -> CGSize { - let assetSize = pixelSize() - let aspect = assetSize.width / assetSize.height - if size.width / aspect <= size.height { - return CGSize(width: size.width, height: round(size.width / aspect)) - } else { - return CGSize(width: round(size.height * aspect), height: round(size.height)) - } - } - - func sizedImage(with size: CGSize, completionHandler: @escaping (UIImage?, Error?) -> Void) { - if let asset = self as? PHAsset { - asset.sizedImage(with: size, completionHandler: completionHandler) - } else { - image(with: size, completionHandler: completionHandler) - } - } - - /// Produces a Publisher which contains a resulting image and image URL where available. - /// - Returns: A Publisher containing resuling image, URL and any errors during export. - func imagePublisher() -> AnyPublisher<(UIImage, URL?), Error> { - return Future<(UIImage, URL?), Error> { promise in - let size = self.fit(size: UIScreen.main.nativeBounds.size) - self.sizedImage(with: size) { (image, error) in - guard let image = image else { - if let error = error { - return promise(.failure(error)) - } - return promise(.failure(WPMediaAssetError.imageAssetExportFailed)) - } - return promise(.success((image, nil))) - } - }.eraseToAnyPublisher() - } - - /// Produces a Publisher containing a URL of saved video and any errors which occurred. - /// - /// - Parameters: - /// - skipTransformCheck: Skips the transform check. - /// - /// - Returns: Publisher containing the URL to a saved video and any errors which occurred. - /// - func videoURLPublisher(skipTransformCheck: Bool = false) -> AnyPublisher { - videoAssetPublisher().tryMap { asset -> AnyPublisher in - let filename = UUID().uuidString - let url = try StoryEditor.mediaCacheDirectory().appendingPathComponent(filename) - let urlAsset = asset as? AVURLAsset - - // Portrait video is exported so that it is rotated for use in Kanvas. - // Once the Metal renderer is fixed to properly rotate this media, this can be removed. - let trackTransform = asset.tracks(withMediaType: .video).first?.preferredTransform - - // DRM: I moved this logic into a variable because it seems to be completely out of place in this method - // and it was causing some issues when sharing videos that needed to be downloaded. I added a parameter - // with a default value that will make sure this check is executed for any old code. - let transformCheck = skipTransformCheck || trackTransform == CGAffineTransform.identity - - if let assetURL = urlAsset?.url, transformCheck { - let exportURL = url.appendingPathExtension(assetURL.pathExtension) - if urlAsset?.url.scheme != "file" { - // Download any file which isn't local and move it to the proper location. - return URLSession.shared.downloadTaskPublisher(url: assetURL).tryMap { (location, _) -> URL in - if let location = location { - try FileManager.default.moveItem(at: location, to: exportURL) - return exportURL - } else { - return url - } - }.eraseToAnyPublisher() - } else { - // Return the local asset URL which we will use directly. - return Just(assetURL).setFailureType(to: Error.self).eraseToAnyPublisher() - } - } else { - // Export any other file which isn't an AVURLAsset since we don't have a URL to use. - return try asset.exportPublisher(url: url) - } - }.flatMap { publisher -> AnyPublisher in - return publisher - }.eraseToAnyPublisher() - } -} - private extension AVAsset { - func exportPublisher(url: URL) throws -> AnyPublisher { - let exportURL = url.appendingPathExtension("mov") - + func exportFixingOrientation(to exportURL: URL) async throws -> URL { + let exportURL = exportURL.deletingPathExtension().appendingPathExtension("mov") let (composition, videoComposition) = try rotate() - if let exportSession = AVAssetExportSession(asset: composition, presetName: AVAssetExportPreset1920x1080) { - exportSession.videoComposition = videoComposition - exportSession.outputURL = exportURL - exportSession.outputFileType = .mov - return exportSession.exportPublisher(url: exportURL) - } else { - throw WPMediaAssetError.videoAssetExportFailed + guard let exportSession = AVAssetExportSession(asset: composition, presetName: AVAssetExportPreset1920x1080) else { + throw AssetExportError.videoAssetExportFailed + } + exportSession.videoComposition = videoComposition + exportSession.outputURL = exportURL + exportSession.outputFileType = .mov + await exportSession.export() + if let error = exportSession.error { + throw error } + return exportURL } /// Applies the `preferredTransform` of the video track. /// - Returns: Returns both an AVMutableComposition containing video + audio and an AVVideoComposition of the rotate video. private func rotate() throws -> (AVMutableComposition, AVVideoComposition) { guard let videoTrack = tracks(withMediaType: .video).first else { - throw WPMediaAssetError.assetMissingVideoTrack + throw AssetExportError.assetMissingVideoTrack } let videoComposition = AVMutableVideoComposition(propertiesOf: self) @@ -376,94 +223,9 @@ private extension AVAsset { } } - -enum WPMediaAssetError: Error { - case imageAssetExportFailed +private enum AssetExportError: Error { + case videoLengthLimitExceeded case videoAssetExportFailed case assetMissingVideoTrack -} - -extension WPMediaAsset { - /// Produces a Publisher `AVAsset` from a `WPMediaAsset` object. - /// - Returns: Publisher with an AVAsset and any errors which occur during export. - func videoAssetPublisher() -> AnyPublisher { - Future { [weak self] promise in - self?.videoAsset(completionHandler: { asset, error in - guard let asset = asset else { - if let error = error { - return promise(.failure(error)) - } - return promise(.failure(WPMediaAssetError.videoAssetExportFailed)) - } - promise(.success(asset)) - }) - }.eraseToAnyPublisher() - } -} - -extension URLSession { - typealias DownloadTaskResult = (location: URL?, response: URLResponse?) - /// Produces a Publisher which contains the result of a Download Task - /// - Parameter url: The URL to download from. - /// - Returns: A publisher containing the result of a Download Task and any errors which occur during the download. - func downloadTaskPublisher(url: URL) -> AnyPublisher { - return Deferred { - Future { promise in - URLSession.shared.downloadTask(with: url) { (location, response, error) in - if let error = error { - promise(.failure(error)) - } else { - promise(.success((location, response))) - } - }.resume() - } - }.eraseToAnyPublisher() - } -} - -extension AVAssetExportSession { - /// Produces a publisher which wraps the export of a video. - /// - Parameter url: The location to save the video to. - /// - Returns: A publisher containing the location the asset was saved to and an error. - func exportPublisher(url: URL) -> AnyPublisher { - return Deferred { - Future { [weak self] promise in - self?.exportAsynchronously { [weak self] in - if let error = self?.error { - promise(.failure(error)) - } - promise(.success(url)) - } - } - }.handleEvents(receiveCancel: { - self.cancelExport() - }).eraseToAnyPublisher() - } -} - -extension MediaLibraryGroup { - - @objc(getMediaLibraryCountForMediaTypes:ofBlog:success:failure:) - func getMediaLibraryCount(forMediaTypes types: Set, of blog: Blog, success: @escaping (Int) -> Void, failure: @escaping (Error) -> Void) { - guard let remote = MediaServiceRemoteFactory().remote(for: blog) else { - DispatchQueue.main.async { - failure(MediaRepository.Error.remoteAPIUnavailable) - } - return - } - - let mediaTypes = types.compactMap { - MediaType(rawValue: $0.uintValue) - } - - Task { @MainActor in - do { - let total = try await remote.getMediaLibraryCount(forMediaTypes: mediaTypes) - success(total) - } catch { - failure(error) - } - } - } - + case unexpectedAssetType } diff --git a/WordPress/Classes/Stores/AccountSettingsStore.swift b/WordPress/Classes/Stores/AccountSettingsStore.swift index 50cf059c504d..a6cd34f432a9 100644 --- a/WordPress/Classes/Stores/AccountSettingsStore.swift +++ b/WordPress/Classes/Stores/AccountSettingsStore.swift @@ -22,15 +22,6 @@ enum AccountSettingsState: Equatable { var succeeded: Bool { return self == .success } - - var failureMessage: String? { - switch self { - case .failure(let error): - return error - default: - return nil - } - } } enum AccountSettingsAction: Action { diff --git a/WordPress/Classes/Stores/StatsInsightsStore.swift b/WordPress/Classes/Stores/StatsInsightsStore.swift index 68bf85252461..4e5be4ae60ff 100644 --- a/WordPress/Classes/Stores/StatsInsightsStore.swift +++ b/WordPress/Classes/Stores/StatsInsightsStore.swift @@ -838,9 +838,6 @@ extension StatsInsightsStore { return state.topTagsAndCategories } - func getPostingActivity() -> StatsPostingStreakInsight? { - return state.postingActivity - } /// Summarizes the daily posting count for the month in the given date. /// Returns an array containing every day of the month and associated post count. /// @@ -947,10 +944,6 @@ extension StatsInsightsStore { return state.annualAndMostPopularTimeStatus } - var isFetchingLastPostSummary: Bool { - return lastPostSummaryStatus == .loading - } - var isFetchingOverview: Bool { /* * Use reflection here to inspect all the members of type StoreFetchingStatus diff --git a/WordPress/Classes/Stores/StatsWidgetsStore.swift b/WordPress/Classes/Stores/StatsWidgetsStore.swift index 0e5279dcd4e8..ce197c2bf7df 100644 --- a/WordPress/Classes/Stores/StatsWidgetsStore.swift +++ b/WordPress/Classes/Stores/StatsWidgetsStore.swift @@ -1,3 +1,4 @@ +import JetpackStatsWidgetsCore import WidgetKit import WordPressAuthenticator @@ -95,7 +96,7 @@ class StatsWidgetsStore { homeWidgetCache[siteID.intValue] = HomeWidgetTodayData(siteID: siteID.intValue, siteName: blog.title ?? oldData.siteName, url: blog.url ?? oldData.url, - timeZone: blog.timeZone, + timeZone: blog.timeZone ?? TimeZone.current, date: Date(), stats: stats) as? T @@ -106,7 +107,7 @@ class StatsWidgetsStore { homeWidgetCache[siteID.intValue] = HomeWidgetAllTimeData(siteID: siteID.intValue, siteName: blog.title ?? oldData.siteName, url: blog.url ?? oldData.url, - timeZone: blog.timeZone, + timeZone: blog.timeZone ?? TimeZone.current, date: Date(), stats: stats) as? T @@ -116,7 +117,7 @@ class StatsWidgetsStore { homeWidgetCache[siteID.intValue] = HomeWidgetThisWeekData(siteID: siteID.intValue, siteName: blog.title ?? oldData.siteName, url: blog.url ?? oldData.url, - timeZone: blog.timeZone, + timeZone: blog.timeZone ?? TimeZone.current, date: Date(), stats: stats) as? T } @@ -162,7 +163,7 @@ private extension StatsWidgetsStore { var timeZone = existingSite?.timeZone ?? TimeZone.current if let blog = Blog.lookup(withID: blogID, in: ContextManager.shared.mainContext) { - timeZone = blog.timeZone + timeZone = blog.timeZone ?? TimeZone.current } let date = existingSite?.date ?? Date() @@ -216,21 +217,21 @@ private extension StatsWidgetsStore { result[blogID.intValue] = HomeWidgetTodayData(siteID: blogID.intValue, siteName: title, url: url, - timeZone: timeZone, + timeZone: timeZone ?? TimeZone.current, date: Date(timeIntervalSinceReferenceDate: 0), stats: TodayWidgetStats()) as? T } else if type == HomeWidgetAllTimeData.self { result[blogID.intValue] = HomeWidgetAllTimeData(siteID: blogID.intValue, siteName: title, url: url, - timeZone: timeZone, + timeZone: timeZone ?? TimeZone.current, date: Date(timeIntervalSinceReferenceDate: 0), stats: AllTimeWidgetStats()) as? T } else if type == HomeWidgetThisWeekData.self { result[blogID.intValue] = HomeWidgetThisWeekData(siteID: blogID.intValue, siteName: title, url: url, - timeZone: timeZone, + timeZone: timeZone ?? TimeZone.current, date: Date(timeIntervalSinceReferenceDate: 0), stats: ThisWeekWidgetStats(days: initializedWeekdays)) as? T } @@ -250,7 +251,7 @@ extension StatsWidgetsStore { } let summaryData = Array(summary?.summaryData.reversed().prefix(ThisWeekWidgetStats.maxDaysToDisplay + 1) ?? []) - let stats = ThisWeekWidgetStats(days: ThisWeekWidgetStats.daysFrom(summaryData: summaryData)) + let stats = ThisWeekWidgetStats(days: ThisWeekWidgetStats.daysFrom(summaryData: summaryData.map { ThisWeekWidgetStats.Input(periodStartDate: $0.periodStartDate, viewsCount: $0.viewsCount) })) StoreContainer.shared.statsWidgets.storeHomeWidgetData(widgetType: HomeWidgetThisWeekData.self, stats: stats) case .week: WidgetCenter.shared.reloadThisWeekTimelines() diff --git a/WordPress/Classes/Stores/UserPersistentRepositoryUtility.swift b/WordPress/Classes/Stores/UserPersistentRepositoryUtility.swift index 2e0e2a15b669..b307eaade20f 100644 --- a/WordPress/Classes/Stores/UserPersistentRepositoryUtility.swift +++ b/WordPress/Classes/Stores/UserPersistentRepositoryUtility.swift @@ -17,6 +17,7 @@ private enum UPRUConstants { static let announcementsVersionDisplayedKey = "announcementsVersionDisplayed" static let isJPContentImportCompleteKey = "jetpackContentImportComplete" static let jetpackContentMigrationStateKey = "jetpackContentMigrationState" + static let mediaAspectRatioModeEnabledKey = "mediaAspectRatioModeEnabled" } protocol UserPersistentRepositoryUtility: AnyObject { @@ -185,4 +186,17 @@ extension UserPersistentRepositoryUtility { UserPersistentStoreFactory.instance().set(newValue.rawValue, forKey: UPRUConstants.jetpackContentMigrationStateKey) } } + + var isMediaAspectRatioModeEnabled: Bool { + get { + let repository = UserPersistentStoreFactory.instance() + if let value = repository.object(forKey: UPRUConstants.mediaAspectRatioModeEnabledKey) as? Bool { + return value + } + return UIDevice.current.userInterfaceIdiom == .pad + } + set { + UserPersistentStoreFactory.instance().set(newValue, forKey: UPRUConstants.mediaAspectRatioModeEnabledKey) + } + } } diff --git a/WordPress/Classes/Stores/UserPersistentStore.swift b/WordPress/Classes/Stores/UserPersistentStore.swift deleted file mode 100644 index a4d8341ac36f..000000000000 --- a/WordPress/Classes/Stores/UserPersistentStore.swift +++ /dev/null @@ -1,126 +0,0 @@ -class UserPersistentStore: UserPersistentRepository { - static let standard = UserPersistentStore(defaultsSuiteName: defaultsSuiteName)! - private static let defaultsSuiteName = WPAppGroupName // TBD - - private let userDefaults: UserDefaults - - init?(defaultsSuiteName: String) { - guard let suiteDefaults = UserDefaults(suiteName: defaultsSuiteName) else { - return nil - } - userDefaults = suiteDefaults - } - - // MARK: - UserPeresistentRepositoryReader - func object(forKey key: String) -> Any? { - if let object = userDefaults.object(forKey: key) { - return object - } - - return UserDefaults.standard.object(forKey: key) - } - - func string(forKey key: String) -> String? { - if let string = userDefaults.string(forKey: key) { - return string - } - - return UserDefaults.standard.string(forKey: key) - } - - func bool(forKey key: String) -> Bool { - userDefaults.bool(forKey: key) || UserDefaults.standard.bool(forKey: key) - } - - func integer(forKey key: String) -> Int { - let suiteValue = userDefaults.integer(forKey: key) - if suiteValue != 0 { - return suiteValue - } - - return UserDefaults.standard.integer(forKey: key) - } - - func float(forKey key: String) -> Float { - let suiteValue = userDefaults.float(forKey: key) - if suiteValue != 0 { - return suiteValue - } - - return UserDefaults.standard.float(forKey: key) - } - - func double(forKey key: String) -> Double { - let suiteValue = userDefaults.double(forKey: key) - if suiteValue != 0 { - return suiteValue - } - - return UserDefaults.standard.double(forKey: key) - } - - func array(forKey key: String) -> [Any]? { - let suiteValue = userDefaults.array(forKey: key) - if suiteValue != nil { - return suiteValue - } - - return UserDefaults.standard.array(forKey: key) - } - - func dictionary(forKey key: String) -> [String: Any]? { - let suiteValue = userDefaults.dictionary(forKey: key) - if suiteValue != nil { - return suiteValue - } - - return UserDefaults.standard.dictionary(forKey: key) - } - - func url(forKey key: String) -> URL? { - if let url = userDefaults.url(forKey: key) { - return url - } - - return UserDefaults.standard.url(forKey: key) - } - - func dictionaryRepresentation() -> [String: Any] { - return userDefaults.dictionaryRepresentation() - } - - // MARK: - UserPersistentRepositoryWriter - func set(_ value: Any?, forKey key: String) { - userDefaults.set(value, forKey: key) - UserDefaults.standard.removeObject(forKey: key) - } - - func set(_ value: Int, forKey key: String) { - userDefaults.set(value, forKey: key) - UserDefaults.standard.removeObject(forKey: key) - } - - func set(_ value: Float, forKey key: String) { - userDefaults.set(value, forKey: key) - UserDefaults.standard.removeObject(forKey: key) - } - - func set(_ value: Double, forKey key: String) { - userDefaults.set(value, forKey: key) - UserDefaults.standard.removeObject(forKey: key) - } - - func set(_ value: Bool, forKey key: String) { - userDefaults.set(value, forKey: key) - UserDefaults.standard.removeObject(forKey: key) - } - - func set(_ url: URL?, forKey key: String) { - userDefaults.set(url, forKey: key) - UserDefaults.standard.removeObject(forKey: key) - } - - func removeObject(forKey key: String) { - userDefaults.removeObject(forKey: key) - } -} diff --git a/WordPress/Classes/System/3DTouch/WP3DTouchShortcutHandler.swift b/WordPress/Classes/System/3DTouch/WP3DTouchShortcutHandler.swift index b7f5b13e85eb..775d5d02c244 100644 --- a/WordPress/Classes/System/3DTouch/WP3DTouchShortcutHandler.swift +++ b/WordPress/Classes/System/3DTouch/WP3DTouchShortcutHandler.swift @@ -8,14 +8,6 @@ open class WP3DTouchShortcutHandler: NSObject { case Stats case Notifications - init?(fullType: String) { - guard let last = fullType.components(separatedBy: ".").last else { - return nil - } - - self.init(rawValue: last) - } - var type: String { return Bundle.main.bundleIdentifier! + ".\(self.rawValue)" } diff --git a/WordPress/Classes/System/RootViewPresenter+MeNavigation.swift b/WordPress/Classes/System/RootViewPresenter+MeNavigation.swift index 54d04943f372..e96ef7a90dba 100644 --- a/WordPress/Classes/System/RootViewPresenter+MeNavigation.swift +++ b/WordPress/Classes/System/RootViewPresenter+MeNavigation.swift @@ -24,6 +24,15 @@ extension RootViewPresenter { } } + func navigateToAllDomains() { + CATransaction.perform { + showMeScreen() + } completion: { + self.meViewController?.navigateToAllDomains() + } + } + + func navigateToAppSettings() { CATransaction.perform { showMeScreen() diff --git a/WordPress/Classes/System/UITestConfigurator.swift b/WordPress/Classes/System/UITestConfigurator.swift new file mode 100644 index 000000000000..eb102277ae41 --- /dev/null +++ b/WordPress/Classes/System/UITestConfigurator.swift @@ -0,0 +1,34 @@ +import Foundation + +struct UITestConfigurator { + static func prepareApplicationForUITests(_ application: UIApplication) { + disableAnimations(application) + logoutAtLaunch() + disableCompliancePopover() + } + + /// This method will disable animations and speed-up keyboad input if command-line arguments includes "NoAnimations" + /// It was designed to be used in UI test suites. To enable it just pass a launch argument into XCUIApplicaton: + /// + /// XCUIApplication().launchArguments = ["-no-animations"] + /// + private static func disableAnimations(_ application: UIApplication) { + if CommandLine.arguments.contains("-no-animations") { + UIView.setAnimationsEnabled(false) + application.windows.first?.layer.speed = MAXFLOAT + application.mainWindow?.layer.speed = MAXFLOAT + } + } + + private static func logoutAtLaunch() { + if CommandLine.arguments.contains("-logout-at-launch") { + AccountHelper.logOutDefaultWordPressComAccount() + } + } + + private static func disableCompliancePopover() { + if CommandLine.arguments.contains("-ui-testing") { + UserDefaults.standard.didShowCompliancePopup = true + } + } +} diff --git a/WordPress/Classes/System/WPGUIConstants.h b/WordPress/Classes/System/WPGUIConstants.h index e8cc1ba4159e..f1ae58cbb097 100644 --- a/WordPress/Classes/System/WPGUIConstants.h +++ b/WordPress/Classes/System/WPGUIConstants.h @@ -3,14 +3,6 @@ extern const CGFloat WPAlphaFull; extern const CGFloat WPAlphaZero; -extern const CGFloat WPColorFull; -extern const CGFloat WPColorZero; - -extern const NSTimeInterval WPAnimationDurationSlow; extern const NSTimeInterval WPAnimationDurationDefault; -extern const NSTimeInterval WPAnimationDurationFast; -extern const NSTimeInterval WPAnimationDurationFaster; -extern const CGFloat WPTableViewTopMargin; extern const CGFloat WPTableViewDefaultRowHeight; -extern const UIEdgeInsets WPTableViewContentInsets; diff --git a/WordPress/Classes/System/WPGUIConstants.m b/WordPress/Classes/System/WPGUIConstants.m index 94b6f9dd7f63..90e3f9161ba2 100644 --- a/WordPress/Classes/System/WPGUIConstants.m +++ b/WordPress/Classes/System/WPGUIConstants.m @@ -3,14 +3,6 @@ const CGFloat WPAlphaFull = 1.0; const CGFloat WPAlphaZero = 0.0; -const CGFloat WPColorFull = 1.0; -const CGFloat WPColorZero = 0.0; - -const NSTimeInterval WPAnimationDurationSlow = 0.6; const NSTimeInterval WPAnimationDurationDefault = 0.33; -const NSTimeInterval WPAnimationDurationFast = 0.15; -const NSTimeInterval WPAnimationDurationFaster = 0.07; -const CGFloat WPTableViewTopMargin = 40.0; const CGFloat WPTableViewDefaultRowHeight = 44.0; -const UIEdgeInsets WPTableViewContentInsets = {40.0, 0.0, 0.0, 0.0}; diff --git a/WordPress/Classes/System/WordPress-Bridging-Header.h b/WordPress/Classes/System/WordPress-Bridging-Header.h index 1a69dbe6bdff..fd46661e248e 100644 --- a/WordPress/Classes/System/WordPress-Bridging-Header.h +++ b/WordPress/Classes/System/WordPress-Bridging-Header.h @@ -17,8 +17,6 @@ #import "CommentService.h" #import "CommentsViewController+Network.h" -#import "ConfigurablePostView.h" -#import "Confirmable.h" #import "Constants.h" #import "CoreDataStack.h" #import "Coordinate.h" @@ -29,16 +27,16 @@ #import "LocalCoreDataService.h" #import "Media.h" -#import "MediaLibraryPickerDataSource.h" #import "MediaService.h" #import "MeHeaderView.h" #import "MenuItem.h" #import "MenuItemsViewController.h" +#import "MenusService.h" #import "MenusViewController.h" +#import "Media+Extensions.h" #import "NSObject+Helpers.h" -#import "PageListTableViewCell.h" #import "PageSettingsViewController.h" #import "PostContentProvider.h" #import "PostCategory.h" @@ -93,6 +91,7 @@ #import "WPAnimatedBox.h" #import "WPAnalyticsTrackerWPCom.h" #import "WPAppAnalytics.h" +#import "WPAnalyticsTrackerAutomatticTracks.h" #import "WPAuthTokenIssueSolver.h" #import "WPBlogTableViewCell.h" #import "WPUploadStatusButton.h" @@ -101,7 +100,6 @@ #import "WPImageViewController.h" #import "WPScrollableViewController.h" #import "WPStyleGuide+Pages.h" -#import "WPStyleGuide+ReadableMargins.h" #import "WPStyleGuide+WebView.h" #import "WPTableViewHandler.h" #import "WPUserAgent.h" @@ -116,8 +114,6 @@ // Pods #import -#import - #import #import #import diff --git a/WordPress/Classes/System/WordPressAppDelegate+PostCoordinatorDelegate.swift b/WordPress/Classes/System/WordPressAppDelegate+PostCoordinatorDelegate.swift new file mode 100644 index 000000000000..4f0cec5f647f --- /dev/null +++ b/WordPress/Classes/System/WordPressAppDelegate+PostCoordinatorDelegate.swift @@ -0,0 +1,27 @@ +import UIKit + +extension WordPressAppDelegate: PostCoordinatorDelegate { + func postCoordinator(_ postCoordinator: PostCoordinator, promptForPasswordForBlog blog: Blog) { + showPasswordInvalidPrompt(for: blog) + } + + func showPasswordInvalidPrompt(for blog: Blog) { + WPError.showAlert(withTitle: Strings.unableToConnect, message: Strings.invalidPasswordMessage, withSupportButton: true) { _ in + + let editSiteViewController = SiteSettingsViewController(blog: blog) + + let navController = UINavigationController(rootViewController: editSiteViewController!) + + editSiteViewController?.navigationItem.rightBarButtonItem = UIBarButtonItem(systemItem: .done, primaryAction: UIAction { [weak navController] _ in + navController?.presentingViewController?.dismiss(animated: true) + }) + + self.window?.topmostPresentedViewController?.present(navController, animated: true) + } + } +} + +private enum Strings { + static let invalidPasswordMessage = NSLocalizedString("common.reEnterPasswordMessage", value: "The username or password stored in the app may be out of date. Please re-enter your password in the settings and try again.", comment: "Error message informing a user about an invalid password.") + static let unableToConnect = NSLocalizedString("common.unableToConnect", value: "Unable to Connect", comment: "An error message.") +} diff --git a/WordPress/Classes/System/WordPressAppDelegate.swift b/WordPress/Classes/System/WordPressAppDelegate.swift index 988cb72dee17..68417181fd29 100644 --- a/WordPress/Classes/System/WordPressAppDelegate.swift +++ b/WordPress/Classes/System/WordPressAppDelegate.swift @@ -93,6 +93,7 @@ class WordPressAppDelegate: UIResponder, UIApplicationDelegate { AppAppearance.overrideAppearance() MemoryCache.shared.register() MediaImageService.migrateCacheIfNeeded() + PostCoordinator.shared.delegate = self // Start CrashLogging as soon as possible (in case a crash happens during startup) try? loggingStack.start() @@ -131,12 +132,13 @@ class WordPressAppDelegate: UIResponder, UIApplicationDelegate { ABTest.start() Media.removeTemporaryData() + NSItemProvider.removeTemporaryData() InteractiveNotificationsManager.shared.registerForUserNotifications() setupPingHub() setupBackgroundRefresh(application) setupComponentsAppearance() - disableAnimationsForUITests(application) - logoutAtLaunchForUITests(application) + UITestConfigurator.prepareApplicationForUITests(application) + DebugMenuViewController.configure(in: window) // This was necessary to properly load fonts for the Stories editor. I believe external libraries may require this call to access fonts. let fonts = Bundle.main.urls(forResourcesWithExtension: "ttf", subdirectory: nil) @@ -342,10 +344,6 @@ class WordPressAppDelegate: UIResponder, UIApplicationDelegate { pingHubManager = PingHubManager() } - private func setupShortcutCreator() { - shortcutCreator = WP3DTouchShortcutCreator() - } - private func setupNoticePresenter() { noticePresenter = NoticePresenter() } @@ -360,25 +358,6 @@ class WordPressAppDelegate: UIResponder, UIApplicationDelegate { // MARK: - Helpers - /// This method will disable animations and speed-up keyboad input if command-line arguments includes "NoAnimations" - /// It was designed to be used in UI test suites. To enable it just pass a launch argument into XCUIApplicaton: - /// - /// XCUIApplication().launchArguments = ["-no-animations"] - /// - private func disableAnimationsForUITests(_ application: UIApplication) { - if CommandLine.arguments.contains("-no-animations") { - UIView.setAnimationsEnabled(false) - application.windows.first?.layer.speed = MAXFLOAT - application.mainWindow?.layer.speed = MAXFLOAT - } - } - - private func logoutAtLaunchForUITests(_ application: UIApplication) { - if CommandLine.arguments.contains("-logout-at-launch") { - AccountHelper.logOutDefaultWordPressComAccount() - } - } - var runningInBackground: Bool { return UIApplication.shared.applicationState == .background } @@ -660,20 +639,6 @@ extension WordPressAppDelegate { } } - var isWelcomeScreenVisible: Bool { - get { - guard let presentedViewController = window?.rootViewController?.presentedViewController as? UINavigationController else { - return false - } - - guard let visibleViewController = presentedViewController.visibleViewController else { - return false - } - - return WordPressAuthenticator.isAuthenticationViewController(visibleViewController) - } - } - @objc func trackLogoutIfNeeded() { if AccountHelper.isLoggedIn == false { WPAnalytics.track(.logout) @@ -906,7 +871,6 @@ extension WordPressAppDelegate { WPStyleGuide.configureLightNavigationBarAppearance() WPStyleGuide.configureToolbarAppearance() - UISegmentedControl.appearance().setTitleTextAttributes( [NSAttributedString.Key.font: WPStyleGuide.regularTextFont()], for: .normal) UISwitch.appearance().onTintColor = .primary let navReferenceAppearance = UINavigationBar.appearance(whenContainedInInstancesOf: [UIReferenceLibraryViewController.self]) @@ -923,21 +887,6 @@ extension WordPressAppDelegate { SVProgressHUD.setErrorImage(UIImage(named: "hud_error")!) SVProgressHUD.setSuccessImage(UIImage(named: "hud_success")!) - // Media Picker styles - let barItemAppearance = UIBarButtonItem.appearance(whenContainedInInstancesOf: [WPMediaPickerViewController.self]) - barItemAppearance.setTitleTextAttributes([NSAttributedString.Key.foregroundColor: UIColor.white, NSAttributedString.Key.font: WPFontManager.systemSemiBoldFont(ofSize: 16.0)], for: .disabled) - UICollectionView.appearance(whenContainedInInstancesOf: [WPMediaPickerViewController.self]).backgroundColor = .neutral(.shade5) - - let cellAppearance = WPMediaCollectionViewCell.appearance(whenContainedInInstancesOf: [WPMediaPickerViewController.self]) - cellAppearance.loadingBackgroundColor = .listBackground - cellAppearance.placeholderBackgroundColor = .neutral(.shade70) - cellAppearance.placeholderTintColor = .neutral(.shade5) - cellAppearance.setCellTintColor(.primary) - - UIButton.appearance(whenContainedInInstancesOf: [WPActionBar.self]).tintColor = .primary - WPActionBar.appearance().barBackgroundColor = .basicBackground - WPActionBar.appearance().lineColor = .basicBackground - // Post Settings styles UITableView.appearance(whenContainedInInstancesOf: [AztecNavigationController.self]).tintColor = .editorPrimary UISwitch.appearance(whenContainedInInstancesOf: [AztecNavigationController.self]).onTintColor = .editorPrimary diff --git a/WordPress/Classes/Utility/Analytics/DashboardDynamicCardAnalyticsEvent.swift b/WordPress/Classes/Utility/Analytics/DashboardDynamicCardAnalyticsEvent.swift new file mode 100644 index 000000000000..e61bbc6e6b92 --- /dev/null +++ b/WordPress/Classes/Utility/Analytics/DashboardDynamicCardAnalyticsEvent.swift @@ -0,0 +1,32 @@ +enum DashboardDynamicCardAnalyticsEvent: Hashable { + + case cardShown(id: String) + case cardTapped(id: String, url: String?) + case cardCtaTapped(id: String, url: String?) + + var name: String { + switch self { + case .cardShown: return "dynamic_dashboard_card_shown" + case .cardTapped: return "dynamic_dashboard_card_tapped" + case .cardCtaTapped: return "dynamic_dashboard_card_cta_tapped" + } + } + + var properties: [String: String] { + switch self { + case .cardShown(let id): + return [Keys.id: id] + case .cardTapped(let id, let url), .cardCtaTapped(let id, let url): + var props = [Keys.id: id] + if let url { + props[Keys.url] = url + } + return props + } + } + + private enum Keys { + static let id = "id" + static let url = "url" + } +} diff --git a/WordPress/Classes/Utility/Analytics/WPAnalytics+Domains.swift b/WordPress/Classes/Utility/Analytics/WPAnalytics+Domains.swift index c55260d4f62c..7504ae371bc1 100644 --- a/WordPress/Classes/Utility/Analytics/WPAnalytics+Domains.swift +++ b/WordPress/Classes/Utility/Analytics/WPAnalytics+Domains.swift @@ -2,29 +2,54 @@ import Foundation extension WPAnalytics { - /// Checks if the Domain Purchasing Feature Flag and AB Experiment are enabled + /// Checks if the Domain Purchasing Feature Flag is enabled. private static var domainPurchasingEnabled: Bool { RemoteFeatureFlag.plansInSiteCreation.enabled() } - static func domainsProperties(for blog: Blog, origin: SiteCreationWebViewViewOrigin? = .menu) -> [AnyHashable: Any] { - domainsProperties(usingCredit: blog.canRegisterDomainWithPaidPlan, origin: origin) + /// Checks if the Domain Management Feature Flag is enabled. + private static var domainManagementEnabled: Bool { + return RemoteFeatureFlag.domainManagement.enabled() } static func domainsProperties( - usingCredit: Bool, - origin: SiteCreationWebViewViewOrigin? + usingCredit: Bool? = nil, + origin: String? = nil, + domainOnly: Bool? = nil ) -> [AnyHashable: Any] { - var dict: [AnyHashable: Any] = ["using_credit": usingCredit.stringLiteral] - if Self.domainPurchasingEnabled, - let origin = origin { - dict["origin"] = origin.rawValue + var dict: [AnyHashable: Any] = [:] + if let usingCredit { + dict["using_credit"] = usingCredit.stringLiteral + } + if Self.domainPurchasingEnabled, let origin = origin { + dict["origin"] = origin + } + if let domainOnly, Self.domainManagementEnabled { + dict["domain_only"] = domainOnly.stringLiteral } return dict } + + static func domainsProperties( + for blog: Blog, + origin: String? + ) -> [AnyHashable: Any] { + Self.domainsProperties( + usingCredit: blog.canRegisterDomainWithPaidPlan, + origin: origin, + domainOnly: nil + ) + } + + static func domainsProperties( + for blog: Blog, + origin: DomainsAnalyticsWebViewOrigin? = .menu + ) -> [AnyHashable: Any] { + Self.domainsProperties(for: blog, origin: origin?.rawValue) + } } -enum SiteCreationWebViewViewOrigin: String { +enum DomainsAnalyticsWebViewOrigin: String { case siteCreation = "site_creation" case menu } diff --git a/WordPress/Classes/Utility/Analytics/WPAnalyticsEvent.swift b/WordPress/Classes/Utility/Analytics/WPAnalyticsEvent.swift index aae26e615bc7..bf617ab86ea4 100644 --- a/WordPress/Classes/Utility/Analytics/WPAnalyticsEvent.swift +++ b/WordPress/Classes/Utility/Analytics/WPAnalyticsEvent.swift @@ -19,6 +19,9 @@ import Foundation case mediaLibraryAddedPhotoViaTenor case editorAddedPhotoViaTenor + // Media + case siteMediaShareTapped + // Settings and Prepublishing Nudges case editorPostPublishTap case editorPostPublishDismissed @@ -227,6 +230,8 @@ import Foundation // Domains case domainsDashboardViewed case domainsDashboardAddDomainTapped + case domainsDashboardGetDomainTapped + case domainsDashboardGetPlanTapped case domainsSearchSelectDomainTapped case domainsRegistrationFormViewed case domainsRegistrationFormSubmitted @@ -236,6 +241,23 @@ import Foundation case domainTransferMoreTapped case domainTransferButtonTapped + // Domain Management + case meDomainsTapped + case allDomainsDomainDetailsWebViewShown + case domainsDashboardAllDomainsTapped + case domainsDashboardDomainsSearchShown + case domainsListShown + case allDomainsFindDomainTapped + case addDomainTapped + case domainsSearchTransferDomainTapped + case domainsSearchRowSelected + case siteSwitcherSiteSelected + case purchaseDomainScreenShown + case purchaseDomainGetDomainTapped + case purchaseDomainChooseSiteTapped + case purchaseDomainCompleted + case myDomainsSearchDomainTapped + // My Site case mySitePullToRefresh @@ -244,6 +266,11 @@ import Foundation case mySiteNoSitesViewActionTapped case mySiteNoSitesViewHidden + // My Site: Header Actions + case mySiteHeaderMoreTapped + case mySiteHeaderAddSiteTapped + case mySiteHeaderPersonalizeHomeTapped + // Site Switcher case mySiteSiteSwitcherTapped case siteSwitcherDisplayed @@ -255,8 +282,11 @@ import Foundation // Post List case postListShareAction + case postListCommentsAction case postListSetAsPostsPageAction case postListSetHomePageAction + case postListSetAsRegularPageAction + case postListSettingsAction // Page List case pageListEditHomepageTapped @@ -280,10 +310,14 @@ import Foundation case accountCloseCompleted // App Settings + case appSettingsOptimizeImagesChanged + case appSettingsMaxImageSizeChanged + case appSettingsImageQualityChanged case appSettingsClearMediaCacheTapped case appSettingsClearSpotlightIndexTapped case appSettingsClearSiriSuggestionsTapped case appSettingsOpenDeviceSettingsTapped + case appSettingsOptimizeImagesPopupTapped // Notifications case notificationsPreviousTapped @@ -413,6 +447,12 @@ import Foundation case promptsOtherAnswersTapped case promptsSettingsShowPromptsTapped + // Bloganuary Nudges + case bloganuaryNudgeCardLearnMoreTapped + case bloganuaryNudgeModalShown + case bloganuaryNudgeModalDismissed + case bloganuaryNudgeModalActionTapped + // Jetpack branding case jetpackPoweredBadgeTapped case jetpackPoweredBannerTapped @@ -509,6 +549,14 @@ import Foundation case freeToPaidPlansDashboardCardMenuTapped case freeToPaidPlansDashboardCardHidden + // SoTW 2023 Nudge + case sotw2023NudgePostEventCardShown + case sotw2023NudgePostEventCardCTATapped + case sotw2023NudgePostEventCardHideTapped + + // Widgets + case widgetsLoadedOnApplicationOpened + /// A String that represents the event var value: String { switch self { @@ -536,6 +584,9 @@ import Foundation return "media_library_photo_added" case .editorAddedPhotoViaTenor: return "editor_photo_added" + // Media + case .siteMediaShareTapped: + return "site_media_shared_tapped" // Editor case .editorPostPublishTap: return "editor_post_publish_tapped" @@ -909,6 +960,10 @@ import Foundation return "domains_dashboard_viewed" case .domainsDashboardAddDomainTapped: return "domains_dashboard_add_domain_tapped" + case .domainsDashboardGetDomainTapped: + return "domains_dashboard_get_domain_tapped" + case .domainsDashboardGetPlanTapped: + return "domains_dashboard_get_plan_tapped" case .domainsSearchSelectDomainTapped: return "domains_dashboard_select_domain_tapped" case .domainsRegistrationFormViewed: @@ -926,6 +981,38 @@ import Foundation case .domainTransferButtonTapped: return "dashboard_card_domain_transfer_button_tapped" + // Domain Management + case .meDomainsTapped: + return "me_all_domains_tapped" + case .allDomainsDomainDetailsWebViewShown: + return "all_domains_domain_details_web_view_shown" + case .domainsDashboardAllDomainsTapped: + return "domains_dashboard_all_domains_tapped" + case .domainsDashboardDomainsSearchShown: + return "domains_dashboard_domains_search_shown" + case .domainsListShown: + return "all_domains_list_shown" + case .allDomainsFindDomainTapped: + return "domain_management_all_domains_find_domain_tapped" + case .addDomainTapped: + return "all_domains_add_domain_tapped" + case .domainsSearchTransferDomainTapped: + return "domains_dashboard_domains_search_transfer_domain_tapped" + case .domainsSearchRowSelected: + return "domain_management_domains_search_row_selected" + case .siteSwitcherSiteSelected: + return "site_switcher_site_selected" + case .purchaseDomainScreenShown: + return "domain_management_purchase_domain_screen_shown" + case .purchaseDomainGetDomainTapped: + return "domain_management_purchase_domain_get_domain_tapped" + case .purchaseDomainChooseSiteTapped: + return "domain_management_purchase_domain_choose_site_tapped" + case .purchaseDomainCompleted: + return "domain_management_purchase_domain_completed" + case .myDomainsSearchDomainTapped: + return "domain_management_my_domains_search_domain_tapped" + // My Site case .mySitePullToRefresh: return "my_site_pull_to_refresh" @@ -938,6 +1025,14 @@ import Foundation case .mySiteNoSitesViewHidden: return "my_site_no_sites_view_hidden" + // My Site Header Actions + case .mySiteHeaderMoreTapped: + return "my_site_header_more_tapped" + case .mySiteHeaderAddSiteTapped: + return "my_site_header_add_site_tapped" + case .mySiteHeaderPersonalizeHomeTapped: + return "my_site_header_personalize_home_tapped" + // Site Switcher case .mySiteSiteSwitcherTapped: return "my_site_site_switcher_tapped" @@ -957,10 +1052,16 @@ import Foundation // Post List case .postListShareAction: return "post_list_button_pressed" + case .postListCommentsAction: + return "post_list_button_pressed" case .postListSetAsPostsPageAction: return "post_list_button_pressed" case .postListSetHomePageAction: return "post_list_button_pressed" + case .postListSetAsRegularPageAction: + return "post_list_button_pressed" + case .postListSettingsAction: + return "post_list_button_pressed" // Page List case .pageListEditHomepageTapped: @@ -995,6 +1096,14 @@ import Foundation return "app_settings_clear_siri_suggestions_tapped" case .appSettingsOpenDeviceSettingsTapped: return "app_settings_open_device_settings_tapped" + case .appSettingsOptimizeImagesChanged: + return "app_settings_optimize_images_changed" + case .appSettingsMaxImageSizeChanged: + return "app_settings_max_image_size_changed" + case .appSettingsImageQualityChanged: + return "app_settings_image_quality_changed" + case .appSettingsOptimizeImagesPopupTapped: + return "app_settings_optimize_images_popup_tapped" // Account Close case .accountCloseTapped: @@ -1218,6 +1327,16 @@ import Foundation case .promptsSettingsShowPromptsTapped: return "blogging_prompts_settings_show_prompts_tapped" + // Bloganuary Nudges + case .bloganuaryNudgeCardLearnMoreTapped: + return "bloganuary_nudge_my_site_card_learn_more_tapped" + case .bloganuaryNudgeModalShown: + return "bloganuary_nudge_learn_more_modal_shown" + case .bloganuaryNudgeModalDismissed: + return "bloganuary_nudge_learn_more_modal_dismissed" + case .bloganuaryNudgeModalActionTapped: + return "bloganuary_nudge_learn_more_modal_action_tapped" + // Jetpack branding case .jetpackPoweredBadgeTapped: return "jetpack_powered_badge_tapped" @@ -1388,6 +1507,18 @@ import Foundation case .freeToPaidPlansDashboardCardMenuTapped: return "free_to_paid_plan_dashboard_card_menu_tapped" + // SoTW 2023 Nudge + case .sotw2023NudgePostEventCardShown: + return "sotw_2023_nudge_post_event_card_shown" + case .sotw2023NudgePostEventCardCTATapped: + return "sotw_2023_nudge_post_event_card_cta_tapped" + case .sotw2023NudgePostEventCardHideTapped: + return "sotw_2023_nudge_post_event_card_hide_tapped" + + // Widgets + case .widgetsLoadedOnApplicationOpened: + return "widgets_loaded_on_application_opened" + } // END OF SWITCH } @@ -1408,10 +1539,16 @@ import Foundation return ["via": "tenor"] case .postListShareAction: return ["button": "share"] + case .postListCommentsAction: + return ["button": "comments"] case .postListSetAsPostsPageAction: return ["button": "set_posts_page"] case .postListSetHomePageAction: return ["button": "set_homepage"] + case .postListSetAsRegularPageAction: + return ["button": "set_regular_page"] + case .postListSettingsAction: + return ["button": "settings"] default: return nil } diff --git a/WordPress/Classes/Utility/Analytics/WPAppAnalytics.h b/WordPress/Classes/Utility/Analytics/WPAppAnalytics.h index c8aae9f55d0d..8d4e5fcf8192 100644 --- a/WordPress/Classes/Utility/Analytics/WPAppAnalytics.h +++ b/WordPress/Classes/Utility/Analytics/WPAppAnalytics.h @@ -141,4 +141,8 @@ extern NSString * const WPAppAnalyticsValueSiteTypeP2; */ + (void)track:(WPAnalyticsStat)stat error:(NSError *)error; +/** + * @brief Track Anaylytics with associate error that is translated to properties, along with available blog details + */ ++ (void)track:(WPAnalyticsStat)stat error:(NSError *)error withBlogID:(NSNumber *)blogID; @end diff --git a/WordPress/Classes/Utility/Analytics/WPAppAnalytics.m b/WordPress/Classes/Utility/Analytics/WPAppAnalytics.m index 47db3af7b578..526b06ca2bbc 100644 --- a/WordPress/Classes/Utility/Analytics/WPAppAnalytics.m +++ b/WordPress/Classes/Utility/Analytics/WPAppAnalytics.m @@ -145,6 +145,7 @@ - (void)applicationDidBecomeActive:(NSNotification*)notification [self incrementSessionCount]; [self trackApplicationOpened]; [SearchAdsAttribution.instance requestDetails]; + [WidgetAnalytics trackLoadedWidgetsOnApplicationOpened]; } - (void)applicationDidEnterBackground:(NSNotification*)notification @@ -337,14 +338,18 @@ + (void)track:(WPAnalyticsStat)stat withProperties:(NSDictionary *)properties { [WPAnalytics track:stat withProperties:properties]; } -+ (void)track:(WPAnalyticsStat)stat error:(NSError * _Nonnull)error { ++ (void)track:(WPAnalyticsStat)stat error:(NSError * _Nonnull)error withBlogID:(NSNumber *)blogID { NSError *err = [self sanitizedErrorFromError:error]; NSDictionary *properties = @{ @"error_code": [@(err.code) stringValue], @"error_domain": err.domain, @"error_description": err.description }; - [self track:stat withProperties: properties]; + [self track:stat withProperties: properties withBlogID:blogID]; +} + ++ (void)track:(WPAnalyticsStat)stat error:(NSError * _Nonnull)error { + [self track:stat error:error withBlogID:nil]; } /** diff --git a/WordPress/Classes/Utility/App Configuration/AppStyleGuide.swift b/WordPress/Classes/Utility/App Configuration/AppStyleGuide.swift index 0853ed209f2f..cfea5805e207 100644 --- a/WordPress/Classes/Utility/App Configuration/AppStyleGuide.swift +++ b/WordPress/Classes/Utility/App Configuration/AppStyleGuide.swift @@ -29,7 +29,6 @@ extension AppStyleGuide { // MARK: - Images extension AppStyleGuide { static let mySiteTabIcon = UIImage(named: "icon-tab-mysites") - static let aboutAppIcon = UIImage(named: "icon-wp") static let quickStartExistingSite = UIImage(named: "wp-illustration-quickstart-existing-site") } diff --git a/WordPress/Classes/Utility/BackgroundTasks/WeeklyRoundupBackgroundTask.swift b/WordPress/Classes/Utility/BackgroundTasks/WeeklyRoundupBackgroundTask.swift index d5452f2a7ead..0e8aac2ac1b0 100644 --- a/WordPress/Classes/Utility/BackgroundTasks/WeeklyRoundupBackgroundTask.swift +++ b/WordPress/Classes/Utility/BackgroundTasks/WeeklyRoundupBackgroundTask.swift @@ -14,6 +14,7 @@ private class WeeklyRoundupDataProvider { case authTokenNotFound case failedToMakePeriodEndDate case dotComSiteWithoutDotComID(_ site: NSManagedObjectID) + case timezoneError case siteFetchingError(_ error: Error) case unknownErrorRetrievingStats(_ site: NSManagedObjectID) case errorRetrievingStats(_ blogID: Int?, error: Error) @@ -253,8 +254,11 @@ private class WeeklyRoundupDataProvider { guard let dotComID = site.dotComID else { throw DataRequestError.dotComSiteWithoutDotComID(site.managedObjectID) } + guard let siteTimezone = site.timeZone else { + throw DataRequestError.timezoneError + } let wpApi = WordPressComRestApi.defaultApi(oAuthToken: authToken, userAgent: WPUserAgent.wordPress()) - return StatsServiceRemoteV2(wordPressComRestApi: wpApi, siteID: dotComID, siteTimezone: site.timeZone) + return StatsServiceRemoteV2(wordPressComRestApi: wpApi, siteID: dotComID, siteTimezone: siteTimezone) } // MARK: - Types @@ -264,7 +268,7 @@ private class WeeklyRoundupDataProvider { let managedObjectID: NSManagedObjectID let authToken: String? let dotComID: Int? - let timeZone: TimeZone + let timeZone: TimeZone? let isAdmin: Bool let isAutomatticP2: Bool @@ -320,7 +324,6 @@ class WeeklyRoundupBackgroundTask: BackgroundTask { enum RunError: Error { case unableToScheduleDynamicNotification(reason: String) - case staticNotificationAlreadyDelivered } private let eventTracker: NotificationEventTracker diff --git a/WordPress/Classes/Utility/Blogging Reminders/BloggingRemindersScheduler.swift b/WordPress/Classes/Utility/Blogging Reminders/BloggingRemindersScheduler.swift index 1dadb8d2c0f4..ea03ec22d95a 100644 --- a/WordPress/Classes/Utility/Blogging Reminders/BloggingRemindersScheduler.swift +++ b/WordPress/Classes/Utility/Blogging Reminders/BloggingRemindersScheduler.swift @@ -29,9 +29,7 @@ class BloggingRemindersScheduler { // MARK: - Error Handling enum Error: Swift.Error { - case cantRetrieveContainerForAppGroup(appGroupName: String) case needsPermissionForPushNotifications - case noPreviousScheduleAttempt } // MARK: - Schedule Data Containers diff --git a/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift b/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift index b5eb69331da8..8e5336fa6f94 100644 --- a/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift +++ b/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift @@ -8,11 +8,10 @@ enum FeatureFlag: Int, CaseIterable { case siteIconCreator case statsNewInsights case betaSiteDesigns - case personalizeHomeTab case commentModerationUpdate case compliancePopover - case domainFocus - case mediaModernization + case googleDomainsCard + case newTabIcons /// Returns a boolean indicating if the feature is enabled var enabled: Bool { @@ -33,16 +32,14 @@ enum FeatureFlag: Int, CaseIterable { return AppConfiguration.statsRevampV2Enabled case .betaSiteDesigns: return false - case .personalizeHomeTab: - return AppConfiguration.isJetpack case .commentModerationUpdate: return false case .compliancePopover: return true - case .domainFocus: - return true - case .mediaModernization: + case .googleDomainsCard: return false + case .newTabIcons: + return BuildConfiguration.current == .localDeveloper } } @@ -77,16 +74,14 @@ extension FeatureFlag { return "New Cards for Stats Insights" case .betaSiteDesigns: return "Fetch Beta Site Designs" - case .personalizeHomeTab: - return "Personalize Home Tab" case .commentModerationUpdate: return "Comments Moderation Update" case .compliancePopover: return "Compliance Popover" - case .domainFocus: - return "Domain Focus" - case .mediaModernization: - return "Media Modernization" + case .googleDomainsCard: + return "Google Domains Promotional Card" + case .newTabIcons: + return "New Tab Icons" } } } diff --git a/WordPress/Classes/Utility/BuildInformation/RemoteFeatureFlag.swift b/WordPress/Classes/Utility/BuildInformation/RemoteFeatureFlag.swift index c20d563f6ac4..078443398795 100644 --- a/WordPress/Classes/Utility/BuildInformation/RemoteFeatureFlag.swift +++ b/WordPress/Classes/Utility/BuildInformation/RemoteFeatureFlag.swift @@ -20,8 +20,10 @@ enum RemoteFeatureFlag: Int, CaseIterable { case contactSupportChatbot case jetpackSocialImprovements case domainManagement + case dynamicDashboardCards case plansInSiteCreation - case readerImprovements // pcdRpT-3Eb-p2 + case bloganuaryDashboardNudge // pcdRpT-4FE-p2 + case wordPressSotWCard var defaultValue: Bool { switch self { @@ -61,9 +63,13 @@ enum RemoteFeatureFlag: Int, CaseIterable { return AppConfiguration.isJetpack case .domainManagement: return false + case .dynamicDashboardCards: + return false case .plansInSiteCreation: return false - case .readerImprovements: + case .bloganuaryDashboardNudge: + return AppConfiguration.isJetpack + case .wordPressSotWCard: return true } } @@ -107,10 +113,14 @@ enum RemoteFeatureFlag: Int, CaseIterable { return "jetpack_social_improvements_v1" case .domainManagement: return "domain_management" + case .dynamicDashboardCards: + return "dynamic_dashboard_cards" case .plansInSiteCreation: return "plans_in_site_creation" - case .readerImprovements: - return "reader_improvements" + case .bloganuaryDashboardNudge: + return "bloganuary_dashboard_nudge" + case .wordPressSotWCard: + return "wp_sotw_2023_nudge" } } @@ -152,10 +162,14 @@ enum RemoteFeatureFlag: Int, CaseIterable { return "Jetpack Social Improvements v1" case .domainManagement: return "Domain Management" + case .dynamicDashboardCards: + return "Dynamic Dashboard Cards" case .plansInSiteCreation: return "Plans in Site Creation" - case .readerImprovements: - return "Reader Improvements v1" + case .bloganuaryDashboardNudge: + return "Bloganuary Dashboard Nudge" + case .wordPressSotWCard: + return "SoTW Nudge Card for WordPress App" } } diff --git a/WordPress/Classes/Utility/ContentCoordinator.swift b/WordPress/Classes/Utility/ContentCoordinator.swift index 99c4bd1fd2e7..1a1ced6ec2c5 100644 --- a/WordPress/Classes/Utility/ContentCoordinator.swift +++ b/WordPress/Classes/Utility/ContentCoordinator.swift @@ -19,7 +19,6 @@ protocol ContentCoordinator { struct DefaultContentCoordinator: ContentCoordinator { enum DisplayError: Error { case missingParameter - case unsupportedFeature case unsupportedType } diff --git a/WordPress/Classes/Utility/Editor/GutenbergSettings.swift b/WordPress/Classes/Utility/Editor/GutenbergSettings.swift index 1932117b4bc5..ad6751cab2e6 100644 --- a/WordPress/Classes/Utility/Editor/GutenbergSettings.swift +++ b/WordPress/Classes/Utility/Editor/GutenbergSettings.swift @@ -99,10 +99,6 @@ class GutenbergSettings { return database.bool(forKey: Key.showPhase2Dialog(forBlogURL: blog.url)) } - func setShowPhase2Dialog(_ showDialog: Bool, for blog: Blog) { - setShowPhase2Dialog(showDialog, forBlogURL: blog.url) - } - func setShowPhase2Dialog(_ showDialog: Bool, forBlogURL url: String?) { database.set(showDialog, forKey: Key.showPhase2Dialog(forBlogURL: url)) } diff --git a/WordPress/Classes/Utility/FormattableContent/FormattableRangesFactory.swift b/WordPress/Classes/Utility/FormattableContent/FormattableRangesFactory.swift index 6d798afb428f..47cf514d73ce 100644 --- a/WordPress/Classes/Utility/FormattableContent/FormattableRangesFactory.swift +++ b/WordPress/Classes/Utility/FormattableContent/FormattableRangesFactory.swift @@ -27,7 +27,6 @@ private enum RangeKeys { static let url = "url" static let indices = "indices" static let id = "id" - static let value = "value" static let siteId = "site_id" static let postId = "post_id" } diff --git a/WordPress/Classes/Utility/FormattableContent/FormattableTextContent.swift b/WordPress/Classes/Utility/FormattableContent/FormattableTextContent.swift index 112c0764a2f4..41267c6b027d 100644 --- a/WordPress/Classes/Utility/FormattableContent/FormattableTextContent.swift +++ b/WordPress/Classes/Utility/FormattableContent/FormattableTextContent.swift @@ -25,13 +25,3 @@ class FormattableTextContent: FormattableContent { self.ranges = ranges } } - -extension FormattableMediaItem { - fileprivate enum MediaKeys { - static let RawType = "type" - static let URL = "url" - static let Indices = "indices" - static let Width = "width" - static let Height = "height" - } -} diff --git a/WordPress/Classes/Utility/ImmuTableViewController.swift b/WordPress/Classes/Utility/ImmuTableViewController.swift index dfdb008dcae5..b69f7aa90289 100644 --- a/WordPress/Classes/Utility/ImmuTableViewController.swift +++ b/WordPress/Classes/Utility/ImmuTableViewController.swift @@ -26,15 +26,6 @@ extension ImmuTablePresenter where Self: UIViewController { } } -extension ImmuTablePresenter { - func prompt(_ controllerGenerator: @escaping (ImmuTableRow) -> T) -> ImmuTableAction where T: Confirmable { - return present({ - let controller = controllerGenerator($0) - return PromptViewController(controller) - }) - } -} - protocol ImmuTableController { var title: String { get } var immuTableRows: [ImmuTableRow.Type] { get } diff --git a/WordPress/Classes/Utility/Kanvas/KanvasCameraCustomUI.swift b/WordPress/Classes/Utility/Kanvas/KanvasCameraCustomUI.swift index 0e28c9a937b5..82c331a32aea 100644 --- a/WordPress/Classes/Utility/Kanvas/KanvasCameraCustomUI.swift +++ b/WordPress/Classes/Utility/Kanvas/KanvasCameraCustomUI.swift @@ -7,13 +7,9 @@ public class KanvasCustomUI { public static let shared = KanvasCustomUI() private static let brightBlue = UIColor.muriel(color: MurielColor(name: .blue)).color(for: UITraitCollection(userInterfaceStyle: .dark)) - private static let brightPurple = UIColor.muriel(color: MurielColor(name: .purple)).color(for: UITraitCollection(userInterfaceStyle: .dark)) private static let brightPink = UIColor.muriel(color: MurielColor(name: .pink)).color(for: UITraitCollection(userInterfaceStyle: .dark)) - private static let brightYellow = UIColor.muriel(color: MurielColor(name: .yellow)).color(for: UITraitCollection(userInterfaceStyle: .dark)) - private static let brightGreen = UIColor.muriel(color: MurielColor(name: .green)).color(for: UITraitCollection(userInterfaceStyle: .dark)) private static let brightRed = UIColor.muriel(color: MurielColor(name: .red)).color(for: UITraitCollection(userInterfaceStyle: .dark)) private static let brightOrange = UIColor.muriel(color: MurielColor(name: .orange)).color(for: UITraitCollection(userInterfaceStyle: .dark)) - private static let white = UIColor.white static private var firstPrimary: UIColor { return KanvasCustomUI.primaryColors.first ?? UIColor.blue @@ -107,72 +103,6 @@ public class KanvasCustomUI { } } -enum CustomKanvasFonts: CaseIterable { - case libreBaskerville - case nunitoBold - case pacifico - case oswaldUpper - case shrikhand - case spaceMonoBold - - struct Shadow { - let radius: CGFloat - let offset: CGPoint - let color: UIColor - } - - var name: String { - switch self { - case .libreBaskerville: - return "LibreBaskerville-Regular" - case .nunitoBold: - return "Nunito-Bold" - case .pacifico: - return "Pacifico-Regular" - case .oswaldUpper: - return "Oswald-Regular" - case .shrikhand: - return "Shrikhand-Regular" - case .spaceMonoBold: - return "SpaceMono-Bold" - } - } - - var size: Int { - switch self { - case .libreBaskerville: - return 20 - case .nunitoBold: - return 24 - case .pacifico: - return 24 - case .oswaldUpper: - return 22 - case .shrikhand: - return 22 - case .spaceMonoBold: - return 20 - } - } - - var shadow: Shadow? { - switch self { - case .libreBaskerville: - return nil - case .nunitoBold: - return Shadow(radius: 1, offset: CGPoint(x: 0, y: 2), color: UIColor.black.withAlphaComponent(75)) - case .pacifico: - return Shadow(radius: 5, offset: .zero, color: UIColor.white.withAlphaComponent(50)) - case .oswaldUpper: - return nil - case .shrikhand: - return Shadow(radius: 1, offset: CGPoint(x: 1, y: 2), color: UIColor.black.withAlphaComponent(75)) - case .spaceMonoBold: - return nil - } - } -} - extension UIFont { static func libreBaskerville(fontSize: CGFloat) -> UIFont { diff --git a/WordPress/Classes/Utility/Media/Blog+VideoLimits.swift b/WordPress/Classes/Utility/Media/Blog+VideoLimits.swift index 0cdf747514b8..cea566554c79 100644 --- a/WordPress/Classes/Utility/Media/Blog+VideoLimits.swift +++ b/WordPress/Classes/Utility/Media/Blog+VideoLimits.swift @@ -13,17 +13,6 @@ extension Blog { return Blog.maximumVideoDurationForFreeSites } - /// Returns `true` if the blog is allowed to upload the given asset. - func canUploadAsset(_ asset: WPMediaAsset) -> Bool { - guard asset.assetType() == .video else { - return true - } - guard let limit = videoDurationLimit else { - return true - } - return asset.duration() <= limit - } - /// Returns `true` if the blog is allowed to upload the video at the given URL. func canUploadVideo(from videoURL: URL) -> Bool { guard let limit = videoDurationLimit else { diff --git a/WordPress/Classes/Utility/Media/ImageDecoder.swift b/WordPress/Classes/Utility/Media/ImageDecoder.swift new file mode 100644 index 000000000000..cfdca0840cc9 --- /dev/null +++ b/WordPress/Classes/Utility/Media/ImageDecoder.swift @@ -0,0 +1,71 @@ +import Foundation + +enum ImageDecoder { + /// Returns an image created from the given URL. The image is decompressed. + /// Returns ``AnimatedImage`` the image is a GIF. + static func makeImage(from fileURL: URL) async throws -> UIImage { + let data = try Data(contentsOf: fileURL) + return try _makeImage(from: data, size: nil) + } + + /// Returns an image created from the given data. The image is decompressed. + /// Returns ``AnimatedImage`` the image is a GIF. + /// + /// - parameter size: The desired size of the thumbnail in pixels. + static func makeImage(from data: Data, size: CGSize? = nil) async throws -> UIImage { + try _makeImage(from: data, size: size) + } +} + +// Forces decompression (or bitmapping) to happen in the background. +// It's very expensive for some image formats, such as JPEG. +private func _makeImage(from data: Data, size: CGSize?) throws -> UIImage { + guard let image = UIImage(data: data) else { + throw URLError(.cannotDecodeContentData) + } + if data.isMatchingMagicNumbers(Data.gifMagicNumbers) { + return AnimatedImage(gifData: data) ?? image + } + if let size { + let size = aspectFillSize(imageSize: image.size.scaled(by: image.scale), targetSize: size) + return image.preparingThumbnail(of: size) ?? image + } + if isDecompressionNeeded(for: data) { + return image.preparingForDisplay() ?? image + } + return image +} + +private func aspectFillSize(imageSize: CGSize, targetSize: CGSize) -> CGSize { + // Scale image to fill the target size but avoid upscaling + let scale = min(1, max(targetSize.width / imageSize.width, targetSize.height / imageSize.height)) + return imageSize.scaled(by: scale).rounded() +} + +private func isDecompressionNeeded(for data: Data) -> Bool { + // This check is required to avoid the following error messages when + // using `preparingForDisplay`: + // + // [Decompressor] Error -17102 decompressing image -- possibly corrupt + // + // More info: https://github.com/SDWebImage/SDWebImage/issues/3365 + data.isMatchingMagicNumbers(Data.jpegMagicNumbers) +} + +private extension Data { + // JPEG magic numbers https://en.wikipedia.org/wiki/JPEG + static let jpegMagicNumbers: [UInt8] = [0xFF, 0xD8, 0xFF] + + // GIF magic numbers https://en.wikipedia.org/wiki/GIF + static let gifMagicNumbers: [UInt8] = [0x47, 0x49, 0x46] + + func isMatchingMagicNumbers(_ numbers: [UInt8?]) -> Bool { + guard self.count >= numbers.count else { + return false + } + return zip(numbers.indices, numbers).allSatisfy { index, number in + guard let number = number else { return true } + return self[index] == number + } + } +} diff --git a/WordPress/Classes/Utility/Media/ImageDownloader+Gravatar.swift b/WordPress/Classes/Utility/Media/ImageDownloader+Gravatar.swift index 99b94e5f229f..ef117d94c760 100644 --- a/WordPress/Classes/Utility/Media/ImageDownloader+Gravatar.swift +++ b/WordPress/Classes/Utility/Media/ImageDownloader+Gravatar.swift @@ -2,7 +2,7 @@ import Foundation extension ImageDownloader { - func downloadGravatarImage(with email: String, completion: @escaping (UIImage?) -> Void) { + nonisolated func downloadGravatarImage(with email: String, completion: @escaping (UIImage?) -> Void) { guard let url = Gravatar.gravatarUrl(for: email) else { completion(nil) diff --git a/WordPress/Classes/Utility/Media/ImageDownloader.swift b/WordPress/Classes/Utility/Media/ImageDownloader.swift index 5902967cfa3d..fc4634120c48 100644 --- a/WordPress/Classes/Utility/Media/ImageDownloader.swift +++ b/WordPress/Classes/Utility/Media/ImageDownloader.swift @@ -1,124 +1,153 @@ import UIKit -// MARK: - ImageDownloadTask protocol +struct ImageRequestOptions { + /// Resize the thumbnail to the given size. By default, `nil`. + var size: CGSize? -/// This protocol can be implemented to represent an image download task handled by the ImageDownloader. -/// -protocol ImageDownloaderTask { - /// Calling this method should cancel the task's execution. - /// - func cancel() + /// If enabled, uses ``MemoryCache`` for caching decompressed images. + var isMemoryCacheEnabled = true + + /// If enabled, uses `URLSession` preconfigured with a custom `URLCache` + /// with a relatively high disk capacity. By default, `true`. + var isDiskCacheEnabled = true } -extension Operation: ImageDownloaderTask {} -extension URLSessionTask: ImageDownloaderTask {} +/// The system that downloads and caches images, and prepares them for display. +actor ImageDownloader { + static let shared = ImageDownloader() -extension URLSession: ImageDownloaderTask { - func cancel() { - invalidateAndCancel() + private let cache: MemoryCacheProtocol + + private let urlSession = URLSession { + $0.urlCache = nil } -} -// MARK: - Image Downloading Tool + private let urlSessionWithCache = URLSession { + $0.urlCache = URLCache( + memoryCapacity: 32 * 1024 * 1024, // 32 MB + diskCapacity: 256 * 1024 * 1024, // 256 MB + diskPath: "org.automattic.ImageDownloader" + ) + } -class ImageDownloader { + init(cache: MemoryCacheProtocol = MemoryCache.shared) { + self.cache = cache + } - /// Shared Instance! - /// - static let shared = ImageDownloader() + // MARK: - Images (URL) - /// Internal URLSession Instance + /// Downloads image for the given `URL`. + func image(from url: URL, options: ImageRequestOptions = .init()) async throws -> UIImage { + var request = URLRequest(url: url) + request.addValue("image/*", forHTTPHeaderField: "Accept") + return try await image(from: request, options: options) + } + + /// Downloads image for the given `URLRequest`. + func image(from request: URLRequest, options: ImageRequestOptions = .init()) async throws -> UIImage { + let key = makeKey(for: request.url, size: options.size) + if options.isMemoryCacheEnabled, let image = cache[key] { + return image + } + let data = try await data(for: request, options: options) + let image = try await ImageDecoder.makeImage(from: data, size: options.size) + if options.isMemoryCacheEnabled { + cache[key] = image + } + return image + } + + // MARK: - Images (Blog) + + /// Returns image for the given URL authenticated for the given host. + func image(from imageURL: URL, host: MediaHost, options: ImageRequestOptions) async throws -> UIImage { + let request = try await authenticatedRequest(for: imageURL, host: host) + return try await image(from: request, options: options) + } + + /// Returns data for the given URL authenticated for the given host. + func data(from imageURL: URL, host: MediaHost, options: ImageRequestOptions) async throws -> Data { + let request = try await authenticatedRequest(for: imageURL, host: host) + return try await data(for: request, options: options) + } + + private func authenticatedRequest(for imageURL: URL, host: MediaHost) async throws -> URLRequest { + var request = try await MediaRequestAuthenticator() + .authenticatedRequest(for: imageURL, host: host) + request.setValue("image/*", forHTTPHeaderField: "Accept") + return request + } + + // MARK: - Caching + + /// Returns an image from the memory cache. /// - private let session = URLSession(configuration: .default) + /// - note: Use it to retrieve the image synchronously, which is no not possible + /// with the async functions. + nonisolated func cachedImage(for imageURL: URL, size: CGSize? = nil) -> UIImage? { + cache[makeKey(for: imageURL, size: size)] + } + nonisolated func setCachedImage(_ image: UIImage?, for imageURL: URL, size: CGSize? = nil) { + cache[makeKey(for: imageURL, size: size)] = image + } - deinit { - session.invalidateAndCancel() + private nonisolated func makeKey(for imageURL: URL?, size: CGSize?) -> String { + guard let imageURL else { + assertionFailure("The request.url was nil") // This should never happen + return "" + } + return imageURL.absoluteString + (size.map { "?size=\($0)" } ?? "") } - /// Downloads image for the given URL. - func image(at url: URL) async throws -> UIImage { - var request = URLRequest(url: url) - request.addValue("image/*", forHTTPHeaderField: "Accept") - return try await image(for: request) + + // MARK: - Networking + + private func data(for request: URLRequest, options: ImageRequestOptions) async throws -> Data { + let session = options.isDiskCacheEnabled ? urlSessionWithCache : urlSession + let (data, response) = try await session.data(for: request) + try validate(response: response) + return data } - /// Downloads image for the given request. - func image(for request: URLRequest) async throws -> UIImage { - final class CancelationToken { - var task: ImageDownloaderTask? + private func validate(response: URLResponse) throws { + guard let response = response as? HTTPURLResponse else { + return // The request was made not over HTTP, e.g. a `file://` request } - let token = CancelationToken() - return try await withTaskCancellationHandler { - try await withUnsafeThrowingContinuation { continuation in - token.task = downloadImage(for: request) { image, error in - if let image { - continuation.resume(returning: image) - } else { - continuation.resume(throwing: error ?? URLError(.unknown)) - } - } - } - } onCancel: { - token.task?.cancel() + guard (200..<400).contains(response.statusCode) else { + throw ImageDownloaderError.unacceptableStatusCode(response.statusCode) } } +} - /// Downloads the UIImage resource at the specified URL. On completion the received closure will be executed. - /// +// MARK: - ImageDownloader (Closures) + +extension ImageDownloader { @discardableResult - func downloadImage(at url: URL, completion: @escaping (UIImage?, Error?) -> Void) -> ImageDownloaderTask { + nonisolated func downloadImage(at url: URL, completion: @escaping (UIImage?, Error?) -> Void) -> ImageDownloaderTask { var request = URLRequest(url: url) request.addValue("image/*", forHTTPHeaderField: "Accept") - return downloadImage(for: request, completion: completion) } - /// Downloads the UIImage resource at the specified endpoint. On completion the received closure will be executed. - /// @discardableResult - func downloadImage(for request: URLRequest, completion: @escaping (UIImage?, Error?) -> Void) -> ImageDownloaderTask { - let task = session.dataTask(with: request) { (data, _, error) in - guard let data = data else { - if let error = error { - completion(nil, error) - } else { - completion(nil, ImageDownloaderError.failed) - } - return - } - - if let gif = self.makeGIF(with: data, request: request) { - completion(gif, nil) - } else if let image = UIImage.init(data: data) { + nonisolated func downloadImage(for request: URLRequest, completion: @escaping (UIImage?, Error?) -> Void) -> ImageDownloaderTask { + let task = Task { + do { + let image = try await self.image(from: request, options: .init()) completion(image, nil) - } else { - completion(nil, ImageDownloaderError.failed) + } catch { + completion(nil, error) } } - - task.resume() - return task - } - - private func makeGIF(with data: Data, request: URLRequest) -> AnimatedImageWrapper? { - guard let url = request.url, url.pathExtension.lowercased() == "gif" else { - return nil - } - - return AnimatedImageWrapper(gifData: data) + return AnonymousImageDownloadTask(closure: task.cancel) } } -// MARK: - AnimatedImageWrapper +// MARK: - AnimatedImage -/// This is a wrapper around `RCTAnimatedImage` that allows including extra information -/// to better render the gifs in text views. -/// -/// This class uses the RCTAnimatedImage to verify the image is a valid gif which is why I'm still -/// using that here. -class AnimatedImageWrapper: UIImage { - var gifData: Data? = nil - var targetSize: CGSize? = nil +final class AnimatedImage: UIImage { + private(set) var gifData: Data? + var targetSize: CGSize? private static let playbackStrategy: GIFPlaybackStrategy = LargeGIFPlaybackStrategy() @@ -136,8 +165,31 @@ class AnimatedImageWrapper: UIImage { } } -// MARK: - Error Types -// +// MARK: - Helpers + +protocol ImageDownloaderTask { + func cancel() +} + +extension Operation: ImageDownloaderTask {} +extension URLSessionTask: ImageDownloaderTask {} + +private struct AnonymousImageDownloadTask: ImageDownloaderTask { + let closure: () -> Void + + func cancel() { + closure() + } +} + enum ImageDownloaderError: Error { - case failed + case unacceptableStatusCode(_ statusCode: Int?) +} + +private extension URLSession { + convenience init(_ conifgure: (URLSessionConfiguration) -> Void) { + let configuration = URLSessionConfiguration.default + conifgure(configuration) + self.init(configuration: configuration) + } } diff --git a/WordPress/Classes/Utility/Media/ImageLoader.swift b/WordPress/Classes/Utility/Media/ImageLoader.swift index d56a48557117..4b57d1089f8e 100644 --- a/WordPress/Classes/Utility/Media/ImageLoader.swift +++ b/WordPress/Classes/Utility/Media/ImageLoader.swift @@ -32,32 +32,35 @@ import AutomatticTracks // MARK: Private Fields private unowned let imageView: CachedAnimatedImageView - private let loadingIndicator: CircularProgressView + private let loadingIndicator: ActivityIndicatorType private var successHandler: ImageLoaderSuccessBlock? private var errorHandler: ImageLoaderFailureBlock? private var placeholder: UIImage? private var selectedPhotonQuality: UInt = Constants.defaultPhotonQuality - private lazy var assetRequestOptions: PHImageRequestOptions = { - let requestOptions = PHImageRequestOptions() - requestOptions.resizeMode = .fast - requestOptions.deliveryMode = .opportunistic - requestOptions.isNetworkAccessAllowed = true - return requestOptions - }() + @objc convenience init(imageView: CachedAnimatedImageView, gifStrategy: GIFStrategy = .mediumGIFs) { + self.init(imageView: imageView, gifStrategy: gifStrategy, loadingIndicator: nil) + } - @objc init(imageView: CachedAnimatedImageView, gifStrategy: GIFStrategy = .mediumGIFs) { + init(imageView: CachedAnimatedImageView, gifStrategy: GIFStrategy = .mediumGIFs, loadingIndicator: ActivityIndicatorType?) { self.imageView = imageView imageView.gifStrategy = gifStrategy - loadingIndicator = CircularProgressView(style: .primary) + + if let loadingIndicator { + self.loadingIndicator = loadingIndicator + } else { + let loadingIndicator = CircularProgressView(style: .primary) + loadingIndicator.backgroundColor = .clear + self.loadingIndicator = loadingIndicator + } super.init() - WPStyleGuide.styleProgressViewWhite(loadingIndicator) - imageView.addLoadingIndicator(loadingIndicator, style: .fullView) + imageView.addLoadingIndicator(self.loadingIndicator, style: .fullView) } + /// Removes the gif animation and prevents it from animate again. /// Call this in a table/collection cell's `prepareForReuse()`. /// @@ -298,105 +301,17 @@ import AutomatticTracks } if self.imageView.shouldShowLoadingIndicator { - self.loadingIndicator.state = .error + (self.loadingIndicator as? CircularProgressView)?.state = .error } self.errorHandler?(error) } } - - private func createError(description: String, key: String = NSLocalizedFailureReasonErrorKey) -> NSError { - let userInfo = [key: description] - return NSError(domain: ImageLoader.classNameWithoutNamespaces(), code: 0, userInfo: userInfo) - } } // MARK: - Loading Media object extension ImageLoader { - @objc(loadImageFromMedia:preferredSize:placeholder:isBlogAtomic:success:error:) - /// Load an image from the given Media object. If it's a gif, it will animate it. - /// For any other type of media, this will load the corresponding static image. - /// - /// - Parameters: - /// - media: The media object - /// - placeholder: A placeholder to show while the image is loading. - /// - size: The preferred size of the image to load. - /// - isBlogAtomic: Whether the blog associated to the media item is Atomic or not - /// - success: A closure to be called if the image was loaded successfully. - /// - error: A closure to be called if there was an error loading the image. - /// - func loadImage(media: Media, - preferredSize size: CGSize = .zero, - placeholder: UIImage?, - isBlogAtomic: Bool, - success: ImageLoaderSuccessBlock?, - error: ImageLoaderFailureBlock?) { - guard let mediaId = media.mediaID?.stringValue else { - let error = createError(description: "The Media id doesn't exist") - callErrorHandler(with: error) - return - } - - self.placeholder = placeholder - successHandler = success - errorHandler = error - - guard let url = url(from: media) else { - if media.remoteStatus == .stub { - MediaThumbnailCoordinator.shared.fetchStubMedia(for: media) { [weak self] (fetchedMedia, fetchedMediaError) in - if let fetchedMedia = fetchedMedia, - let fetchedMediaId = fetchedMedia.mediaID?.stringValue, fetchedMediaId == mediaId { - DispatchQueue.main.async { - self?.loadImage(media: fetchedMedia, preferredSize: size, placeholder: placeholder, isBlogAtomic: isBlogAtomic, success: success, error: error) - } - } else { - self?.callErrorHandler(with: fetchedMediaError) - } - } - } else { - let error = NSError(domain: NSURLErrorDomain, code: NSURLErrorBadURL, userInfo: nil) - callErrorHandler(with: error) - } - - return - } - - if url.isGif { - let host = MediaHost(with: media.blog, isAtomic: isBlogAtomic) { error in - // We'll log the error, so we know it's there, but we won't halt execution. - WordPressAppDelegate.crashLogging?.logError(error) - } - - loadGif(with: url, from: host, preferredSize: size) - } else if imageView.image == nil { - imageView.clean() - loadImage(from: media, preferredSize: size) - } - } - - private func loadImage(from media: Media, preferredSize size: CGSize) { - imageView.image = placeholder - imageView.startLoadingAnimation() - media.image(with: size) { [weak self] (image, error) in - if let image = image { - self?.imageView.image = image - self?.callSuccessHandler() - } else { - self?.callErrorHandler(with: error) - } - } - } - - private func url(from media: Media) -> URL? { - if let localUrl = media.absoluteLocalURL { - return localUrl - } else if let urlString = media.remoteURL, let remoteUrl = URL(string: urlString) { - return remoteUrl - } - return nil - } - private func getPhotonUrl(for url: URL, size: CGSize) -> URL? { var finalSize = size if url.isGif { @@ -411,109 +326,6 @@ extension ImageLoader { } } -// MARK: - Loading PHAsset object - -extension ImageLoader { - - @objc(loadImageFromPHAsset:preferredSize:placeholder:success:error:) - /// Load an image from the given PHAsset object. If it's a gif, it will animate it. - /// For any other type of media, this will load the corresponding static image. - /// - /// - Parameters: - /// - asset: The PHAsset object - /// - placeholder: A placeholder to show while the image is loading. - /// - size: The preferred size of the image to load. - /// - success: A closure to be called if the image was loaded successfully. - /// - error: A closure to be called if there was an error loading the image. - /// - func loadImage(asset: PHAsset, preferredSize size: CGSize = .zero, placeholder: UIImage?, success: ImageLoaderSuccessBlock?, error: ImageLoaderFailureBlock?) { - self.placeholder = placeholder - successHandler = success - errorHandler = error - - guard asset.assetType() == .image else { - let error = self.createError(description: ErrorDescriptions.phassetIsNotImage) - callErrorHandler(with: error) - return - } - - if asset.utTypeIdentifier() == UTType.gif.identifier { - loadGif(from: asset) - } else { - loadImage(from: asset, preferredSize: size) - } - } - - private func loadGif(from asset: PHAsset) { - imageView.image = placeholder - imageView.startLoadingAnimation() - - PHImageManager.default().requestImageDataAndOrientation(for: asset, - options: assetRequestOptions, - resultHandler: { [weak self] (data, str, orientation, info) -> Void in - guard info?[PHImageErrorKey] == nil else { - var error: NSError? - if let phImageError = info?[PHImageErrorKey] as? NSError { - let userInfo = [NSUnderlyingErrorKey: phImageError] - error = NSError(domain: ImageLoader.classNameWithoutNamespaces(), code: 0, userInfo: userInfo) - } else { - error = self?.createError(description: ErrorDescriptions.phassetGenericError) - } - self?.callErrorHandler(with: error) - return - } - - guard let data = data else { - let error = self?.createError(description: ErrorDescriptions.phassetReturnedDataIsEmpty) - self?.callErrorHandler(with: error) - return - } - - self?.imageView.setAnimatedImage(data, success: { - self?.callSuccessHandler() - }) - }) - } - - private func loadImage(from asset: PHAsset, preferredSize size: CGSize) { - imageView.image = placeholder - imageView.startLoadingAnimation() - - var optimizedSize: CGSize = size - if optimizedSize == .zero { - // When using a zero size, default to the maximum screen dimension. - let screenSize = UIScreen.main.bounds - let screenSizeMax = max(screenSize.width, screenSize.height) - optimizedSize = CGSize(width: screenSizeMax, height: screenSizeMax) - } - - PHImageManager.default().requestImage(for: asset, - targetSize: optimizedSize, - contentMode: .aspectFill, - options: assetRequestOptions) { [weak self] (image, info) in - guard info?[PHImageErrorKey] == nil else { - var error: NSError? - if let phImageError = info?[PHImageErrorKey] as? NSError { - let userInfo = [NSUnderlyingErrorKey: phImageError] - error = NSError(domain: ImageLoader.classNameWithoutNamespaces(), code: 0, userInfo: userInfo) - } else { - error = self?.createError(description: ErrorDescriptions.phassetGenericError) - } - self?.callErrorHandler(with: error) - return - } - guard let image = image else { - let error = self?.createError(description: ErrorDescriptions.phassetReturnedDataIsEmpty) - self?.callErrorHandler(with: error) - return - } - - self?.imageView.image = image - self?.callSuccessHandler() - } - } -} - // MARK: - Constants private extension ImageLoader { @@ -522,10 +334,4 @@ private extension ImageLoader { static let maxPhotonQuality: UInt = 100 static let defaultPhotonQuality: UInt = 80 } - - enum ErrorDescriptions { - static let phassetIsNotImage: String = "Error in \(ImageLoader.classNameWithoutNamespaces()): the provided PHAsset is not an image." - static let phassetReturnedDataIsEmpty: String = "Error in \(ImageLoader.classNameWithoutNamespaces()): no data returned for provided PHAsset." - static let phassetGenericError: String = "Error in \(ImageLoader.classNameWithoutNamespaces()): PHAsset could not be retrieved." - } } diff --git a/WordPress/Classes/Utility/Media/ImageView.swift b/WordPress/Classes/Utility/Media/ImageView.swift new file mode 100644 index 000000000000..ded1ddfa47fc --- /dev/null +++ b/WordPress/Classes/Utility/Media/ImageView.swift @@ -0,0 +1,135 @@ +import UIKit +import Gifu + +/// A simple image view that supports rendering both static and animated images +/// (see ``AnimatedImage``). +@MainActor +final class ImageView: UIView { + let imageView = GIFImageView() + + private var errorView: UIImageView? + private var spinner: UIActivityIndicatorView? + private let downloader: ImageDownloader = .shared + private var task: Task? + + enum LoadingStyle { + case background + case spinner + } + + var loadingStyle = LoadingStyle.background + + override init(frame: CGRect) { + super.init(frame: frame) + + addSubview(imageView) + imageView.translatesAutoresizingMaskIntoConstraints = false + pinSubviewToAllEdges(imageView) + + imageView.clipsToBounds = true + imageView.contentMode = .scaleAspectFill + + backgroundColor = .secondarySystemBackground + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + task?.cancel() + } + + func prepareForReuse() { + task?.cancel() + task = nil + + if imageView.isAnimatingGIF { + imageView.prepareForReuse() + } else { + imageView.image = nil + } + } + + // MARK: - Sources + + func setImage(with imageURL: URL, size: CGSize? = nil) { + task?.cancel() + + if let image = downloader.cachedImage(for: imageURL, size: size) { + setState(.success(image)) + } else { + setState(.loading) + task = Task { [downloader, weak self] in + do { + let options = ImageRequestOptions(size: size) + let image = try await downloader.image(from: imageURL, options: options) + guard !Task.isCancelled else { return } + self?.setState(.success(image)) + } catch { + guard !Task.isCancelled else { return } + self?.setState(.failure) + } + } + } + } + + // MARK: - State + + enum State { + case loading + case success(UIImage) + case failure + } + + func setState(_ state: State) { + imageView.isHidden = true + errorView?.isHidden = true + spinner?.stopAnimating() + + switch state { + case .loading: + switch loadingStyle { + case .background: + backgroundColor = .secondarySystemBackground + case .spinner: + makeSpinner().startAnimating() + } + case .success(let image): + if let gif = image as? AnimatedImage, let data = gif.gifData { + imageView.animate(withGIFData: data) + } else { + imageView.image = image + } + imageView.isHidden = false + backgroundColor = .clear + case .failure: + makeErrorView().isHidden = false + } + } + + private func makeSpinner() -> UIActivityIndicatorView { + if let spinner { + return spinner + } + let spinner = UIActivityIndicatorView() + addSubview(spinner) + spinner.translatesAutoresizingMaskIntoConstraints = false + pinSubviewAtCenter(spinner) + self.spinner = spinner + return spinner + } + + private func makeErrorView() -> UIImageView { + if let errorView { + return errorView + } + let errorView = UIImageView(image: UIImage(systemName: "exclamationmark.triangle")) + errorView.tintColor = .separator + addSubview(errorView) + errorView.translatesAutoresizingMaskIntoConstraints = false + pinSubviewAtCenter(errorView) + self.errorView = errorView + return errorView + } +} diff --git a/WordPress/Classes/Utility/Media/ItemProviderMediaExporter.swift b/WordPress/Classes/Utility/Media/ItemProviderMediaExporter.swift index 97fb5766967d..da373f1e9846 100644 --- a/WordPress/Classes/Utility/Media/ItemProviderMediaExporter.swift +++ b/WordPress/Classes/Utility/Media/ItemProviderMediaExporter.swift @@ -38,6 +38,9 @@ final class ItemProviderMediaExporter: MediaExporter { func processGIF(at url: URL) throws { let pixelSize = url.pixelSize let media = MediaExport(url: url, fileSize: url.fileSize, width: pixelSize.width, height: pixelSize.height, duration: nil) + let exportProgress = Progress(totalUnitCount: 1) + exportProgress.completedUnitCount = 1 + progress.addChild(exportProgress, withPendingUnitCount: MediaExportProgressUnits.halfDone) onCompletion(media) } diff --git a/WordPress/Classes/Utility/Media/MediaAssetExporter.swift b/WordPress/Classes/Utility/Media/MediaAssetExporter.swift deleted file mode 100644 index 7850765f62fe..000000000000 --- a/WordPress/Classes/Utility/Media/MediaAssetExporter.swift +++ /dev/null @@ -1,253 +0,0 @@ -import Foundation -import MobileCoreServices -import UniformTypeIdentifiers -import AVFoundation -import Photos - -/// Media export handling of PHAssets -/// -class MediaAssetExporter: MediaExporter { - - var mediaDirectoryType: MediaDirectory = .uploads - - var imageOptions: MediaImageExporter.Options? - var videoOptions: MediaVideoExporter.Options? - - var allowableFileExtensions = Set() - - public enum AssetExportError: MediaExportError { - case unsupportedPHAssetMediaType - case expectedPHAssetImageType - case expectedPHAssetVideoType - case expectedPHAssetGIFType - case failedLoadingPHImageManagerRequest - case unavailablePHAssetImageResource - case unavailablePHAssetVideoResource - case failedRequestingVideoExportSession - - public var errorDescription: String? { description } - - var description: String { - switch self { - case .unsupportedPHAssetMediaType: - return NSLocalizedString("The item could not be added to the Media Library.", comment: "Message shown when an asset failed to load while trying to add it to the Media library.") - case .expectedPHAssetImageType, - .failedLoadingPHImageManagerRequest, - .unavailablePHAssetImageResource: - return NSLocalizedString("The image could not be added to the Media Library.", comment: "Message shown when an image failed to load while trying to add it to the Media library.") - case .expectedPHAssetVideoType, - .unavailablePHAssetVideoResource, - .failedRequestingVideoExportSession: - return NSLocalizedString("The video could not be added to the Media Library.", comment: "Message shown when a video failed to load while trying to add it to the Media library.") - case .expectedPHAssetGIFType: - return NSLocalizedString("The GIF could not be added to the Media Library.", comment: "Message shown when a GIF failed to load while trying to add it to the Media library.") - } - } - } - - /// Default shared instance of the PHImageManager - /// - fileprivate lazy var imageManager = { - return PHImageManager.default() - }() - - let asset: PHAsset - - init(asset: PHAsset) { - self.asset = asset - } - - @discardableResult public func export(onCompletion: @escaping OnMediaExport, onError: @escaping (MediaExportError) -> Void) -> Progress { - switch asset.mediaType { - case .image: - return exportImage(forAsset: asset, onCompletion: onCompletion, onError: onError) - case .video: - return exportVideo(forAsset: asset, onCompletion: onCompletion, onError: onError) - default: - onError(AssetExportError.unsupportedPHAssetMediaType) - } - return Progress.discreteCompletedProgress() - } - - @discardableResult fileprivate func exportImage(forAsset asset: PHAsset, onCompletion: @escaping OnMediaExport, onError: @escaping (MediaExportError) -> Void) -> Progress { - - guard asset.mediaType == .image else { - onError(exporterErrorWith(error: AssetExportError.expectedPHAssetImageType)) - return Progress.discreteCompletedProgress() - } - var filename = UUID().uuidString + ".jpg" - var resourceAvailableLocally = false - // Get the resource matching the type, to export. - let resources = PHAssetResource.assetResources(for: asset).filter({ $0.type == .photo }) - if let resource = resources.first { - resourceAvailableLocally = true - filename = resource.originalFilename - if UTType(resource.uniformTypeIdentifier) == .gif { - // Since this is a GIF, handle the export in it's own way. - return exportGIF(forAsset: asset, resource: resource, onCompletion: onCompletion, onError: onError) - } - } - - // Configure the options for requesting the image. - let options = PHImageRequestOptions() - options.version = .current - options.deliveryMode = .highQualityFormat - options.resizeMode = .exact - options.isNetworkAccessAllowed = true - // If we have a resource object that means we have a local copy of the asset so we can request the image in sync mode. - options.isSynchronous = resourceAvailableLocally - let progress = Progress.discreteProgress(totalUnitCount: MediaExportProgressUnits.done) - progress.isCancellable = true - options.progressHandler = { (progressValue, error, stop, info) in - progress.completedUnitCount = Int64(progressValue * Double(MediaExportProgressUnits.halfDone)) - if progress.isCancelled { - stop.pointee = true - } - } - - // Configure an error handler for the image request. - let onImageRequestError: (Error?) -> Void = { (error) in - guard let error = error else { - onError(AssetExportError.failedLoadingPHImageManagerRequest) - return - } - onError(self.exporterErrorWith(error: error)) - } - - // Request the image. - imageManager.requestImageDataAndOrientation(for: asset, - options: options, - resultHandler: { (data, uti, orientation, info) in - progress.completedUnitCount = MediaExportProgressUnits.halfDone - guard let imageData = data else { - onImageRequestError(info?[PHImageErrorKey] as? Error) - return - } - // Hand off the image export to a shared image writer. - let exporter = MediaImageExporter(data: imageData, filename: filename, typeHint: uti) - exporter.mediaDirectoryType = self.mediaDirectoryType - if let options = self.imageOptions { - exporter.options = options - if options.exportImageType == nil, let utiToUse = uti { - exporter.options.exportImageType = self.preferedExportTypeFor(uti: utiToUse) - } - } - let exportProgress = exporter.export(onCompletion: { (imageExport) in - onCompletion(imageExport) - }, onError: onError) - progress.addChild(exportProgress, withPendingUnitCount: MediaExportProgressUnits.halfDone) - }) - return progress - } - - func preferedExportTypeFor(uti: String) -> String? { - guard !self.allowableFileExtensions.isEmpty, - let extensionType = UTType(uti)?.preferredFilenameExtension else { - return nil - } - if allowableFileExtensions.contains(extensionType) { - return uti - } else { - return UTType.jpeg.identifier - } - } - - /// Exports and writes an asset's video data to a local Media URL. - /// - /// - parameter onCompletion: Called on successful export, with the local file URL of the exported asset. - /// - parameter onError: Called if an error was encountered during export. - /// - fileprivate func exportVideo(forAsset asset: PHAsset, onCompletion: @escaping OnMediaExport, onError: @escaping OnExportError) -> Progress { - guard asset.mediaType == .video else { - onError(exporterErrorWith(error: AssetExportError.expectedPHAssetVideoType)) - return Progress.discreteCompletedProgress() - } - // Get the resource matching the type, to export. - let resources = PHAssetResource.assetResources(for: asset).filter({ $0.type == .video }) - guard let videoResource = resources.first else { - onError(exporterErrorWith(error: AssetExportError.unavailablePHAssetVideoResource)) - return Progress.discreteCompletedProgress() - } - - // Configure a video exporter to handle an export session. - let exporterVideoOptions = videoOptions ?? MediaVideoExporter.Options() - let originalFilename = videoResource.originalFilename - - // Request an export session, which may take time to download the complete video data. - let options = PHVideoRequestOptions() - options.isNetworkAccessAllowed = true - let progress = Progress.discreteProgress(totalUnitCount: MediaExportProgressUnits.done) - progress.isCancellable = true - options.progressHandler = { (progressValue, error, stop, info) in - progress.completedUnitCount = Int64(progressValue * Double(MediaExportProgressUnits.halfDone)) - if progress.isCancelled { - stop.pointee = true - } - } - imageManager.requestExportSession(forVideo: asset, - options: options, - exportPreset: exporterVideoOptions.exportPreset, - resultHandler: { (session, info) -> Void in - progress.completedUnitCount = MediaExportProgressUnits.halfDone - guard let session = session else { - if let error = info?[PHImageErrorKey] as? Error { - onError(self.exporterErrorWith(error: error)) - } else { - onError(AssetExportError.failedRequestingVideoExportSession) - } - return - } - let videoExporter = MediaVideoExporter(session: session, filename: originalFilename) - videoExporter.options = exporterVideoOptions - videoExporter.mediaDirectoryType = self.mediaDirectoryType - let exportProgress = videoExporter.export(onCompletion: { (videoExport) in - onCompletion(videoExport) - }, - onError: onError) - progress.addChild(exportProgress, withPendingUnitCount: MediaExportProgressUnits.halfDone) - }) - return progress - } - - /// Exports and writes an asset's GIF data to a local Media URL. - /// - /// - parameter onCompletion: Called on successful export, with the local file URL of the exported asset. - /// - parameter onError: Called if an error was encountered during export. - /// - fileprivate func exportGIF(forAsset asset: PHAsset, resource: PHAssetResource, onCompletion: @escaping OnMediaExport, onError: @escaping OnExportError) -> Progress { - - guard UTType(resource.uniformTypeIdentifier) == .gif else { - onError(exporterErrorWith(error: AssetExportError.expectedPHAssetGIFType)) - return Progress.discreteCompletedProgress() - } - let url: URL - do { - url = try mediaFileManager.makeLocalMediaURL(withFilename: resource.originalFilename, - fileExtension: "gif") - } catch { - onError(exporterErrorWith(error: error)) - return Progress.discreteCompletedProgress() - } - let options = PHAssetResourceRequestOptions() - options.isNetworkAccessAllowed = true - let progress = Progress.discreteProgress(totalUnitCount: MediaExportProgressUnits.done) - progress.isCancellable = false - let manager = PHAssetResourceManager.default() - manager.writeData(for: resource, - toFile: url, - options: options, - completionHandler: { (error) in - progress.completedUnitCount = progress.totalUnitCount - if let error = error { - onError(self.exporterErrorWith(error: error)) - return - } - onCompletion(MediaExport(url: url, - fileSize: url.fileSize, - width: url.pixelSize.width, - height: url.pixelSize.height, - duration: 0)) - }) - return progress - } -} diff --git a/WordPress/Classes/Utility/Media/MediaExternalExporter.swift b/WordPress/Classes/Utility/Media/MediaExternalExporter.swift index ef9f3b87b53c..0f5110566d91 100644 --- a/WordPress/Classes/Utility/Media/MediaExternalExporter.swift +++ b/WordPress/Classes/Utility/Media/MediaExternalExporter.swift @@ -1,17 +1,5 @@ import Foundation -/// A media asset protocol used to export media from external sources -/// -protocol MediaExternalAsset { - - /// The external URL - var URL: URL { get } - /// Asset name - var name: String { get } - // Caption - var caption: String { get } -} - enum MediaExternalExporterError: MediaExportError { case unknownError @@ -45,20 +33,20 @@ class MediaExternalExporter: MediaExporter { var mediaDirectoryType: MediaDirectory = .uploads - let asset: MediaExternalAsset + let asset: ExternalMediaAsset - init(externalAsset: MediaExternalAsset) { + init(externalAsset: ExternalMediaAsset) { asset = externalAsset } /// Downloads and export the external media asset /// func export(onCompletion: @escaping OnMediaExport, onError: @escaping OnExportError) -> Progress { - if asset.URL.isGif { - return downloadGif(from: asset.URL, onCompletion: onCompletion, onError: onError) + if asset.largeURL.isGif { + return downloadGif(from: asset.largeURL, onCompletion: onCompletion, onError: onError) } - WPImageSource.shared().downloadImage(for: asset.URL, withSuccess: { (image) in + WPImageSource.shared().downloadImage(for: asset.largeURL, withSuccess: { (image) in self.imageDownloaded(image: image, error: nil, onCompletion: onCompletion, onError: onError) }) { (error) in self.imageDownloaded(image: nil, error: error, onCompletion: onCompletion, onError: onError) diff --git a/WordPress/Classes/Utility/Media/MediaImageExporter.swift b/WordPress/Classes/Utility/Media/MediaImageExporter.swift index b3d4d8a1d452..16a8beca5b13 100644 --- a/WordPress/Classes/Utility/Media/MediaImageExporter.swift +++ b/WordPress/Classes/Utility/Media/MediaImageExporter.swift @@ -277,6 +277,10 @@ class MediaImageExporter: MediaExporter { /// Write a given image source, succeeds unless an error is thrown, returns the resulting properties if available. /// func writeImageSource(_ source: CGImageSource) throws -> WriteResultProperties { + if (sourceUTType as String) == UTType.gif.identifier && (CGImageSourceGetType(source) as? String) == UTType.gif.identifier { + return try writeAnimatedImageSource(source) + } + // Create the destination with the URL, or error guard let destination = CGImageDestinationCreateWithURL(url as CFURL, sourceUTType, 1, nil) else { throw ImageExportError.imageSourceDestinationWithURLFailed @@ -344,5 +348,28 @@ class MediaImageExporter: MediaExporter { return WriteResultProperties(width: width, height: height) } + + private func writeAnimatedImageSource(_ source: CGImageSource) throws -> WriteResultProperties { + guard let destination = CGImageDestinationCreateWithURL(url as CFURL, sourceUTType, CGImageSourceGetCount(source), nil) else { + throw ImageExportError.imageSourceDestinationWithURLFailed + } + var options: [NSString: Any] = [:] + if let maximumSize { + options[kCGImageSourceCreateThumbnailWithTransform] = true + options[kCGImageSourceCreateThumbnailFromImageAlways] = true + options[kCGImageSourceThumbnailMaxPixelSize] = maximumSize + } + var outputSize: CGSize? + for i in 0.. Progress { - let exporter = MediaImageExporter(image: image, filename: UUID().uuidString) - exporter.mediaDirectoryType = .temporary - exporter.options = imageExporterOptions - return exporter.export(onCompletion: { (export) in - self.exportImageToThumbnailCache(export, onCompletion: onCompletion, onError: onError) - }, onError: onError) - } - /// Export a known video at the URL, being either a file URL or a remote URL. /// @discardableResult func exportThumbnail(forVideoURL url: URL, onCompletion: @escaping OnThumbnailExport, onError: @escaping OnExportError) -> Progress { @@ -303,21 +292,6 @@ extension MediaThumbnailExporter { token.progress?.cancel() } } - - func exportThumbnail(forImage image: UIImage) async throws -> (ThumbnailIdentifier, MediaExport) { - let token = MediaExportCancelationToken() - return try await withTaskCancellationHandler { - try await withUnsafeThrowingContinuation { continuation in - token.progress = exportThumbnail(forImage: image, onCompletion: { - continuation.resume(returning: ($0, $1)) - }, onError: { - continuation.resume(throwing: $0) - }) - } - } onCancel: { - token.progress?.cancel() - } - } } private final class MediaExportCancelationToken { diff --git a/WordPress/Classes/Utility/Media/MediaVideoExporter.swift b/WordPress/Classes/Utility/Media/MediaVideoExporter.swift index 9d14fc5482d5..391c795b056f 100644 --- a/WordPress/Classes/Utility/Media/MediaVideoExporter.swift +++ b/WordPress/Classes/Utility/Media/MediaVideoExporter.swift @@ -3,7 +3,7 @@ import MobileCoreServices import UniformTypeIdentifiers import AVFoundation -/// Media export handling of Videos from PHAssets or AVAssets. +/// Media export handling of Videos from AVAssets. /// class MediaVideoExporter: MediaExporter { diff --git a/WordPress/Classes/ViewRelated/Media/MemoryCache.swift b/WordPress/Classes/Utility/Media/MemoryCache.swift similarity index 81% rename from WordPress/Classes/ViewRelated/Media/MemoryCache.swift rename to WordPress/Classes/Utility/Media/MemoryCache.swift index 760735624cf4..b5f87074b93b 100644 --- a/WordPress/Classes/ViewRelated/Media/MemoryCache.swift +++ b/WordPress/Classes/Utility/Media/MemoryCache.swift @@ -1,9 +1,12 @@ import Foundation import AlamofireImage import WordPressUI -import SDWebImage -final class MemoryCache { +protocol MemoryCacheProtocol: AnyObject { + subscript(key: String) -> UIImage? { get set } +} + +final class MemoryCache: MemoryCacheProtocol { /// A shared image cache used by the entire system. static let shared = MemoryCache() @@ -21,8 +24,21 @@ final class MemoryCache { // MARK: - UIImage + subscript(key: String) -> UIImage? { + get { + getImage(forKey: key) + } + set { + if let newValue { + setImage(newValue, forKey: key) + } else { + removeImage(forKey: key) + } + } + } + func setImage(_ image: UIImage, forKey key: String) { - cache.setObject(image, forKey: key as NSString, cost: Int(image.sd_memoryCost)) + cache.setObject(image, forKey: key as NSString, cost: image.cost) } func getImage(forKey key: String) -> UIImage? { @@ -48,6 +64,15 @@ final class MemoryCache { } } +private extension UIImage { + /// Returns a rought estimation of how much space the image takes in memory. + var cost: Int { + let dataCost = (self as? AnimatedImage)?.gifData?.count ?? 0 + let imageCost = cgImage.map { $0.bytesPerRow * $0.height } ?? 0 + return dataCost + imageCost + } +} + extension MemoryCache { /// Registers the cache with all the image loading systems used by the app. func register() { diff --git a/WordPress/Classes/Utility/Migration/ContentMigrationCoordinator.swift b/WordPress/Classes/Utility/Migration/ContentMigrationCoordinator.swift index 69f36c3fa6db..d9da11a7da68 100644 --- a/WordPress/Classes/Utility/Migration/ContentMigrationCoordinator.swift +++ b/WordPress/Classes/Utility/Migration/ContentMigrationCoordinator.swift @@ -76,9 +76,11 @@ return } + let hasBlogs = AccountHelper.hasBlogs + guard isLocalPostsSynced() else { let error = MigrationError.localDraftsNotSynced - tracker.trackContentExportFailed(reason: error.localizedDescription) + tracker.trackContentExportFailed(reason: error.localizedDescription, hasBlogs: hasBlogs) processResult(.failure(error), completion: completion) return } @@ -86,12 +88,12 @@ dataMigrator.exportData { [weak self] result in switch result { case .success: - self?.tracker.trackContentExportSucceeded() + self?.tracker.trackContentExportSucceeded(hasBlogs: hasBlogs) self?.processResult(.success(()), completion: completion) case .failure(let error): DDLogError("[Jetpack Migration] Error exporting data: \(error)") - self?.tracker.trackContentExportFailed(reason: error.localizedDescription) + self?.tracker.trackContentExportFailed(reason: error.localizedDescription, hasBlogs: hasBlogs) self?.processResult(.failure(.exportFailure), completion: completion) } } @@ -187,7 +189,7 @@ protocol ContentMigrationEligibilityProvider { extension AppConfiguration: ContentMigrationEligibilityProvider { var isEligibleForMigration: Bool { - Self.isWordPress && AccountHelper.isLoggedIn && AccountHelper.hasBlogs + Self.isWordPress && AccountHelper.isLoggedIn } } diff --git a/WordPress/Classes/Utility/Networking/RequestAuthenticator.swift b/WordPress/Classes/Utility/Networking/RequestAuthenticator.swift index 2a77e064c6e5..490c310f5d74 100644 --- a/WordPress/Classes/Utility/Networking/RequestAuthenticator.swift +++ b/WordPress/Classes/Utility/Networking/RequestAuthenticator.swift @@ -10,10 +10,6 @@ import Foundation /// class RequestAuthenticator: NSObject { - enum Error: Swift.Error { - case atomicSiteWithoutDotComID(blog: Blog) - } - enum DotComAuthenticationType { case regular case regularMapped(siteID: Int) diff --git a/WordPress/Classes/Utility/PageTree.swift b/WordPress/Classes/Utility/PageTree.swift new file mode 100644 index 000000000000..ea0a58fa91b4 --- /dev/null +++ b/WordPress/Classes/Utility/PageTree.swift @@ -0,0 +1,252 @@ +final class PageTree { + + // A node in a tree, which of course is also a tree itself. + private class TreeNode { + struct PageData { + var postID: NSNumber? + var parentID: NSNumber? + } + let pageID: TaggedManagedObjectID + let pageData: PageData + var children = [TreeNode]() + var parentNode: TreeNode? + + init(page: Page, children: [TreeNode] = [], parentNode: TreeNode? = nil) { + self.pageID = TaggedManagedObjectID(page) + self.pageData = PageData(postID: page.postID, parentID: page.parentID) + self.children = children + self.parentNode = parentNode + } + + // The `PageTree` type is used to loaded + // Some page There are pages They are pages that doesn't belong to the root level, but their parent pages haven't been loaded yet. + var isOrphan: Bool { + (pageData.parentID?.int64Value ?? 0) > 0 && parentNode == nil + } + + func dfsList(in context: NSManagedObjectContext) throws -> [Page] { + var pages = [Page]() + _ = try depthFirstSearch { level, node in + let page = try context.existingObject(with: node.pageID) + page.hierarchyIndex = level + page.hasVisibleParent = node.parentNode != nil + pages.append(page) + return false + } + return pages + } + + /// Perform depth-first search starting with the current (`self`) node. + /// + /// - Parameter closure: A closure that takes a node and its level in the page tree as arguments and returns + /// a boolean value indicate whether the search should be stopped. + /// - Returns: `true` if search has been stopped by the closure. + @discardableResult + func depthFirstSearch(using closure: (Int, TreeNode) throws -> Bool) rethrows -> Bool { + try depthFirstSearch(level: 0, using: closure) + } + + private func depthFirstSearch(level: Int, using closure: (Int, TreeNode) throws -> Bool) rethrows -> Bool { + let shouldStop = try closure(level, self) + if shouldStop { + return true + } + + for child in children { + let shouldStop = try child.depthFirstSearch(level: level + 1, using: closure) + if shouldStop { + return true + } + } + + return false + } + + /// Perform breadth-first search starting with the current (`self`) node. + /// + /// - Parameter closure: A closure that takes a node as argument and returns a boolean value indicate whether + /// the search should be stopped. + /// - Returns: `true` if search has been stopped by the closure. + func breadthFirstSearch(using closure: (TreeNode) -> Bool) { + var queue = [TreeNode]() + queue.append(self) + while let current = queue.popLast() { + let shouldStop = closure(current) + if shouldStop { + break + } + + queue.append(contentsOf: current.children) + } + } + + func add(_ newNodes: [TreeNode], parentID: NSNumber) -> Bool { + assert(parentID != 0) + + return depthFirstSearch { _, node in + if node.pageData.postID == parentID { + node.children.append(contentsOf: newNodes) + newNodes.forEach { $0.parentNode = node } + return true + } + return false + } + } + } + + // The top level (or root level) pages, or nodes. + // They can be two types node: + // - child nodes. They are top level pages. + // - orphan nodes. They are pages that doesn't belong to the root level, but their parent pages haven't been loaded yet. + private var nodes = [TreeNode]() + + // `orphanNodes` contains indexes of orphan nodes in the `nodes` array (the value part in the dictionary), which are + // grouped using their parent id (the key part in the dictionary). + // IMPORTANT: Make sure `orphanNodes` is up-to-date after the `nodes` array is modified. + private var orphanNodes = [NSNumber: [Int]]() + + /// Add *new pages* to the page tree. + /// + /// This function assumes none of array elements already exists in the current page tree. + func add(_ newPages: [Page]) { + let newNodes = newPages.map { TreeNode(page: $0) } + relocateOrphans(to: newNodes) + + // First try to constrcuture a smaller subtree from the given pages, then move the new subtree to the existing + // page tree (`self`). + // The number of pages in a subtree can be changed if we want to futher tweak the performance. + let batch = 100 + for index in stride(from: 0, to: newNodes.count, by: batch) { + let tree = PageTree() + tree.add(Array(newNodes[index..) -> [NSNumber: TreeNode] { + guard !originalIDs.isEmpty else { + return [:] + } + + var ids = originalIDs + var result = [NSNumber: TreeNode]() + + // The new node is not at the root level, find its parent in the root level nodes. + for child in nodes { + if ids.isEmpty { + break + } + + // Using BFS under the assumption that page tree in most sites is a shallow tree, where most pages are in top layers. + child.breadthFirstSearch { node in + let postID = node.pageData.postID ?? 0 + let foundIndex = ids.firstIndex(of: postID) + if let foundIndex { + ids.remove(at: foundIndex) + result[postID] = node + } + return ids.isEmpty + } + } + + return result + } + + func hierarchyList(in context: NSManagedObjectContext) throws -> [Page] { + try nodes.reduce(into: []) { + try $0.append(contentsOf: $1.dfsList(in: context)) + } + } + + static func hierarchyList(of pages: [Page]) throws -> [Page] { + guard let context = pages.first?.managedObjectContext else { + return [] + } + + let tree = PageTree() + tree.add(pages) + return try tree.hierarchyList(in: context) + } +} diff --git a/WordPress/Classes/Utility/PushNotificationsManager.swift b/WordPress/Classes/Utility/PushNotificationsManager.swift index ee24f7d720ae..84be3f3dc2d9 100644 --- a/WordPress/Classes/Utility/PushNotificationsManager.swift +++ b/WordPress/Classes/Utility/PushNotificationsManager.swift @@ -424,7 +424,6 @@ extension PushNotificationsManager { static let originKey = "origin" static let badgeResetValue = "badge-reset" static let local = "qs-local-notification" - static let bloggingPrompts = "blogging-prompts-notification" } enum Tracking { diff --git a/WordPress/Classes/Utility/ReachabilityUtils+OnlineActions.swift b/WordPress/Classes/Utility/ReachabilityUtils+OnlineActions.swift index 1c0f5221014a..c12cc113fbc9 100644 --- a/WordPress/Classes/Utility/ReachabilityUtils+OnlineActions.swift +++ b/WordPress/Classes/Utility/ReachabilityUtils+OnlineActions.swift @@ -12,6 +12,9 @@ extension ReachabilityUtils { /// Performs the action when an internet connection is available /// If no internet connection is available an error message is displayed /// + /// - warning: Do not use it as it can't reliably identify as the connection + /// is reachable or not and can significantly lag behind the actual + /// connectivity status. @objc class func onAvailableInternetConnectionDo(_ action: () -> Void) { guard ReachabilityUtils.isInternetReachable() else { WPError.showAlert(withTitle: DefaultNoConnectionMessage.title, message: DefaultNoConnectionMessage.message) diff --git a/WordPress/Classes/Utility/StringRankedSearch.swift b/WordPress/Classes/Utility/StringRankedSearch.swift new file mode 100644 index 000000000000..9d217b6f2e1d --- /dev/null +++ b/WordPress/Classes/Utility/StringRankedSearch.swift @@ -0,0 +1,103 @@ +import Foundation + +struct StringRankedSearch { + /// By default, `[.caseInsensitive, .diacriticInsensitive]`. + var options: String.CompareOptions = [.caseInsensitive, .diacriticInsensitive] + + private let term: String + private let terms: [String] + + init(searchTerm: String) { + self.term = searchTerm.trimmingCharacters(in: .whitespaces) + self.terms = term.components(separatedBy: .whitespaces).filter { !$0.isEmpty } + } + + /// Returns a score in a `0.0...1.0` range where `1.0` is maximum confidence. + /// Anything above `0.5` suggests a good probability of a match. + func score(for string: String?) -> Double { + guard let string else { + return 0 + } + let words = string + .trimmingCharacters(in: .whitespaces) + .components(separatedBy: .whitespaces) + .filter { !$0.isEmpty } + guard !words.isEmpty else { + return 0 + } + var score = 0.0 + var matchIndices: Set = [] + for term in terms { + // Get the maximum score for each word. There is no penalty for a + // position of the word in the input string. + let match = words.enumerated() + .map { (index, word) in (index: index, score: self.score(for: word, term: term)) } + .max { $0.score < $1.score }! + score += match.score + matchIndices.insert(match.index) + } + let bonusForDistanceBetweenMatches = self.bonusForDistanceBetweenMatches(matchIndices.sorted(), words: words) + let bonusForLengthMatch = score * (Double(min(string.count, term.count)) / Double(max(string.count, term.count))) + let bonusForCountMatch = score * (Double(min(terms.count, words.count)) / Double(max(terms.count, words.count))) + return (0.9 * (score / Double(terms.count))) + + (0.05 * bonusForDistanceBetweenMatches) + + (0.025 * bonusForLengthMatch) * + (0.025 * bonusForCountMatch) + } + + private func bonusForDistanceBetweenMatches(_ indices: [Int], words: [String]) -> Double { + let distance = zip(indices.dropLast(), indices.dropFirst()) + .map { $1 - $0 } + .reduce(0, +) + return 1.0 - (Double(distance) / Double(words.count)) + } + + // Returns score in a `0.0...1.0` range. + private func score(for input: String, term: String) -> Double { + guard !input.isEmpty else { + return 0 + } + let score = fuzzyScore(for: input, term: term) / Double(term.count) + let bonusForLengthMatch = score * (Double(min(input.count, term.count)) / Double(max(input.count, term.count))) + let bonusForTermLength = term.count > 3 ? 0.1 : (term.count > 2 ? 0.05 : 0.0) + return (0.8 * score) + (0.1 * bonusForLengthMatch) + bonusForTermLength + } + + // Returns score in a `0.0...1.0` range. + private func fuzzyScore(for input: String, term: String) -> Double { + var score = 0.0 + var inputIndex = input.startIndex + var termIndex = term.startIndex + + func findNextMatch() -> Range? { + // Look for a perfect match first + if let range = input[inputIndex...].range(of: term[termIndex...], options: options) { + return range // Found these characters in a row + } + return input[inputIndex...].range(of: String(term[termIndex]), options: options) + } + + while termIndex < term.endIndex, inputIndex < input.endIndex, let range = findNextMatch() { + var matchIndex = range.lowerBound + while matchIndex < range.upperBound { + if matchIndex == inputIndex { + score += 0.9 // Bonus: matches the position exactly + } else if term[termIndex].isLetter != input[input.index(before: matchIndex)].isLetter { + score += 0.8 // Bonus: letter followed by non-letter or the other way around + } + if input[matchIndex] == term[termIndex] { + score += 0.1 // Bonus: exact match, including case and diacritics + } + termIndex = term.index(after: termIndex) + matchIndex = input.index(after: matchIndex) + inputIndex = matchIndex + } + inputIndex = range.upperBound + } + + guard term.distance(from: termIndex, to: term.endIndex) < 2 else { + return 0 // Too many misses + } + return score + } +} diff --git a/WordPress/Classes/Utility/Universal Links/Route.swift b/WordPress/Classes/Utility/Universal Links/Route.swift index a17391cca58e..2be5f73392ae 100644 --- a/WordPress/Classes/Utility/Universal Links/Route.swift +++ b/WordPress/Classes/Utility/Universal Links/Route.swift @@ -91,7 +91,7 @@ extension Route { /// enum DeepLinkSource: Equatable { case link - case banner + case banner(campaign: String? = nil) case email(campaign: String) case widget case lockScreenWidget @@ -123,6 +123,8 @@ enum DeepLinkSource: Equatable { switch self { case .email(let campaign): return campaign + case .banner(let campaign): + return campaign default: return nil } diff --git a/WordPress/Classes/Utility/Universal Links/Routes+Banners.swift b/WordPress/Classes/Utility/Universal Links/Routes+Banners.swift index 14f4408c840c..08f5a67a44ff 100644 --- a/WordPress/Classes/Utility/Universal Links/Routes+Banners.swift +++ b/WordPress/Classes/Utility/Universal Links/Routes+Banners.swift @@ -12,7 +12,7 @@ import Foundation struct AppBannerRoute: Route { let path = "/get" let section: DeepLinkSection? = nil - let source: DeepLinkSource = .banner + let source: DeepLinkSource = .banner() let shouldTrack: Bool = false let jetpackPowered: Bool = false @@ -28,15 +28,34 @@ extension AppBannerRoute: NavigationAction { return } + let campaign = AppBannerCampaign.getCampaign(from: values) + // Convert the fragment into a URL and ask the link router to handle // it like a normal route. var components = URLComponents() components.scheme = "https" components.host = "wordpress.com" components.path = fragment + if let campaign { + components.queryItems = [ + URLQueryItem(name: "campaign", value: campaign) + ] + } if let url = components.url { - router.handle(url: url, shouldTrack: true, source: .banner) + router.handle(url: url, shouldTrack: true, source: .banner(campaign: campaign)) + } + } +} + +enum AppBannerCampaign: String { + case qrCodeMedia = "qr-code-media" + + static func getCampaign(from values: [String: String]) -> String? { + guard let url = values[MatchedRouteURLComponentKey.url.rawValue], + let queryItems = URLComponents(string: url)?.queryItems else { + return nil } + return queryItems.first(where: { $0.name == "campaign" })?.value } } diff --git a/WordPress/Classes/Utility/Universal Links/Routes+Me.swift b/WordPress/Classes/Utility/Universal Links/Routes+Me.swift index 87258b3c84f9..9216bffc5f00 100644 --- a/WordPress/Classes/Utility/Universal Links/Routes+Me.swift +++ b/WordPress/Classes/Utility/Universal Links/Routes+Me.swift @@ -7,6 +7,13 @@ struct MeRoute: Route { let jetpackPowered: Bool = false } +struct MeAllDomainsRoute: Route { + let path = "/domains/manage" + let section: DeepLinkSection? = .me + let action: NavigationAction = MeNavigationAction.allDomains + let jetpackPowered: Bool = true +} + struct MeAccountSettingsRoute: Route { let path = "/me/account" let section: DeepLinkSection? = .me @@ -25,6 +32,7 @@ enum MeNavigationAction: NavigationAction { case root case accountSettings case notificationSettings + case allDomains func perform(_ values: [String: String] = [:], source: UIViewController? = nil, router: LinkRouter) { switch self { @@ -34,6 +42,8 @@ enum MeNavigationAction: NavigationAction { RootViewCoordinator.sharedPresenter.navigateToAccountSettings() case .notificationSettings: RootViewCoordinator.sharedPresenter.switchNotificationsTabToNotificationSettings() + case .allDomains: + RootViewCoordinator.sharedPresenter.navigateToAllDomains() } } } diff --git a/WordPress/Classes/Utility/Universal Links/Routes+MySites.swift b/WordPress/Classes/Utility/Universal Links/Routes+MySites.swift index f8ae48a68001..4c12fcff4a3b 100644 --- a/WordPress/Classes/Utility/Universal Links/Routes+MySites.swift +++ b/WordPress/Classes/Utility/Universal Links/Routes+MySites.swift @@ -66,9 +66,17 @@ extension MySitesRoute: Route { extension MySitesRoute: NavigationAction { func perform(_ values: [String: String], source: UIViewController? = nil, router: LinkRouter) { let coordinator = RootViewCoordinator.sharedPresenter.mySitesCoordinator + let campaign = AppBannerCampaign.getCampaign(from: values) guard let blog = blog(from: values) else { - WPAppAnalytics.track(.deepLinkFailed, withProperties: ["route": path]) + var properties: [AnyHashable: Any] = [ + "route": path, + "error": "invalid_site_id" + ] + if let campaign { + properties["campaign"] = campaign + } + WPAppAnalytics.track(.deepLinkFailed, withProperties: properties) if failAndBounce(values) == false { coordinator.showRootViewController() @@ -84,7 +92,11 @@ extension MySitesRoute: NavigationAction { case .posts: coordinator.showPosts(for: blog) case .media: - coordinator.showMedia(for: blog) + if campaign.flatMap(AppBannerCampaign.init) == .qrCodeMedia { + coordinator.showMediaPicker(for: blog) + } else { + coordinator.showMedia(for: blog) + } case .comments: coordinator.showComments(for: blog) case .sharing: diff --git a/WordPress/Classes/Utility/Universal Links/UniversalLinkRouter.swift b/WordPress/Classes/Utility/Universal Links/UniversalLinkRouter.swift index a1ed4e922c46..510899c684ef 100644 --- a/WordPress/Classes/Utility/Universal Links/UniversalLinkRouter.swift +++ b/WordPress/Classes/Utility/Universal Links/UniversalLinkRouter.swift @@ -45,6 +45,7 @@ struct UniversalLinkRouter: LinkRouter { static let meRoutes: [Route] = [ MeRoute(), MeAccountSettingsRoute(), + MeAllDomainsRoute(), MeNotificationSettingsRoute() ] diff --git a/WordPress/Classes/Utility/WPContentSearchHelper.swift b/WordPress/Classes/Utility/WPContentSearchHelper.swift deleted file mode 100644 index fb5f80dfeec2..000000000000 --- a/WordPress/Classes/Utility/WPContentSearchHelper.swift +++ /dev/null @@ -1,82 +0,0 @@ -import UIKit - -/// WPContentSearchHelper is a helper class to encapsulate searches over a time interval. -/// -/// The helper is configured with an interval and callback to call once the time elapses. -/// The helper automatically cancels pending callbacks when a new text update is incurred. -class WPContentSearchHelper: NSObject { - - /// The current searchText set on the helper. - @objc var searchText: String? = nil - - // MARK: - Methods for configuring the timing of search callbacks. - - fileprivate var observers = [WPContentSearchObserver]() - fileprivate let defaultDeferredSearchObservationInterval = TimeInterval(0.30) - - @objc func configureImmediateSearch(_ handler: @escaping ()->Void) { - let observer = WPContentSearchObserver() - observer.interval = 0.0 - observer.completion = handler - observers.append(observer) - } - - /// Add a search callback configured as a common deferred search. - @objc func configureDeferredSearch(_ handler: @escaping ()->Void) { - let observer = WPContentSearchObserver() - observer.interval = defaultDeferredSearchObservationInterval - observer.completion = handler - observers.append(observer) - } - - /// Remove any current configuration, such as local and remote search callbacks. - @objc func resetConfiguration() { - stopAllObservers() - observers.removeAll() - } - - // MARK: - Methods for updating the search. - - /// Update the current search text, ideally in real-time along with user input. - @objc func searchUpdated(_ text: String?) { - stopAllObservers() - searchText = text - for observer in observers { - let timer = Timer.init(timeInterval: observer.interval, - target: observer, - selector: #selector(WPContentSearchObserver.timerFired), - userInfo: nil, - repeats: false) - RunLoop.main.add(timer, forMode: RunLoop.Mode.common) - } - } - - /// Cancel the current search and any pending callbacks. - @objc func searchCanceled() { - stopAllObservers() - } - - // MARK: - Private Methods - - /// Stop the observers from firing. - fileprivate func stopAllObservers() { - for observer in observers { - observer.timer?.invalidate() - observer.timer = nil - } - } -} - -// MARK: - Private Classes - -/// Object encapsulating the callback and timing information. -private class WPContentSearchObserver: NSObject { - - @objc var interval = TimeInterval(0.0) - @objc var timer: Timer? - @objc var completion = {} - - @objc func timerFired() { - completion() - } -} diff --git a/WordPress/Classes/Utility/WPStyleGuide+WebView.h b/WordPress/Classes/Utility/WPStyleGuide+WebView.h index b4993eff66cd..de253a52ce62 100644 --- a/WordPress/Classes/Utility/WPStyleGuide+WebView.h +++ b/WordPress/Classes/Utility/WPStyleGuide+WebView.h @@ -1,6 +1,5 @@ #import - - +#import #pragma mark - WebViewController Styles diff --git a/WordPress/Classes/Utility/WebKitViewController.swift b/WordPress/Classes/Utility/WebKitViewController.swift index f08b8e343ee3..338825ad1b8a 100644 --- a/WordPress/Classes/Utility/WebKitViewController.swift +++ b/WordPress/Classes/Utility/WebKitViewController.swift @@ -220,6 +220,9 @@ class WebKitViewController: UIViewController, WebKitAuthenticatable { webView.customUserAgent = WPUserAgent.wordPress() webView.navigationDelegate = self webView.uiDelegate = self + if #available(iOS 16.4, *) { + webView.isInspectable = true + } loadWebViewRequest() @@ -513,8 +516,10 @@ class WebKitViewController: UIViewController, WebKitAuthenticatable { assertionFailure("Observed change to web view that we are not handling") } - // Set the title for the HUD which shows up on tap+hold w/ accessibile font sizes enabled - navigationItem.title = "\(titleView.titleLabel.text ?? "")\n\n\(String(describing: titleView.subtitleLabel.text ?? ""))" + if customTitle == nil { + // Set the title for the HUD which shows up on tap+hold w/ accessible font sizes enabled + navigationItem.title = "\(titleView.titleLabel.text ?? "")\n\n\(String(describing: titleView.subtitleLabel.text ?? ""))" + } // Accessibility values which emulate those found in Safari navigationItem.accessibilityLabel = NSLocalizedString("Title", comment: "Accessibility label for web page preview title") diff --git a/WordPress/Classes/Utility/WebViewControllerFactory.swift b/WordPress/Classes/Utility/WebViewControllerFactory.swift index cce60657d323..654bb0323d96 100644 --- a/WordPress/Classes/Utility/WebViewControllerFactory.swift +++ b/WordPress/Classes/Utility/WebViewControllerFactory.swift @@ -45,10 +45,13 @@ class WebViewControllerFactory: NSObject { return controller(configuration: configuration, source: source) } - static func controllerWithDefaultAccountAndSecureInteraction(url: URL, source: String) -> WebKitViewController { + static func controllerWithDefaultAccountAndSecureInteraction(url: URL, + source: String, + title: String? = nil) -> WebKitViewController { let configuration = WebViewControllerConfiguration(url: url) configuration.authenticateWithDefaultAccount() configuration.secureInteraction = true + configuration.customTitle = title return controller(configuration: configuration, source: source) } diff --git a/WordPress/Classes/ViewRelated/Activity/ActivityDateFormatting.swift b/WordPress/Classes/ViewRelated/Activity/ActivityDateFormatting.swift index d580b0b00270..63b887a9342b 100644 --- a/WordPress/Classes/ViewRelated/Activity/ActivityDateFormatting.swift +++ b/WordPress/Classes/ViewRelated/Activity/ActivityDateFormatting.swift @@ -29,6 +29,6 @@ struct ActivityDateFormatting { return TimeZone(secondsFromGMT: 0)! } - return blog.timeZone + return blog.timeZone ?? TimeZone.current } } diff --git a/WordPress/Classes/ViewRelated/Activity/ActivityListViewModel.swift b/WordPress/Classes/ViewRelated/Activity/ActivityListViewModel.swift index 74f0e61434a0..65181c36ae9a 100644 --- a/WordPress/Classes/ViewRelated/Activity/ActivityListViewModel.swift +++ b/WordPress/Classes/ViewRelated/Activity/ActivityListViewModel.swift @@ -25,7 +25,6 @@ class ActivityListViewModel: Observable { private(set) var before: Date? private(set) var selectedGroups: [ActivityGroup] = [] - var errorViewModel: NoResultsViewController.Model? private(set) var refreshing = false { didSet { if refreshing != oldValue { diff --git a/WordPress/Classes/ViewRelated/Activity/BaseActivityListViewController.swift b/WordPress/Classes/ViewRelated/Activity/BaseActivityListViewController.swift index 5567bfee0ca3..a227643b5647 100644 --- a/WordPress/Classes/ViewRelated/Activity/BaseActivityListViewController.swift +++ b/WordPress/Classes/ViewRelated/Activity/BaseActivityListViewController.swift @@ -429,16 +429,6 @@ extension BaseActivityListViewController: ActivityPresenter { } } -// MARK: - Restores handling - -extension BaseActivityListViewController { - - fileprivate func restoreSiteToRewindID(_ rewindID: String) { - navigationController?.popToViewController(self, animated: true) - store.actionDispatcher.dispatch(ActivityAction.rewind(site: site, rewindID: rewindID)) - } -} - // MARK: - NoResults Handling private extension BaseActivityListViewController { diff --git a/WordPress/Classes/ViewRelated/Activity/FormattableContent/FormattableActivity.swift b/WordPress/Classes/ViewRelated/Activity/FormattableContent/FormattableActivity.swift index b125f5a69505..969dd9e0c74c 100644 --- a/WordPress/Classes/ViewRelated/Activity/FormattableContent/FormattableActivity.swift +++ b/WordPress/Classes/ViewRelated/Activity/FormattableContent/FormattableActivity.swift @@ -3,7 +3,6 @@ class FormattableActivity { let activity: Activity private let formatter = FormattableContentFormatter() - private var cachedContentGroup: FormattableContentGroup? = nil private var contentGroup: FormattableContentGroup? { guard let content = activity.content as? [String: AnyObject], content.isEmpty == false else { diff --git a/WordPress/Classes/ViewRelated/Activity/FormattableContent/Ranges/ActivityPluginRange.swift b/WordPress/Classes/ViewRelated/Activity/FormattableContent/Ranges/ActivityPluginRange.swift index 8635a6cb9346..73c79274292f 100644 --- a/WordPress/Classes/ViewRelated/Activity/FormattableContent/Ranges/ActivityPluginRange.swift +++ b/WordPress/Classes/ViewRelated/Activity/FormattableContent/Ranges/ActivityPluginRange.swift @@ -17,9 +17,4 @@ class ActivityPluginRange: ActivityRange { private var urlString: String { return "https://wordpress.com/plugins/\(pluginSlug)/\(siteSlug)" } - - private static func urlWith(pluginSlug: String, siteSlug: String) -> URL? { - let urlString = "https://wordpress.com/plugins/\(pluginSlug)/\(siteSlug)" - return URL(string: urlString) - } } diff --git a/WordPress/Classes/ViewRelated/Activity/WPStyleGuide+Activity.swift b/WordPress/Classes/ViewRelated/Activity/WPStyleGuide+Activity.swift index cc281d7a36dc..5faa20bae5d2 100644 --- a/WordPress/Classes/ViewRelated/Activity/WPStyleGuide+Activity.swift +++ b/WordPress/Classes/ViewRelated/Activity/WPStyleGuide+Activity.swift @@ -24,39 +24,16 @@ extension WPStyleGuide { .foregroundColor: UIColor.text] } - public static func gravatarPlaceholderImage() -> UIImage { - return gravatar - } - public static func summaryRegularStyle() -> [NSAttributedString.Key: Any] { return [.paragraphStyle: summaryParagraph, .font: summaryRegularFont, .foregroundColor: UIColor.text] } - public static func summaryBoldStyle() -> [NSAttributedString.Key: Any] { - return [.paragraphStyle: summaryParagraph, - .font: summaryBoldFont, - .foregroundColor: UIColor.text] - } - - public static func timestampStyle() -> [NSAttributedString.Key: Any] { - return [.font: timestampFont, - .foregroundColor: UIColor.textSubtle] - } - public static func backgroundColor() -> UIColor { return .listForeground } - public static func backgroundDiscardedColor() -> UIColor { - return .neutral(.shade5) - } - - public static func backgroundRewindableColor() -> UIColor { - return .primaryLight - } - public static func getGridiconTypeForActivity(_ activity: Activity) -> GridiconType? { return stringToGridiconTypeMapping[activity.gridicon] } @@ -104,20 +81,10 @@ extension WPStyleGuide { return WPStyleGuide.fontForTextStyle(.body, symbolicTraits: .traitItalic) } - fileprivate static let gravatar = UIImage(named: "gravatar")! - - private static var timestampFont: UIFont { - return WPStyleGuide.fontForTextStyle(.caption1) - } - private static var summaryRegularFont: UIFont { return WPStyleGuide.fontForTextStyle(.footnote) } - private static var summaryBoldFont: UIFont { - return WPStyleGuide.fontForTextStyle(.footnote, fontWeight: .semibold) - } - private static var summaryLineSize: CGFloat { return WPStyleGuide.fontSizeForTextStyle(.footnote) * 1.3 } diff --git a/WordPress/Classes/ViewRelated/Aztec/ViewControllers/AztecPostViewController.swift b/WordPress/Classes/ViewRelated/Aztec/ViewControllers/AztecPostViewController.swift index 754e1f0d2171..6f7476dbb537 100644 --- a/WordPress/Classes/ViewRelated/Aztec/ViewControllers/AztecPostViewController.swift +++ b/WordPress/Classes/ViewRelated/Aztec/ViewControllers/AztecPostViewController.swift @@ -7,12 +7,13 @@ import Gridicons import WordPressShared import MobileCoreServices import WordPressEditor -import WPMediaPicker import AVKit import MobileCoreServices import AutomatticTracks import MediaEditor import UniformTypeIdentifiers +import Photos +import PhotosUI // MARK: - Aztec's Native Editor! // @@ -109,6 +110,8 @@ class AztecPostViewController: UIViewController, PostEditor { case expectedSecondaryAction = 1 } + private var toolbarMode: FormatBarMode = .text + /// The editor view. /// fileprivate(set) lazy var editorView: Aztec.EditorView = { @@ -369,18 +372,6 @@ class AztecPostViewController: UIViewController, PostEditor { /// fileprivate var activeMediaRequests = [ImageDownloaderTask]() - /// Media Library Data Source - /// - lazy var mediaLibraryDataSource: MediaLibraryPickerDataSource = { - let dataSource = MediaLibraryPickerDataSource(post: self.post) - dataSource.ignoreSyncErrors = true - return dataSource - }() - - /// Device Photo Library Data Source - /// - fileprivate lazy var devicePhotoLibraryDataSource = WPPHAssetDataSource() - fileprivate let mediaCoordinator = MediaCoordinator.shared /// Media Progress View @@ -442,30 +433,19 @@ class AztecPostViewController: UIViewController, PostEditor { UIAccessibility.isVoiceOverRunning } - fileprivate var mediaPickerInputViewController: WPInputMediaPickerViewController? + private var mediaPickerInputViewController: PHPickerViewController? + private var selectedPickerResults: [PHPickerResult] = [] fileprivate var originalLeadingBarButtonGroup = [UIBarButtonItemGroup]() fileprivate var originalTrailingBarButtonGroup = [UIBarButtonItemGroup]() - /// The view to show when media picker has no assets to show. - /// - fileprivate let noResultsView = NoResultsViewController.controller() - - fileprivate var mediaLibraryChangeObserverKey: NSObjectProtocol? = nil - - /// Presents whatever happens when FormatBar's more button is selected /// fileprivate lazy var moreCoordinator: AztecMediaPickingCoordinator = { return AztecMediaPickingCoordinator(delegate: self) }() - - /// Helps choosing the correct view controller for previewing a media asset - /// - private var mediaPreviewHelper: MediaPreviewHelper? = nil - private let database: KeyValueDatabase = UserDefaults.standard private enum Key { static let classicDeprecationNoticeHasBeenShown = "kClassicDeprecationNoticeHasBeenShown" @@ -536,7 +516,6 @@ class AztecPostViewController: UIViewController, PostEditor { configureNavigationBar() configureView() configureSubviews() - noResultsView.configureForNoAssets(userCanUploadMedia: false) // UI elements might get their properties reset when the view is effectively loaded. Refresh it all! refreshInterface() @@ -912,44 +891,6 @@ class AztecPostViewController: UIViewController, PostEditor { navigationBarManager.reloadPublishButton() } - fileprivate func updateSearchBar(mediaPicker: WPMediaPickerViewController) { - let isSearching = mediaLibraryDataSource.searchQuery?.count ?? 0 != 0 - let hasAssets = mediaLibraryDataSource.totalAssetCount > 0 - - if isSearching || hasAssets { - mediaPicker.showSearchBar() - if let searchBar = mediaPicker.searchBar { - WPStyleGuide.configureSearchBar(searchBar) - } - } else { - mediaPicker.hideSearchBar() - } - } - - fileprivate func registerChangeObserver(forPicker picker: WPMediaPickerViewController) { - assert(mediaLibraryChangeObserverKey == nil) - mediaLibraryChangeObserverKey = mediaLibraryDataSource.registerChangeObserverBlock({ [weak self] _, _, _, _, _ in - - self?.updateSearchBar(mediaPicker: picker) - - let isNotSearching = self?.mediaLibraryDataSource.searchQuery?.count ?? 0 == 0 - let hasNoAssets = self?.mediaLibraryDataSource.numberOfAssets() == 0 - - if isNotSearching && hasNoAssets { - self?.noResultsView.removeFromView() - self?.noResultsView.configureForNoAssets(userCanUploadMedia: false) - } - }) - } - - fileprivate func unregisterChangeObserver() { - if let mediaLibraryChangeObserverKey = mediaLibraryChangeObserverKey { - mediaLibraryDataSource.unregisterChangeObserver(mediaLibraryChangeObserverKey) - } - mediaLibraryChangeObserverKey = nil - } - - // MARK: - Keyboard Handling override var keyCommands: [UIKeyCommand] { @@ -1586,13 +1527,13 @@ extension AztecPostViewController { switch mediaIdentifier { case .deviceLibrary: trackFormatBarAnalytics(stat: .editorMediaPickerTappedDevicePhotos) - presentMediaPickerFullScreen(animated: true, dataSourceType: .device) + presentDeviceMediaPicker(animated: true) case .camera: trackFormatBarAnalytics(stat: .editorMediaPickerTappedCamera) - mediaPickerInputViewController?.showCapture() + MediaPickerMenu(viewController: self).showCamera(delegate: self) case .mediaLibrary: trackFormatBarAnalytics(stat: .editorMediaPickerTappedMediaLibrary) - presentMediaPickerFullScreen(animated: true, dataSourceType: .mediaLibrary) + presentSiteMediaPicker() case .otherApplications: trackFormatBarAnalytics(stat: .editorMediaPickerTappedOtherApps) showMore(from: barItem) @@ -1605,11 +1546,7 @@ extension AztecPostViewController { } func handleFormatBarTrailingItem(_ item: UIButton) { - guard let mediaPicker = mediaPickerInputViewController else { - return - } - - mediaPickerController(mediaPicker.mediaPicker, didFinishPicking: mediaPicker.mediaPicker.selectedAssets) + insertPickerResults() } @objc func toggleBold() { @@ -1803,68 +1740,7 @@ extension AztecPostViewController { richTextView.removeLink(inRange: range) } - private var mediaInputToolbar: UIToolbar { - let toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: Constants.toolbarHeight)) - toolbar.barTintColor = WPStyleGuide.aztecFormatBarBackgroundColor - toolbar.tintColor = WPStyleGuide.aztecFormatBarActiveColor - let gridButton = UIBarButtonItem(image: .gridicon(.grid), style: .plain, target: self, action: #selector(mediaAddShowFullScreen)) - gridButton.accessibilityLabel = NSLocalizedString("Open full media picker", comment: "Editor button to swich the media picker from quick mode to full picker") - toolbar.items = [ - UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(mediaAddInputCancelled)), - UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil), - gridButton, - UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil), - UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(mediaAddInputDone)) - ] - - for item in toolbar.items! { - item.tintColor = WPStyleGuide.aztecFormatBarActiveColor - item.setTitleTextAttributes([.foregroundColor: WPStyleGuide.aztecFormatBarActiveColor], for: .normal) - } - - return toolbar - - } - - // MARK: - Media Input toolbar button actions - - /// Method to be called when the grid icon is pressed on the media input toolbar. - /// - /// - Parameter sender: the button that was pressed. - /// - @objc func mediaAddShowFullScreen(_ sender: UIBarButtonItem) { - presentMediaPickerFullScreen(animated: true) - restoreInputAssistantItems() - } - - /// Method to be called when canceled is pressed. - /// - /// - Parameter sender: the button that was pressed. - @objc func mediaAddInputCancelled(_ sender: UIBarButtonItem) { - - guard let mediaPicker = mediaPickerInputViewController?.mediaPicker else { - return - } - mediaPickerControllerDidCancel(mediaPicker) - restoreInputAssistantItems() - } - - /// Method to be called when done is pressed on the media input toolbar. - /// - /// - Parameter sender: the button that was pressed. - @objc func mediaAddInputDone(_ sender: UIBarButtonItem) { - - guard let mediaPicker = mediaPickerInputViewController?.mediaPicker - else { - return - } - let selectedAssets = mediaPicker.selectedAssets - mediaPickerController(mediaPicker, didFinishPicking: selectedAssets) - restoreInputAssistantItems() - } - func restoreInputAssistantItems() { - richTextView.inputAssistantItem.leadingBarButtonGroups = originalLeadingBarButtonGroup richTextView.inputAssistantItem.trailingBarButtonGroups = originalTrailingBarButtonGroup richTextView.autocorrectionType = .yes @@ -1887,69 +1763,33 @@ extension AztecPostViewController { @IBAction @objc func presentMediaPickerWasPressed() { if let item = formatBar.leadingItem { - presentMediaPicker(fromButton: item, animated: true) + presentEmbeddedMediaPicker(fromButton: item, animated: true) } } - fileprivate func presentMediaPickerFullScreen(animated: Bool, dataSourceType: MediaPickerDataSourceType = .device) { - - let options = WPMediaPickerOptions() - options.showMostRecentFirst = true - options.filter = [.all] - options.allowCaptureOfMedia = false - options.showSearchBar = true - options.badgedUTTypes = [UTType.gif.identifier] - options.preferredStatusBarStyle = WPStyleGuide.preferredStatusBarStyle - - let picker = WPNavigationMediaPickerViewController() - - switch dataSourceType { - case .device: - picker.dataSource = devicePhotoLibraryDataSource - case .mediaLibrary: - picker.startOnGroupSelector = false - picker.showGroupSelector = false - picker.dataSource = mediaLibraryDataSource - registerChangeObserver(forPicker: picker.mediaPicker) - @unknown default: - fatalError() - } - - picker.selectionActionTitle = Constants.mediaPickerInsertText - picker.mediaPicker.options = options - picker.delegate = self - picker.previewActionTitle = NSLocalizedString("Edit %@", comment: "Button that displays the media editor to the user") - picker.modalPresentationStyle = .currentContext - if let previousPicker = mediaPickerInputViewController?.mediaPicker { - picker.mediaPicker.selectedAssets = previousPicker.selectedAssets - } + fileprivate func presentDeviceMediaPicker(animated: Bool) { + MediaPickerMenu(viewController: self, isMultipleSelectionEnabled: true) + .showPhotosPicker(delegate: self) + } - present(picker, animated: true) + private func presentSiteMediaPicker() { + MediaPickerMenu(viewController: self, isMultipleSelectionEnabled: true) + .showSiteMediaPicker(blog: post.blog, delegate: self) } private func toggleMediaPicker(fromButton button: UIButton) { - if mediaPickerInputViewController != nil { + switch toolbarMode { + case .media: closeMediaPickerInputViewController() trackFormatBarAnalytics(stat: .editorMediaPickerTappedDismiss) - } else { - presentMediaPicker(fromButton: button, animated: true) + case .text: + presentEmbeddedMediaPicker(fromButton: button, animated: true) } } - private func presentMediaPicker(fromButton button: UIButton, animated: Bool = true) { + private func presentEmbeddedMediaPicker(fromButton button: UIButton, animated: Bool = true) { trackFormatBarAnalytics(stat: .editorTappedImage) - let options = WPMediaPickerOptions() - options.showMostRecentFirst = true - options.filter = [WPMediaType.image, WPMediaType.video] - options.allowMultipleSelection = true - options.allowCaptureOfMedia = false - options.scrollVertically = true - options.badgedUTTypes = [UTType.gif.identifier] - options.preferredStatusBarStyle = WPStyleGuide.preferredStatusBarStyle - - let picker = WPInputMediaPickerViewController(options: options) - mediaPickerInputViewController = picker updateToolbar(formatBar, forMode: .media) originalLeadingBarButtonGroup = richTextView.inputAssistantItem.leadingBarButtonGroups @@ -1960,19 +1800,33 @@ extension AztecPostViewController { richTextView.autocorrectionType = .no - picker.mediaPicker.viewControllerToUseToPresent = self - picker.dataSource = WPPHAssetDataSource.sharedInstance() - picker.mediaPicker.mediaPickerDelegate = self +#if swift(>=5.9) // Requires Xcode 15 + if #available(iOS 17, *) { + var configuration = PHPickerConfiguration() + configuration.filter = .any(of: [.images, .videos]) + configuration.preferredAssetRepresentationMode = .current + configuration.selectionLimit = 0 + configuration.disabledCapabilities = [ + .collectionNavigation, .collectionNavigation, .search, .stagingArea + ] + configuration.edgesWithoutContentMargins = .all + configuration.selection = .continuousAndOrdered + + let picker = PHPickerViewController(configuration: configuration) + picker.delegate = self + mediaPickerInputViewController = picker + + if currentKeyboardFrame != .zero { + // iOS is not adjusting the media picker's height to match the default keyboard's height when autoresizingMask + // is set to UIViewAutoresizingFlexibleHeight (even though the docs claim it should). Need to manually + // set the picker's frame to the current keyboard's frame. + picker.view.autoresizingMask = [] + picker.view.frame = CGRect(x: 0, y: 0, width: currentKeyboardFrame.width, height: mediaKeyboardHeight) + } - if currentKeyboardFrame != .zero { - // iOS is not adjusting the media picker's height to match the default keyboard's height when autoresizingMask - // is set to UIViewAutoresizingFlexibleHeight (even though the docs claim it should). Need to manually - // set the picker's frame to the current keyboard's frame. - picker.view.autoresizingMask = [] - picker.view.frame = CGRect(x: 0, y: 0, width: currentKeyboardFrame.width, height: mediaKeyboardHeight) + presentToolbarViewControllerAsInputView(picker) } - - presentToolbarViewControllerAsInputView(picker) +#endif } @objc func toggleEditingMode() { @@ -2065,8 +1919,7 @@ extension AztecPostViewController { } private func showMore(from: FormatBarItem) { - let moreCoordinatorContext = MediaPickingContext(origin: self, view: view, blog: post.blog) - moreCoordinator.present(context: moreCoordinatorContext) + moreCoordinator.present(in: self, blog: post.blog) } private func presentToolbarViewControllerAsInputView(_ viewController: UIViewController) { @@ -2106,6 +1959,8 @@ extension AztecPostViewController { } fileprivate func updateToolbar(_ toolbar: Aztec.FormatBar, forMode mode: FormatBarMode) { + self.toolbarMode = mode + if let leadingItem = toolbar.leadingItem { rotateMediaToolbarItem(leadingItem, forMode: mode) } @@ -2316,10 +2171,6 @@ private extension AztecPostViewController { extension AztecPostViewController { - private func stopEditing() { - view.endEditing(true) - } - func contentByStrippingMediaAttachments() -> String { if editorView.editingMode == .html { setHTML(htmlTextView.text) @@ -2451,13 +2302,12 @@ extension AztecPostViewController { attachment?.uploadID = media.uploadID } - /// Sets the badge title of `attachment` to "GIF" if either the media is being imported from Tenor, - /// or if it's a PHAsset with an animated playback style. + /// Sets the badge title of `attachment` to "GIF". private func setGifBadgeIfNecessary(for attachment: MediaAttachment, asset: ExportableAsset, source: MediaSource) { var isGif = source == .tenor - if let asset = asset as? PHAsset, - asset.playbackStyle == .imageAnimated { + if let asset = (asset as? NSItemProvider), + asset.hasItemConformingToTypeIdentifier(UTType.gif.identifier) { isGif = true } @@ -2474,10 +2324,6 @@ extension AztecPostViewController { insert(exportableAsset: image, source: source) } - fileprivate func insertDeviceMedia(phAsset: PHAsset, source: MediaSource = .deviceLibrary) { - insert(exportableAsset: phAsset, source: source) - } - private func insertStockPhotosMedia(_ media: StockPhotosMedia) { insert(exportableAsset: media, source: .stockPhotos) } @@ -2827,7 +2673,7 @@ extension AztecPostViewController { videoSrcURL.scheme == VideoShortcodeProcessor.videoPressScheme, let videoPressID = videoSrcURL.host { // It's videoPress video so let's fetch the information for the video - let remote = MediaServiceRemoteFactory().remote(for: self.post.blog) + let remote = try? MediaServiceRemoteFactory().remote(for: self.post.blog) remote?.getMetadataFromVideoPressID(videoPressID, isSitePrivate: self.post.blog.isPrivate(), success: { metadata in if let metadata, let originalURL = metadata.originalURL { videoAttachment.updateURL(metadata.getURLWithToken(url: originalURL) ?? originalURL) @@ -3005,7 +2851,7 @@ extension AztecPostViewController { return } // It's videoPress video so let's fetch the information for the video - let remote = MediaServiceRemoteFactory().remote(for: self.post.blog) + let remote = try? MediaServiceRemoteFactory().remote(for: self.post.blog) guard let remote else { displayUnableToPlayVideoAlert() DDLogError("Unable to create a remote instance for \(String(describing: self.post.blog.dotComID))") @@ -3081,9 +2927,7 @@ extension AztecPostViewController { } func closeMediaPickerInputViewController() { - guard mediaPickerInputViewController != nil else { - return - } + selectedPickerResults = [] mediaPickerInputViewController = nil changeRichTextInputView(to: nil) updateToolbar(formatBar, forMode: .text) @@ -3210,106 +3054,76 @@ extension AztecPostViewController: TextViewAttachmentDelegate { } } +// MARK: - MediaPickerViewController (SiteMediaPickerViewControllerDelegate) -// MARK: - MediaPickerViewController Delegate Conformance -// -extension AztecPostViewController: WPMediaPickerViewControllerDelegate { - - func emptyViewController(forMediaPickerController picker: WPMediaPickerViewController) -> UIViewController? { - if picker != mediaPickerInputViewController?.mediaPicker { - return noResultsView +extension AztecPostViewController: SiteMediaPickerViewControllerDelegate { + func siteMediaPickerViewController(_ viewController: SiteMediaPickerViewController, didFinishWithSelection selection: [Media]) { + dismiss(animated: true) + mediaSelectionMethod = .fullScreenPicker + for media in selection { + insertSiteMediaLibrary(media: media) } - return nil } +} - func mediaPickerController(_ picker: WPMediaPickerViewController, didUpdateSearchWithAssetCount assetCount: Int) { - noResultsView.removeFromView() +// MARK: - AztecPostViewController (ImagePickerControllerDelegate) - if (mediaLibraryDataSource.searchQuery?.count ?? 0) > 0 { - noResultsView.configureForNoSearchResult() +extension AztecPostViewController: ImagePickerControllerDelegate { + func imagePicker(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { + dismiss(animated: true) { + guard let mediaType = info[.mediaType] as? String else { + return + } + switch mediaType { + case UTType.image.identifier: + if let image = info[.originalImage] as? UIImage { + self.insertImage(image: image, source: .camera) + } + case UTType.movie.identifier: + guard let videoURL = info[.mediaURL] as? URL else { + return + } + guard self.post.blog.canUploadVideo(from: videoURL) else { + self.presentVideoLimitExceededAfterCapture(on: self) + return + } + self.insert(exportableAsset: videoURL as NSURL, source: .camera) + default: + break + } } } +} - func mediaPickerControllerWillBeginLoadingData(_ picker: WPMediaPickerViewController) { - updateSearchBar(mediaPicker: picker) - noResultsView.configureForFetching() - } +extension AztecPostViewController: VideoLimitsAlertPresenter {} - func mediaPickerControllerDidEndLoadingData(_ picker: WPMediaPickerViewController) { - updateSearchBar(mediaPicker: picker) - noResultsView.removeFromView() - noResultsView.configureForNoAssets(userCanUploadMedia: false) - } +// MARK: - MediaPickerViewController (PHPickerViewControllerDelegate) - func mediaPickerControllerDidCancel(_ picker: WPMediaPickerViewController) { - if picker != mediaPickerInputViewController?.mediaPicker { - unregisterChangeObserver() - mediaLibraryDataSource.searchCancelled() - dismiss(animated: true) - } - } +extension AztecPostViewController: PHPickerViewControllerDelegate { + func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { + selectedPickerResults = results - func mediaPickerController(_ picker: WPMediaPickerViewController, didFinishPicking assets: [WPMediaAsset]) { - if picker != mediaPickerInputViewController?.mediaPicker { - unregisterChangeObserver() - mediaLibraryDataSource.searchCancelled() - dismiss(animated: true) - mediaSelectionMethod = .fullScreenPicker + // The delegate is configured to get called continuously + if picker == mediaPickerInputViewController { + updateFormatBarInsertAssetCount() } else { - mediaSelectionMethod = .inlinePicker + dismiss(animated: true) + insertPickerResults() } + } - closeMediaPickerInputViewController() - - if assets.isEmpty { + private func insertPickerResults() { + guard !selectedPickerResults.isEmpty else { return } - - for asset in assets { - switch asset { - case let phAsset as PHAsset: - insertDeviceMedia(phAsset: phAsset) - case let media as Media: - insertSiteMediaLibrary(media: media) - default: - continue - } - } - } - - - func mediaPickerController(_ picker: WPMediaPickerViewController, selectionChanged assets: [WPMediaAsset]) { - updateFormatBarInsertAssetCount() - } - - func mediaPickerController(_ picker: WPMediaPickerViewController, didSelect asset: WPMediaAsset) { - updateFormatBarInsertAssetCount() - } - - func mediaPickerController(_ picker: WPMediaPickerViewController, didDeselect asset: WPMediaAsset) { - updateFormatBarInsertAssetCount() - } - - func mediaPickerController(_ picker: WPMediaPickerViewController, handleError error: Error) -> Bool { - let alert = WPMediaPickerAlertHelper.buildAlertControllerWithError(error) - present(alert, animated: true) - return true - } - - func mediaPickerController(_ picker: WPMediaPickerViewController, previewViewControllerFor assets: [WPMediaAsset], selectedIndex selected: Int) -> UIViewController? { - if let phAssets = assets as? [PHAsset], phAssets.allSatisfy({ $0.mediaType == .image }) { - edit(fromMediaPicker: picker, assets: phAssets) - return nil - } else { - mediaPreviewHelper = MediaPreviewHelper(assets: assets) - return mediaPreviewHelper?.previewViewController(selectedIndex: selected) + for result in selectedPickerResults { + insert(exportableAsset: result.itemProvider, source: .deviceLibrary) } + closeMediaPickerInputViewController() } private func updateFormatBarInsertAssetCount() { - guard let assetCount = mediaPickerInputViewController?.mediaPicker.selectedAssets.count else { - return - } + let assetCount = selectedPickerResults.count if assetCount == 0 { insertToolbarItem.isEnabled = false @@ -3373,18 +3187,11 @@ extension AztecPostViewController: UIDocumentPickerDelegate { } } -extension AztecPostViewController: StockPhotosPickerDelegate { - func stockPhotosPicker(_ picker: StockPhotosPicker, didFinishPicking assets: [StockPhotosMedia]) { - assets.forEach { - insert(exportableAsset: $0, source: .stockPhotos) - } - } -} - -extension AztecPostViewController: TenorPickerDelegate { - func tenorPicker(_ picker: TenorPicker, didFinishPicking assets: [TenorMedia]) { - assets.forEach { - insert(exportableAsset: $0, source: .tenor) +extension AztecPostViewController: ExternalMediaPickerViewDelegate { + func externalMediaPickerViewController(_ viewController: ExternalMediaPickerViewController, didFinishWithSelection selection: [ExternalMediaAsset]) { + viewController.presentingViewController?.dismiss(animated: true) + selection.forEach { + insert(exportableAsset: $0, source: viewController.source) } } } @@ -3406,9 +3213,6 @@ extension AztecPostViewController { } struct Constants { - static let defaultMargin = CGFloat(20) - static let blogPickerCompactSize = CGSize(width: 125, height: 30) - static let blogPickerRegularSize = CGSize(width: 300, height: 30) static let savingDraftButtonSize = CGSize(width: 130, height: 30) static let uploadingButtonSize = CGSize(width: 150, height: 30) static let moreAttachmentText = "more" @@ -3498,11 +3302,6 @@ extension AztecPostViewController { static let monospace = UIFont(name: "Menlo-Regular", size: 16.0)! } - struct Restoration { - static let restorationIdentifier = "AztecPostViewController" - static let postIdentifierKey = AbstractPost.classNameWithoutNamespaces() - } - struct MediaUploadingCancelAlert { static let title = NSLocalizedString("Cancel media uploads", comment: "Dialog box title for when the user is canceling an upload.") static let message = NSLocalizedString("You are currently uploading media. This action will cancel uploads in progress.\n\nAre you sure?", comment: "This prompt is displayed when the user attempts to stop media uploads in the post editor.") @@ -3592,41 +3391,6 @@ extension AztecPostViewController { // MARK: - Media Editing // extension AztecPostViewController { - private func edit(fromMediaPicker picker: WPMediaPickerViewController, assets: [PHAsset]) { - let mediaEditor = WPMediaEditor(assets) - - // When the photo's library is updated (eg.: a new photo is added) - // the actionBar is appearing and conflicting with Media Editor. - // We hide it to prevent that issue - picker.actionBar?.isHidden = true - - mediaEditor.edit(from: picker, - onFinishEditing: { [weak self] images, actions in - images.forEach { mediaEditorImage in - if let image = mediaEditorImage.editedImage { - self?.insertImage(image: image) - } else if let phAsset = mediaEditorImage as? PHAsset { - self?.insertDeviceMedia(phAsset: phAsset) - } - } - - self?.dismissMediaPicker() - }, onCancel: { - // Dismiss the Preview screen in Media Picker - picker.navigationController?.popViewController(animated: false) - - // Show picker actionBar again - picker.actionBar?.isHidden = false - }) - } - - private func dismissMediaPicker() { - unregisterChangeObserver() - mediaLibraryDataSource.searchCancelled() - closeMediaPickerInputViewController() - dismiss(animated: false) - } - private func edit(_ imageAttachment: ImageAttachment) { guard imageAttachment.mediaURL?.isGif == false else { diff --git a/WordPress/Classes/ViewRelated/Blaze Campaigns/BlazeCampaignsViewController.swift b/WordPress/Classes/ViewRelated/Blaze Campaigns/BlazeCampaignsViewController.swift index 0f847682716a..8a64f68eed7c 100644 --- a/WordPress/Classes/ViewRelated/Blaze Campaigns/BlazeCampaignsViewController.swift +++ b/WordPress/Classes/ViewRelated/Blaze Campaigns/BlazeCampaignsViewController.swift @@ -1,6 +1,7 @@ import UIKit import WordPressKit import WordPressFlux +import DesignSystem final class BlazeCampaignsViewController: UIViewController, NoResultsViewHost, BlazeCampaignsStreamDelegate { // MARK: - Views diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/BlogDashboardViewController.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/BlogDashboardViewController.swift index 58c072266ff0..76726b6c7f5e 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/BlogDashboardViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/BlogDashboardViewController.swift @@ -29,11 +29,6 @@ final class BlogDashboardViewController: UIViewController { return refreshControl }() - /// The "My Site" parent view controller - var mySiteViewController: MySiteViewController? { - return parent as? MySiteViewController - } - /// The "My Site" main scroll view var mySiteScrollView: UIScrollView? { return view.superview?.superview as? UIScrollView @@ -321,7 +316,6 @@ extension BlogDashboardViewController { private enum Constants { - static let estimatedWidth: CGFloat = 100 static let estimatedHeight: CGFloat = 44 static let horizontalSectionInset: CGFloat = 12 static let verticalSectionInset: CGFloat = 20 diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/BlogDashboardCardConfigurable.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/BlogDashboardCardConfigurable.swift index e2536e9310ec..086080893a92 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/BlogDashboardCardConfigurable.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/BlogDashboardCardConfigurable.swift @@ -1,10 +1,29 @@ import Foundation protocol BlogDashboardCardConfigurable { + func configure(blog: Blog, viewController: BlogDashboardViewController?, model: DashboardCardModel) func configure(blog: Blog, viewController: BlogDashboardViewController?, apiResponse: BlogDashboardRemoteEntity?) + func configure(blog: Blog, viewController: BlogDashboardViewController?, model: DashboardDynamicCardModel) var row: Int { get set } } +extension BlogDashboardCardConfigurable { + func configure(blog: Blog, viewController: BlogDashboardViewController?, apiResponse: BlogDashboardRemoteEntity?) { + } + + func configure(blog: Blog, viewController: BlogDashboardViewController?, model: DashboardDynamicCardModel) { + } + + func configure(blog: Blog, viewController: BlogDashboardViewController?, model: DashboardCardModel) { + switch model { + case .normal(let model): + self.configure(blog: blog, viewController: viewController, apiResponse: model.apiResponse) + case .dynamic(let model): + self.configure(blog: blog, viewController: viewController, model: model) + } + } +} + extension BlogDashboardCardConfigurable where Self: UIView { var row: Int { get { diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/BlogDashboardDynamicCardCell.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/BlogDashboardDynamicCardCell.swift new file mode 100644 index 000000000000..ce6d654b324b --- /dev/null +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/BlogDashboardDynamicCardCell.swift @@ -0,0 +1,126 @@ +import UIKit +import SwiftUI + +final class BlogDashboardDynamicCardCell: DashboardCollectionViewCell { + + // MARK: - Properties + + private var coordinator: BlogDashboardDynamicCardCoordinator? + + // MARK: - Views + + private let frameView = BlogDashboardCardFrameView() + private weak var presentingViewController: UIViewController? + private var hostingController: UIHostingController? + + // MARK: - Init + + override init(frame: CGRect) { + super.init(frame: frame) + self.setupFrameView() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Configuration + + func configure(blog: Blog, viewController: BlogDashboardViewController?, model: DashboardDynamicCardModel) { + self.coordinator = .init(viewController: viewController, model: model) + + self.presentingViewController = viewController + self.configureMoreButton(for: model, blog: blog) + + self.frameView.setTitle(model.payload.title) + self.frameView.onViewTap = { [weak self] in + self?.didTapCard(with: model) + } + + if let viewController { + self.configureHostingController(with: model, parent: viewController) + } + + self.coordinator?.didAppear() + } + + private func configureHostingController(with model: DashboardDynamicCardModel, parent: UIViewController) { + let content = DynamicDashboardCard(model: model) { [weak self] in + self?.didTapAction(with: model) + } + + if let hostingController { + hostingController.rootView = content + } else { + let hostingController = DynamicDashboardCardViewController(rootView: content) + hostingController.view.backgroundColor = .clear + hostingController.view.translatesAutoresizingMaskIntoConstraints = false + hostingController.willMove(toParent: parent) + parent.addChild(hostingController) + self.frameView.add(subview: hostingController.view) + hostingController.didMove(toParent: parent) + self.hostingController = hostingController + } + + self.hostingController?.view?.invalidateIntrinsicContentSize() + } + + private func setupFrameView() { + self.frameView.ellipsisButton.showsMenuAsPrimaryAction = true + self.frameView.translatesAutoresizingMaskIntoConstraints = false + self.contentView.addSubview(frameView) + self.contentView.pinSubviewToAllEdges(frameView, priority: .defaultHigh) + } + + private func configureMoreButton(for card: DashboardDynamicCardModel, blog: Blog) { + self.frameView.addMoreMenu( + items: + [ + UIMenu( + options: .displayInline, + children: [ + BlogDashboardHelpers.makeHideCardAction(for: card, blog: blog) + ] + ) + ], + card: card + ) + } + + // MARK: - User Interaction + + private func didTapAction(with model: DashboardDynamicCardModel) { + self.coordinator?.didTapCardCTA() + } + + private func didTapCard(with model: DashboardDynamicCardModel) { + self.coordinator?.didTapCard() + } +} + +// MARK: - DynamicDashboardCard Extension + +private extension DynamicDashboardCard { + + init(model: DashboardDynamicCardModel, callback: (() -> Void)?) { + let payload = model.payload + + let featureImageURL = URL(string: payload.featuredImage ?? "") + let rows = (payload.rows ?? []).map { + Input.Row( + title: $0.title, + description: $0.description, + imageURL: URL(string: $0.icon ?? "") + ) + } + let action: Input.Action? = { + guard let title = payload.action, let callback else { + return nil + } + return .init(title: title, callback: callback) + }() + + let input = Input(featureImageURL: featureImageURL, rows: rows, action: action) + self.init(input: input) + } +} diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/DashboardDomainRegistrationCardCell.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/DashboardDomainRegistrationCardCell.swift index 513123b8c11f..bfe435bcc475 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/DashboardDomainRegistrationCardCell.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/DashboardDomainRegistrationCardCell.swift @@ -103,7 +103,7 @@ extension DashboardDomainRegistrationCardCell { comment: "Action to redeem domain credit." ) static let content = NSLocalizedString( - "All WordPress.com plans include a custom domain name. Register your free premium domain now.", + "All WordPress.com annual plans include a custom domain name. Register your free domain now.", comment: "Information about redeeming domain credit on site dashboard." ) static let hideThis = NSLocalizedString( diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Domains/BaseDashboardDomainsCardCell.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Domains/BaseDashboardDomainsCardCell.swift index 4dbea785636c..8ef9171c1d27 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Domains/BaseDashboardDomainsCardCell.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Domains/BaseDashboardDomainsCardCell.swift @@ -46,13 +46,6 @@ class BaseDashboardDomainsCardCell: DashboardCollectionViewCell { return stackView }() - private lazy var dashboardIcon: UIImageView = { - let image = UIImage.gridicon(.domains).withTintColor(.white).withRenderingMode(.alwaysOriginal) - let imageView = UIImageView(image: image) - imageView.translatesAutoresizingMaskIntoConstraints = false - return imageView - }() - private lazy var contextMenu: UIMenu = { let hideThisAction = UIAction(title: viewModel.strings.hideThis, image: Style.hideThisImage, diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Domains/GoogleDomains/DashboardGoogleDomainsCardCell.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Domains/GoogleDomains/DashboardGoogleDomainsCardCell.swift index 97185039ef66..efab24ca8808 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Domains/GoogleDomains/DashboardGoogleDomainsCardCell.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Domains/GoogleDomains/DashboardGoogleDomainsCardCell.swift @@ -86,7 +86,7 @@ final class DashboardGoogleDomainsCardCell: DashboardCollectionViewCell { extension DashboardGoogleDomainsCardCell: DashboardGoogleDomainsCardCellProtocol { func presentGoogleDomainsWebView(with url: URL) { - + // TODO: Use `TransferDomainsWebViewController` instead. let webViewController = WebViewControllerFactory.controllerAuthenticatedWithDefaultAccount( url: url, source: "domain_focus_card" diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Domains/GoogleDomains/DashboardGoogleDomainsCardView.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Domains/GoogleDomains/DashboardGoogleDomainsCardView.swift index 0913ead48c69..8e4c2b4cfaeb 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Domains/GoogleDomains/DashboardGoogleDomainsCardView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Domains/GoogleDomains/DashboardGoogleDomainsCardView.swift @@ -1,4 +1,5 @@ import SwiftUI +import DesignSystem struct DashboardGoogleDomainsCardView: View { private var buttonAction: () -> () diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Dynamic/BlogDashboardDynamicCardCoordinator.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Dynamic/BlogDashboardDynamicCardCoordinator.swift new file mode 100644 index 000000000000..59b669b479c7 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Dynamic/BlogDashboardDynamicCardCoordinator.swift @@ -0,0 +1,91 @@ +import Foundation + +final class BlogDashboardDynamicCardCoordinator { + + // MARK: - Dependencies + + private let analyticsTracker: AnalyticsEventTracking.Type + private let model: DashboardDynamicCardModel + private let linkRouter: LinkRouter + + private weak var viewController: UIViewController? + + // MARK: - Init + + init(viewController: UIViewController?, + model: DashboardDynamicCardModel, + linkRouter: LinkRouter = UniversalLinkRouter.shared, + analyticsTracker: AnalyticsEventTracking.Type = WPAnalytics.self) { + self.viewController = viewController + self.model = model + self.linkRouter = linkRouter + self.analyticsTracker = analyticsTracker + } + + // MARK: - API + + func didAppear() { + self.track(.cardShown(id: model.payload.id), frequency: .oncePerSession) + } + + func didTapCard() { + let payload = model.payload + if let urlString = payload.url, + let url = URL(string: urlString) { + routeToCardDestination(url: url) + } + self.track(.cardTapped(id: payload.id, url: payload.url)) + } + + func didTapCardCTA() { + let payload = model.payload + if let urlString = model.payload.url, + let url = URL(string: urlString) { + routeToCardDestination(url: url) + } + self.track(.cardCtaTapped(id: payload.id, url: payload.url)) + } + + private func routeToCardDestination(url: URL) { + if linkRouter.canHandle(url: url) { + routeToUniversalURL(url: url) + } else { + routeToWebView(url: url) + } + } + + private func routeToWebView(url: URL) { + guard UIApplication.shared.canOpenURL(url) else { + return + } + let configuration = WebViewControllerConfiguration(url: url) + configuration.authenticateWithDefaultAccount() + let controller = WebViewControllerFactory.controller(configuration: configuration, source: "dashboard") + let navController = UINavigationController(rootViewController: controller) + viewController?.present(navController, animated: true) + } + + private func routeToUniversalURL(url: URL) { + linkRouter.handle(url: url, shouldTrack: true, source: nil) + } +} + +// MARK: - Analytics + +private extension BlogDashboardDynamicCardCoordinator { + + private static var firedAnalyticEvents = Set() + + func track(_ event: DashboardDynamicCardAnalyticsEvent, frequency: TrackingFrequency = .multipleTimesPerSession) { + guard frequency == .multipleTimesPerSession || (frequency == .oncePerSession && !Self.firedAnalyticEvents.contains(event)) else { + return + } + self.analyticsTracker.track(.init(name: event.name, properties: event.properties)) + Self.firedAnalyticEvents.insert(event) + } + + enum TrackingFrequency { + case oncePerSession + case multipleTimesPerSession + } +} diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Dynamic/DynamicDashboardCard.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Dynamic/DynamicDashboardCard.swift new file mode 100644 index 000000000000..477a27711ac1 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Dynamic/DynamicDashboardCard.swift @@ -0,0 +1,164 @@ +import SwiftUI +import DesignSystem + +struct DynamicDashboardCard: View { + private enum Constants { + static let rowImageDiameter: CGFloat = 48 + } + + struct Input { + struct Row: Identifiable { + let id: UUID = UUID() + + let title: String? + let description: String? + let imageURL: URL? + } + + struct Action { + let title: String? + let callback: (() -> Void) + } + + let featureImageURL: URL? + let rows: [Row] + let action: Action? + } + + private let input: Input + + init(input: Input) { + self.input = input + } + + var body: some View { + VStack(spacing: Length.Padding.single) { + featureImage + rowsVStack + actionHStack + } + .padding(.bottom, Length.Padding.single) + .padding(.horizontal, Length.Padding.double) + .fixedSize(horizontal: false, vertical: true) + } + + @ViewBuilder + var featureImage: some View { + if let featureImageURL = input.featureImageURL { + AsyncImage(url: featureImageURL) { phase in + Group { + if let image = phase.image { + image + .resizable() + .aspectRatio(contentMode: .fit) + } else { + Color.DS.Background.secondary + .frame(maxWidth: .infinity) + .frame(height: 150) + } + } + .clipShape( + RoundedRectangle( + cornerRadius: Length.Radius.small + ) + ) + } + } + } + + @ViewBuilder + var rowsVStack: some View { + VStack(spacing: Length.Padding.single) { + ForEach(input.rows) { row in + HStack(alignment: .top, spacing: Length.Padding.split) { + if let imageURL = row.imageURL { + AsyncImage(url: imageURL) + .frame( + width: Constants.rowImageDiameter, + height: Constants.rowImageDiameter + ) + .clipShape(Circle()) + } + + VStack(alignment: .leading) { + if let title = row.title { + Text(title) + .style(.bodyLarge(.emphasized)) + .foregroundStyle(Color.DS.Foreground.primary) + } + + if let description = row.description { + Text(description) + .style(.bodySmall(.regular)) + .foregroundStyle(Color.DS.Foreground.secondary) + } + } + Spacer() + } + } + } + } + + @ViewBuilder + var actionHStack: some View { + if let title = input.action?.title { + HStack { + DSButton(title: title, style: .init( + emphasis: .tertiary, + size: .small, + isJetpack: AppConfiguration.isJetpack + )) { + input.action?.callback() + } + Spacer() + } + } + } +} + +final class DynamicDashboardCardViewController: UIHostingController { + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + self.view.invalidateIntrinsicContentSize() + } +} + +// DesignSystem.DSButtonStyle extension to omit `isJetpack` from project target. +extension DSButtonStyle { + init(emphasis: DSButtonStyle.Emphasis, size: DSButtonStyle.Size) { + self.init( + emphasis: emphasis, + size: size, + isJetpack: AppConfiguration.isJetpack + ) + } +} + +#if DEBUG +struct DynamicDashboardCard_Previews: PreviewProvider { + static var previews: some View { + DynamicDashboardCard( + input: .init( + featureImageURL: URL(string: "https://i.pickadummy.com/index.php?imgsize=400x200")!, + rows: [ + .init( + title: "Title first", + description: "Description first", + imageURL: URL(string: "https://i.pickadummy.com/index.php?imgsize=48x48")! + ), + .init( + title: "Title second", + description: "Description second", + imageURL: URL(string: "https://i.pickadummy.com/index.php?imgsize=48x48")! + ) + ], + action: .init(title: "Action button", callback: { + () + }) + ) + ) + .padding() + } +} +#endif diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Free to Paid Plans/DashboardDomainsCardSearchView.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Free to Paid Plans/DashboardDomainsCardSearchView.swift index ca3508e6d38a..03010c9f9d43 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Free to Paid Plans/DashboardDomainsCardSearchView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Free to Paid Plans/DashboardDomainsCardSearchView.swift @@ -58,7 +58,7 @@ private extension DashboardDomainsCardSearchView { enum Constants { static let iconName = "globe" - static let searchBarPlaceholder = "domain.blog" + static let searchBarPlaceholder = "yourgroovydomain.com" } enum Colors { diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Free to Paid Plans/FreeToPaidPlansCoordinator.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Free to Paid Plans/FreeToPaidPlansCoordinator.swift index d7662557ea9c..af025385e61c 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Free to Paid Plans/FreeToPaidPlansCoordinator.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Free to Paid Plans/FreeToPaidPlansCoordinator.swift @@ -2,18 +2,23 @@ import UIKit import SwiftUI @objc final class FreeToPaidPlansCoordinator: NSObject { + + typealias PurchaseCallback = ((UIViewController, String) -> Void) + static func presentFreeDomainWithAnnualPlanFlow( in dashboardViewController: BlogDashboardViewController, source: String, blog: Blog ) { - let domainSuggestionsViewController = RegisterDomainSuggestionsViewController.instance( - site: blog, + let coordinator = RegisterDomainCoordinator(site: blog) + let domainSuggestionsViewController = DomainSelectionViewController( + service: DomainsServiceAdapter(coreDataStack: ContextManager.shared), domainSelectionType: .purchaseWithPaidPlan, - includeSupportButton: false + includeSupportButton: false, + coordinator: coordinator ) - let purchaseCallback = { (checkoutViewController: CheckoutViewController, domainName: String) in + let purchaseCallback = { (checkoutViewController: UIViewController, domainName: String) in let blogService = BlogService(coreDataStack: ContextManager.shared) blogService.syncBlogAndAllMetadata(blog) { } @@ -29,9 +34,24 @@ import SwiftUI PlansTracker.trackPurchaseResult(source: "plan_selection") } + let domainAddedToCart = plansFlowAfterDomainAddedToCartBlock(customTitle: nil, purchaseCallback: purchaseCallback) + + coordinator.domainAddedToCartAndLinkedToSiteCallback = domainAddedToCart + + let navigationController = UINavigationController(rootViewController: domainSuggestionsViewController) + dashboardViewController.present(navigationController, animated: true) + } + + /// Creates a block that launches the plans selection flow after a domain is added to the user's shopping cart + /// - Parameters: + /// - customTitle: Title of of the presented view. If nil the title displays the title of the webview.. + /// - purchaseCallback: closure to be called when user completes a plan purchase. + static func plansFlowAfterDomainAddedToCartBlock(customTitle: String?, + analyticsSource: String? = nil, + purchaseCallback: @escaping PurchaseCallback) -> RegisterDomainCoordinator.DomainAddedToCartCallback { let planSelected = { (planSelectionViewController: PlanSelectionViewController, domainName: String, checkoutURL: URL) in let viewModel = CheckoutViewModel(url: checkoutURL) - let checkoutViewController = CheckoutViewController(viewModel: viewModel, purchaseCallback: { checkoutViewController in + let checkoutViewController = CheckoutViewController(viewModel: viewModel, customTitle: customTitle, purchaseCallback: { checkoutViewController in purchaseCallback(checkoutViewController, domainName) }) checkoutViewController.configureSandboxStore { @@ -39,17 +59,18 @@ import SwiftUI } } - let domainAddedToCart = { (domainViewController: RegisterDomainSuggestionsViewController, domainName: String) in + let domainAddedToCart = { (domainViewController: UIViewController, domainName: String, blog: Blog) in guard let viewModel = PlanSelectionViewModel(blog: blog) else { return } - let planSelectionViewController = PlanSelectionViewController(viewModel: viewModel) + let planSelectionViewController = PlanSelectionViewController( + viewModel: viewModel, + customTitle: customTitle, + analyticsSource: analyticsSource + ) planSelectionViewController.planSelectedCallback = { planSelectionViewController, checkoutURL in planSelected(planSelectionViewController, domainName, checkoutURL) } domainViewController.navigationController?.pushViewController(planSelectionViewController, animated: true) } - domainSuggestionsViewController.domainAddedToCartCallback = domainAddedToCart - - let navigationController = UINavigationController(rootViewController: domainSuggestionsViewController) - dashboardViewController.present(navigationController, animated: true) + return domainAddedToCart } } diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Pages/DashboardPageCreationCell.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Pages/DashboardPageCreationCell.swift index f5c332503bba..0190e6b4fe02 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Pages/DashboardPageCreationCell.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Pages/DashboardPageCreationCell.swift @@ -153,7 +153,6 @@ private extension DashboardPageCreationCell { static let labelsStackViewLayoutMargins: NSDirectionalEdgeInsets = .init(top: 15, leading: 0, bottom: 15, trailing: 0) static let labelsStackViewCompactLayoutMargins: NSDirectionalEdgeInsets = .init(top: 15, leading: 0, bottom: 7, trailing: 0) static let createPageButtonContentInsets = NSDirectionalEdgeInsets.zero - static let createPageButtonContentEdgeInsets = UIEdgeInsets.zero static let promoImageSize: CGSize = .init(width: 110, height: 80) static let promoImageSuperViewInsets: UIEdgeInsets = .init(top: 10, left: 0, bottom: 10, right: 0) static let promoImageCornerRadius: CGFloat = 5 diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Pages/PagesCardViewModel.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Pages/PagesCardViewModel.swift index 0f53e5ee2b29..440687045f1c 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Pages/PagesCardViewModel.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Pages/PagesCardViewModel.swift @@ -232,7 +232,7 @@ private extension PagesCardViewModel { func sync() { isSyncing = true let filter = filter - DashboardPostsSyncManager.shared.syncPosts(blog: blog, postType: .page, statuses: filter.statuses.strings) + DashboardPostsSyncManager.shared.syncPosts(blog: blog, postType: .page, statuses: filter.statuses) } func hideLoading() { @@ -271,8 +271,7 @@ extension PagesCardViewModel: DashboardPostsSyncManagerListener { func postsSynced(success: Bool, blog: Blog, postType: DashboardPostsSyncManager.PostType, - posts: [AbstractPost]?, - for statuses: [String]) { + for statuses: [BasePost.Status]) { guard postType == .page, self.blog == blog else { return diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Posts/BlogDashboardCardFrameView.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Posts/BlogDashboardCardFrameView.swift index 89c6319d17c1..689b4a4e41e2 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Posts/BlogDashboardCardFrameView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Posts/BlogDashboardCardFrameView.swift @@ -185,6 +185,11 @@ class BlogDashboardCardFrameView: UIView { /// Adds the "more" button with the given actions to the corner of the cell. func addMoreMenu(items: [UIMenuElement], card: DashboardCard) { + self.addMoreMenu(items: items, card: card as BlogDashboardAnalyticPropertiesProviding) + } + + /// Adds the "more" button with the given actions to the corner of the cell. + func addMoreMenu(items: [UIMenuElement], card: BlogDashboardAnalyticPropertiesProviding) { onEllipsisButtonTap = { BlogDashboardAnalytics.trackContextualMenuAccessed(for: card) } diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Posts/DashboardPostsListCardCell.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Posts/DashboardPostsListCardCell.swift index b42cfa44e9e6..72d4d466a9f1 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Posts/DashboardPostsListCardCell.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Posts/DashboardPostsListCardCell.swift @@ -72,7 +72,7 @@ class DashboardPostsListCardCell: UICollectionViewCell, Reusable { frameView.add(subview: tableView) contentView.addSubview(frameView) - contentView.pinSubviewToAllEdges(frameView, priority: Constants.constraintPriority) + contentView.pinSubviewToAllEdges(frameView, priority: UILayoutPriority(999)) } func trackPostsDisplayed() { @@ -109,8 +109,6 @@ extension DashboardPostsListCardCell { } private func addDraftsContextMenu(card: DashboardCard, blog: Blog) { - guard FeatureFlag.personalizeHomeTab.enabled else { return } - frameView.addMoreMenu(items: [ UIMenu(options: .displayInline, children: [ makeDraftsListMenuAction() @@ -122,8 +120,6 @@ extension DashboardPostsListCardCell { } private func addScheduledContextMenu(card: DashboardCard, blog: Blog) { - guard FeatureFlag.personalizeHomeTab.enabled else { return } - frameView.addMoreMenu(items: [ UIMenu(options: .displayInline, children: [ makeScheduledListMenuAction() @@ -220,10 +216,4 @@ private extension DashboardPostsListCardCell { static let viewAllDrafts = NSLocalizedString("my-sites.drafts.card.viewAllDrafts", value: "View all drafts", comment: "Title for the View all drafts button in the More menu") static let viewAllScheduledPosts = NSLocalizedString("my-sites.scheduled.card.viewAllScheduledPosts", value: "View all scheduled posts", comment: "Title for the View all scheduled drafts button in the More menu") } - - enum Constants { - static let iconSize = CGSize(width: 18, height: 18) - static let constraintPriority = UILayoutPriority(999) - static let numberOfPosts = 3 - } } diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Posts/PostsCardViewModel.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Posts/PostsCardViewModel.swift index 9c3367107387..a8e05c695df2 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Posts/PostsCardViewModel.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Posts/PostsCardViewModel.swift @@ -198,7 +198,7 @@ private extension PostsCardViewModel { func sync() { isSyncing = true let filter = postListFilter - DashboardPostsSyncManager.shared.syncPosts(blog: blog, postType: .post, statuses: filter.statuses.strings) + DashboardPostsSyncManager.shared.syncPosts(blog: blog, postType: .post, statuses: filter.statuses) } func updateFilter() { @@ -272,7 +272,6 @@ private extension PostsCardViewModel { enum Constants { static let numberOfPosts = 3 - static let numberOfPostsToSync: NSNumber = 3 } enum Strings { @@ -286,9 +285,8 @@ extension PostsCardViewModel: DashboardPostsSyncManagerListener { func postsSynced(success: Bool, blog: Blog, postType: DashboardPostsSyncManager.PostType, - posts: [AbstractPost]?, - for statuses: [String]) { - let currentStatuses = postListFilter.statuses.strings + for statuses: [BasePost.Status]) { + let currentStatuses = postListFilter.statuses guard postType == .post, self.blog == blog, currentStatuses.allSatisfy(statuses.contains) else { diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Prompts/BloggingPromptsAttribution.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Prompts/BloggingPromptsAttribution.swift index 0434b85fc765..944954499c60 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Prompts/BloggingPromptsAttribution.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Prompts/BloggingPromptsAttribution.swift @@ -1,5 +1,8 @@ +import WordPressUI + enum BloggingPromptsAttribution: String { case dayone + case bloganuary var attributedText: NSAttributedString { let baseText = String(format: Strings.fromTextFormat, source) @@ -15,18 +18,21 @@ enum BloggingPromptsAttribution: String { var source: String { switch self { case .dayone: return Strings.dayOne + case .bloganuary: return Strings.bloganuary } } var iconImage: UIImage? { switch self { case .dayone: return Constants.dayOneIcon + case .bloganuary: return Constants.bloganuaryIcon } } private struct Strings { static let fromTextFormat = NSLocalizedString("From %1$@", comment: "Format for blogging prompts attribution. %1$@ is the attribution source.") static let dayOne = "Day One" + static let bloganuary = "Bloganuary" } private struct Constants { @@ -40,5 +46,17 @@ enum BloggingPromptsAttribution: String { ] static let iconSize = CGSize(width: 18, height: 18) static let dayOneIcon = UIImage(named: "logo-dayone")?.resizedImage(Constants.iconSize, interpolationQuality: .default) + + /// This is computed so it can react accordingly on color scheme changes. + static var bloganuaryIcon: UIImage? { + UIImage(named: "logo-bloganuary")? + .withRenderingMode(.alwaysTemplate) + .resizedImage(Constants.bloganuaryIconSize, interpolationQuality: .default) + .withAlignmentRectInsets(.init(allEdges: -6.0)) + .withTintColor(.label) + } + + /// Unlike the dayOne icon, the bloganuary icon has no implicit 6px padding surrounding the icon. + static let bloganuaryIconSize = CGSize(width: 12, height: 12) } } diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Prompts/DashboardBloganuaryCardCell.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Prompts/DashboardBloganuaryCardCell.swift new file mode 100644 index 000000000000..87c5e6b13508 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Prompts/DashboardBloganuaryCardCell.swift @@ -0,0 +1,197 @@ +import SwiftUI + +class DashboardBloganuaryCardCell: DashboardCollectionViewCell { + + private var blog: Blog? { + didSet { + updateUI() + } + } + + private weak var presenterViewController: BlogDashboardViewController? + + /// Checks whether the Bloganuary nudge card should be shown on the dashboard. + /// + /// The card is only going to be shown in December, and will be hidden in January. + /// It's also going to be shown for blogs that are marked as potential blogs by the backend, regardless + /// of whether the user has manually disabled the blogging prompts. + /// + /// - Parameters: + /// - blog: The current `Blog` instance. + /// - date: The date to check. Defaults to today. + /// - Returns: `true` if the Bloganuary card should be shown. `false` otherwise. + static func shouldShowCard(for blog: Blog, date: Date = Date()) -> Bool { + guard RemoteFeatureFlag.bloganuaryDashboardNudge.enabled(), + let context = blog.managedObjectContext else { + return false + } + + // Check for date eligibility. + let isDateWithinEligibleMonths: Bool = { + let components = date.dateAndTimeComponents() + guard let month = components.month else { + return false + } + + // NOTE: For simplicity, we're going to hardcode the date check if the date is within December or January. + return Constants.eligibleMonths.contains(month) + }() + + // Check if the blog is marked as a potential blogging site. + let isPotentialBloggingSite: Bool = context.performAndWait { + return (try? BloggingPromptSettings.of(blog))?.isPotentialBloggingSite ?? false + } + + return isDateWithinEligibleMonths && isPotentialBloggingSite + } + + func configure(blog: Blog, viewController: BlogDashboardViewController?, apiResponse: BlogDashboardRemoteEntity?) { + self.blog = blog + self.presenterViewController = viewController + + BlogDashboardAnalytics.shared.track(.dashboardCardShown, + properties: [ + "type": DashboardCard.bloganuaryNudge.rawValue, + "subtype": DashboardCard.bloganuaryNudge.rawValue + ]) + } + + // MARK: Private methods + + @MainActor + private func updateUI() { + guard let blog, + let blogID = blog.dotComID?.intValue else { + return + } + + contentView.subviews.forEach { $0.removeFromSuperview() } + + let cardView = BloganuaryNudgeCardView(onLearnMoreTapped: { [weak self] in + // check if the prompts card is enabled in the dashboard. + let promptsCardEnabled = BlogDashboardPersonalizationService(siteID: blogID).isEnabled(.prompts) + let overlayView = BloganuaryOverlayViewController(blogID: blogID, promptsEnabled: promptsCardEnabled) + + let navigationController = UINavigationController(rootViewController: overlayView) + navigationController.modalPresentationStyle = .formSheet + if let sheet = navigationController.sheetPresentationController { + sheet.prefersGrabberVisible = WPDeviceIdentification.isiPhone() + } + + BloganuaryTracker.trackCardLearnMoreTapped(promptsEnabled: promptsCardEnabled) + + self?.presenterViewController?.present(navigationController, animated: true) + }) + + let hostView = UIView.embedSwiftUIView(cardView) + let frameView = makeCardFrameView() + frameView.add(subview: hostView) + + contentView.addSubview(frameView) + contentView.pinSubviewToAllEdges(frameView) + } + + private func makeCardFrameView() -> BlogDashboardCardFrameView { + let frameView = BlogDashboardCardFrameView() + frameView.translatesAutoresizingMaskIntoConstraints = false + frameView.configureButtonContainerStackView() + + // NOTE: this is intentionally called *before* configuring the ellipsis button action, + // to avoid additional trailing padding. + frameView.hideHeader() + + if let blog { + frameView.onEllipsisButtonTap = { + BlogDashboardAnalytics.trackContextualMenuAccessed(for: .bloganuaryNudge) + } + frameView.ellipsisButton.showsMenuAsPrimaryAction = true + let action = BlogDashboardHelpers.makeHideCardAction(for: .bloganuaryNudge, blog: blog) + frameView.ellipsisButton.menu = UIMenu(title: String(), options: .displayInline, children: [action]) + } + + return frameView + } + + struct Constants { + // Only show the card in December and January. + static let eligibleMonths = [1, 12] + } +} + +// MARK: - SwiftUI + +private struct BloganuaryNudgeCardView: View { + let onLearnMoreTapped: (() -> Void)? + + var body: some View { + VStack(alignment: .leading, spacing: 12.0) { + bloganuaryImage + .resizable() + .frame(width: 24.0, height: 24.0) + textContainer + Button { + onLearnMoreTapped?() + } label: { + Text(Strings.cta) + .font(.subheadline) + } + } + .padding(.top, 12.0) + .padding([.horizontal, .bottom], 16.0) + } + + var bloganuaryImage: Image { + if let uiImage = UIImage(named: "logo-bloganuary")?.withRenderingMode(.alwaysTemplate).withTintColor(.label) { + return Image(uiImage: uiImage) + } + return Image("logo-bloganuary", bundle: .main) + } + + var textContainer: some View { + VStack(alignment: .leading, spacing: 8.0) { + Text(cardTitle) + .font(.headline) + .fontWeight(.semibold) + Text(Strings.description) + .font(.subheadline) + .foregroundStyle(.secondary) + } + } + + var cardTitle: String { + let components = Date().dateAndTimeComponents() + guard let month = components.month, + DashboardBloganuaryCardCell.Constants.eligibleMonths.contains(month) else { + return Strings.title + } + + return month == 1 ? Strings.runningTitle : Strings.title + } + + struct Strings { + static let title = NSLocalizedString( + "bloganuary.dashboard.card.title", + value: "Bloganuary is coming!", + comment: "Title for the Bloganuary dashboard card." + ) + + // The card title string to be shown while Bloganuary is running + static let runningTitle = NSLocalizedString( + "bloganuary.dashboard.card.runningTitle", + value: "Bloganuary is here!", + comment: "Title for the Bloganuary dashboard card while Bloganuary is running." + ) + + static let description = NSLocalizedString( + "bloganuary.dashboard.card.description", + value: "For the month of January, blogging prompts will come from Bloganuary — our community challenge to build a blogging habit for the new year.", + comment: "Short description for the Bloganuary event, shown right below the title." + ) + + static let cta = NSLocalizedString( + "bloganuary.dashboard.card.button.learnMore", + value: "Learn more", + comment: "Title for a button that, when tapped, shows more info about participating in Bloganuary." + ) + } +} diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Prompts/DashboardPromptsCardCell.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Prompts/DashboardPromptsCardCell.swift index d25c8a3dfd39..d6f01a846376 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Prompts/DashboardPromptsCardCell.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Prompts/DashboardPromptsCardCell.swift @@ -513,12 +513,6 @@ private extension DashboardPromptsCardCell { BlogDashboardAnalytics.trackHideTapped(for: .prompts) let service = BlogDashboardPersonalizationService(siteID: siteID) service.setEnabled(false, for: .prompts) - if !FeatureFlag.personalizeHomeTab.enabled { - let notice = Notice(title: Strings.promptRemovedTitle, message: Strings.promptRemovedSubtitle, feedbackType: .success, actionTitle: Strings.undoSkipTitle) { _ in - service.setEnabled(true, for: .prompts) - } - ActionDispatcher.dispatch(NoticeAction.post(notice)) - } } func learnMoreTapped() { @@ -547,12 +541,6 @@ private extension DashboardPromptsCardCell { static let errorTitle = NSLocalizedString("Error loading prompt", comment: "Text displayed when there is a failure loading a blogging prompt.") static let promptSkippedTitle = NSLocalizedString("Prompt skipped", comment: "Title of the notification presented when a prompt is skipped") static let undoSkipTitle = NSLocalizedString("Undo", comment: "Button in the notification presented when a prompt is skipped") - static let promptRemovedTitle = NSLocalizedString("prompts.notification.removed.title", - value: "Blogging Prompts hidden", - comment: "Title of the notification when prompts are hidden from the dashboard card") - static let promptRemovedSubtitle = NSLocalizedString("prompts.notification.removed.subtitle", - value: "Visit Site Settings to turn back on", - comment: "Subtitle of the notification when prompts are hidden from the dashboard card") } struct Style { diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Quick Actions/DashboardQuickActionsCardCell.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Quick Actions/DashboardQuickActionsCardCell.swift index ab57c1f71ed5..124de8b9f203 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Quick Actions/DashboardQuickActionsCardCell.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Quick Actions/DashboardQuickActionsCardCell.swift @@ -98,7 +98,9 @@ final class DashboardQuickActionsCardCell: UICollectionViewCell, Reusable, UITab } case .media: trackQuickActionsEvent(.openedMediaLibrary, blog: blog) - MediaLibraryViewController.showForBlog(blog, from: parentViewController) + let controller = SiteMediaViewController(blog: blog) + parentViewController.show(controller, sender: nil) + QuickStartTourGuide.shared.visited(.mediaScreen) case .stats: trackQuickActionsEvent(.statsAccessed, blog: blog) StatsViewController.show(for: blog, from: parentViewController) diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Quick Start/DashboardQuickStartCardCell.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Quick Start/DashboardQuickStartCardCell.swift index 853cce02101f..872d0ed9665a 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Quick Start/DashboardQuickStartCardCell.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Quick Start/DashboardQuickStartCardCell.swift @@ -110,7 +110,6 @@ extension DashboardQuickStartCardCell { } private enum Metrics { - static let iconSize = CGSize(width: 18, height: 18) static let constraintPriority = UILayoutPriority(999) } } diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Stats/DashboardStatsCardCell.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Stats/DashboardStatsCardCell.swift index 577c9c27db9b..8574350f062f 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Stats/DashboardStatsCardCell.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Stats/DashboardStatsCardCell.swift @@ -76,16 +76,14 @@ extension DashboardStatsCardCell: BlogDashboardCardConfigurable { self.showStats(for: blog, from: viewController) } - if FeatureFlag.personalizeHomeTab.enabled { - frameView.addMoreMenu(items: [ - UIMenu(options: .displayInline, children: [ - makeShowStatsMenuAction(for: blog, in: viewController) - ]), - UIMenu(options: .displayInline, children: [ - BlogDashboardHelpers.makeHideCardAction(for: .todaysStats, blog: blog) - ]) - ], card: .todaysStats) - } + frameView.addMoreMenu(items: [ + UIMenu(options: .displayInline, children: [ + makeShowStatsMenuAction(for: blog, in: viewController) + ]), + UIMenu(options: .displayInline, children: [ + BlogDashboardHelpers.makeHideCardAction(for: .todaysStats, blog: blog) + ]) + ], card: .todaysStats) statsStackView?.views = viewModel?.todaysViews statsStackView?.visitors = viewModel?.todaysVisitors @@ -157,7 +155,6 @@ private extension DashboardStatsCardCell { enum Constants { static let spacing: CGFloat = 20 - static let iconSize = CGSize(width: 18, height: 18) static let constraintPriority = UILayoutPriority(999) diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Helpers/BlogDashboardAnalytics.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Helpers/BlogDashboardAnalytics.swift index b3e90f6d86bc..9a065d30ee73 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Helpers/BlogDashboardAnalytics.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Helpers/BlogDashboardAnalytics.swift @@ -31,11 +31,19 @@ class BlogDashboardAnalytics { } } + static func trackContextualMenuAccessed(for card: BlogDashboardAnalyticPropertiesProviding) { + WPAnalytics.track(.dashboardCardContextualMenuAccessed, properties: card.analyticProperties) + } + + static func trackHideTapped(for card: BlogDashboardAnalyticPropertiesProviding) { + WPAnalytics.track(.dashboardCardHideTapped, properties: card.analyticProperties) + } + static func trackContextualMenuAccessed(for card: DashboardCard) { - WPAnalytics.track(.dashboardCardContextualMenuAccessed, properties: ["card": card.rawValue]) + self.trackContextualMenuAccessed(for: card as BlogDashboardAnalyticPropertiesProviding) } static func trackHideTapped(for card: DashboardCard) { - WPAnalytics.track(.dashboardCardHideTapped, properties: ["card": card.rawValue]) + self.trackHideTapped(for: card as BlogDashboardAnalyticPropertiesProviding) } } diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Helpers/BlogDashboardHelpers.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Helpers/BlogDashboardHelpers.swift index 0b3f51757722..e243378aa26a 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Helpers/BlogDashboardHelpers.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Helpers/BlogDashboardHelpers.swift @@ -1,7 +1,13 @@ import Foundation struct BlogDashboardHelpers { + typealias Card = BlogDashboardAnalyticPropertiesProviding & BlogDashboardPersonalizable + static func makeHideCardAction(for card: DashboardCard, blog: Blog) -> UIAction { + Self.makeHideCardAction(for: card as Card, blog: blog) + } + + static func makeHideCardAction(for card: Card, blog: Blog) -> UIAction { makeHideCardAction { BlogDashboardAnalytics.trackHideTapped(for: card) BlogDashboardPersonalizationService(siteID: blog.dotComID?.intValue ?? 0) diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Helpers/BlogDashboardState.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Helpers/BlogDashboardState.swift index 8582d322fde2..ff665769eae4 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Helpers/BlogDashboardState.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Helpers/BlogDashboardState.swift @@ -28,8 +28,8 @@ class BlogDashboardState { !hasCachedData && failedToLoad } - @Atomic var postsSyncingStatuses: [String] = [] - @Atomic var pagesSyncingStatuses: [String] = [] + @Atomic var postsSyncingStatuses: [BasePost.Status] = [] + @Atomic var pagesSyncingStatuses: [BasePost.Status] = [] private init() { } diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Helpers/DashboardPostsSyncManager.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Helpers/DashboardPostsSyncManager.swift index 03c7037d04b7..24948b96cc5a 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Helpers/DashboardPostsSyncManager.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Helpers/DashboardPostsSyncManager.swift @@ -4,8 +4,7 @@ protocol DashboardPostsSyncManagerListener: AnyObject { func postsSynced(success: Bool, blog: Blog, postType: DashboardPostsSyncManager.PostType, - posts: [AbstractPost]?, - for statuses: [String]) + for statuses: [BasePost.Status]) } class DashboardPostsSyncManager { @@ -22,7 +21,7 @@ class DashboardPostsSyncManager { // MARK: Private Variables - private let postService: PostService + private let postRepository: PostRepository private let blogService: BlogService @Atomic private var listeners: [DashboardPostsSyncManagerListener] = [] @@ -32,9 +31,9 @@ class DashboardPostsSyncManager { // MARK: Initializer - init(postService: PostService = PostService(managedObjectContext: ContextManager.shared.mainContext), + init(postRepository: PostRepository = PostRepository(coreDataStack: ContextManager.shared), blogService: BlogService = BlogService(coreDataStack: ContextManager.shared)) { - self.postService = postService + self.postRepository = postRepository self.blogService = blogService } @@ -50,7 +49,7 @@ class DashboardPostsSyncManager { } } - func syncPosts(blog: Blog, postType: PostType, statuses: [String]) { + func syncPosts(blog: Blog, postType: PostType, statuses: [BasePost.Status]) { let toBeSynced = postType.statusesNotBeingSynced(statuses, for: blog) guard toBeSynced.isEmpty == false else { return @@ -58,14 +57,6 @@ class DashboardPostsSyncManager { postType.markStatusesAsBeingSynced(toBeSynced, for: blog) - let options = PostServiceSyncOptions() - options.statuses = toBeSynced - options.authorID = blog.userID - options.number = Constants.numberOfPostsToSync - options.order = .descending - options.orderBy = .byModified - options.purgesLocalSync = true - // If the userID is nil we need to sync authors // But only if the user is an admin if blog.userID == nil && blog.isAdmin { @@ -74,17 +65,33 @@ class DashboardPostsSyncManager { self?.syncPosts(blog: blog, postType: postType, statuses: toBeSynced) }, failure: { [weak self] error in postType.stopSyncingStatuses(toBeSynced, for: blog) - self?.notifyListenersOfPostsSync(success: false, blog: blog, postType: postType, posts: nil, for: toBeSynced) + self?.notifyListenersOfPostsSync(success: false, blog: blog, postType: postType, for: toBeSynced) }) return } - postService.syncPosts(ofType: postType.postServiceType, with: options, for: blog) { [weak self] posts in - postType.stopSyncingStatuses(toBeSynced, for: blog) - self?.notifyListenersOfPostsSync(success: true, blog: blog, postType: postType, posts: posts, for: toBeSynced) - } failure: { [weak self] error in + Task { @MainActor [weak self, postRepository, authorID = blog.userID, blogID = TaggedManagedObjectID(blog)] in + let success: Bool + do { + _ = try await postRepository.search( + type: postType == .post ? Post.self : Page.self, + input: nil, + statuses: toBeSynced, + tag: nil, + authorUserID: authorID, + offset: 0, + limit: Constants.numberOfPostsToSync, + orderBy: .byModified, + descending: true, + in: blogID + ) + success = true + } catch { + success = false + } + postType.stopSyncingStatuses(toBeSynced, for: blog) - self?.notifyListenersOfPostsSync(success: false, blog: blog, postType: postType, posts: nil, for: toBeSynced) + self?.notifyListenersOfPostsSync(success: success, blog: blog, postType: postType, for: toBeSynced) } } @@ -97,30 +104,20 @@ class DashboardPostsSyncManager { private func notifyListenersOfPostsSync(success: Bool, blog: Blog, postType: PostType, - posts: [AbstractPost]?, - for statuses: [String]) { + for statuses: [BasePost.Status]) { for aListener in listeners { - aListener.postsSynced(success: success, blog: blog, postType: postType, posts: posts, for: statuses) + aListener.postsSynced(success: success, blog: blog, postType: postType, for: statuses) } } enum Constants { - static let numberOfPostsToSync: NSNumber = 3 + static let numberOfPostsToSync: Int = 3 } } private extension DashboardPostsSyncManager.PostType { - var postServiceType: PostServiceType { - switch self { - case .post: - return .post - case .page: - return .page - } - } - - func statusesNotBeingSynced(_ statuses: [String], for blog: Blog) -> [String] { - var currentlySyncing: [String] + func statusesNotBeingSynced(_ statuses: [BasePost.Status], for blog: Blog) -> [BasePost.Status] { + var currentlySyncing: [BasePost.Status] switch self { case .post: currentlySyncing = blog.dashboardState.postsSyncingStatuses @@ -131,7 +128,7 @@ private extension DashboardPostsSyncManager.PostType { return notCurrentlySyncing } - func markStatusesAsBeingSynced(_ toBeSynced: [String], for blog: Blog) { + func markStatusesAsBeingSynced(_ toBeSynced: [BasePost.Status], for blog: Blog) { switch self { case .post: blog.dashboardState.postsSyncingStatuses.append(contentsOf: toBeSynced) @@ -140,7 +137,7 @@ private extension DashboardPostsSyncManager.PostType { } } - func stopSyncingStatuses(_ statuses: [String], for blog: Blog) { + func stopSyncingStatuses(_ statuses: [BasePost.Status], for blog: Blog) { switch self { case .post: blog.dashboardState.postsSyncingStatuses = blog.dashboardState.postsSyncingStatuses.filter({ !statuses.contains($0) }) diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Models/BlogDashboardAnalyticPropertiesProviding.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Models/BlogDashboardAnalyticPropertiesProviding.swift new file mode 100644 index 000000000000..4347807ae2e6 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Models/BlogDashboardAnalyticPropertiesProviding.swift @@ -0,0 +1,6 @@ +import Foundation + +protocol BlogDashboardAnalyticPropertiesProviding { + + var analyticProperties: [AnyHashable: Any] { get } +} diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Models/DashboardCard+Personalization.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Models/DashboardCard+Personalization.swift new file mode 100644 index 000000000000..9dea5d3f536c --- /dev/null +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Models/DashboardCard+Personalization.swift @@ -0,0 +1,52 @@ +import Foundation + +extension DashboardCard: BlogDashboardPersonalizable { + + var blogDashboardPersonalizationKey: String? { + switch self { + case .todaysStats: + return "todays-stats-card-enabled-site-settings" + case .draftPosts: + return "draft-posts-card-enabled-site-settings" + case .scheduledPosts: + return "scheduled-posts-card-enabled-site-settings" + case .blaze: + return "blaze-card-enabled-site-settings" + case .bloganuaryNudge: + return "bloganuary-nudge-card-enabled-site-settings" + case .prompts: + // Warning: there is an irregularity with the prompts key that doesn't + // have a "-card" component in the key name. Keeping it like this to + // avoid having to migrate data. + return "prompts-enabled-site-settings" + case .freeToPaidPlansDashboardCard: + return "free-to-paid-plans-dashboard-card-enabled-site-settings" + case .domainRegistration: + return "register-domain-dashboard-card" + case .googleDomains: + return "google-domains-card-enabled-site-settings" + case .activityLog: + return "activity-log-card-enabled-site-settings" + case .pages: + return "pages-card-enabled-site-settings" + case .quickStart: + // The "Quick Start" cell used to use `BlogDashboardPersonalizationService`. + // It no longer does, but it's important to keep the flag around for + // users that hidden it using this flag. + return "quick-start-card-enabled-site-settings" + case .dynamic, .jetpackBadge, .jetpackInstall, .jetpackSocial, .failure, .ghost, .personalize, .empty: + return nil + } + } + + /// Specifies whether the card settings should be applied across + /// different sites or only to a particular site. + var blogDashboardPersonalizationSettingsScope: BlogDashboardPersonalizationService.SettingsScope { + switch self { + case .googleDomains: + return .siteGeneric + default: + return .siteSpecific + } + } +} diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/DashboardCard.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Models/DashboardCard.swift similarity index 71% rename from WordPress/Classes/ViewRelated/Blog/Blog Dashboard/DashboardCard.swift rename to WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Models/DashboardCard.swift index 476e19501558..1068063e1ab6 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/DashboardCard.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Models/DashboardCard.swift @@ -7,8 +7,10 @@ import Foundation /// /// Remote cards should be separately added to RemoteDashboardCard enum DashboardCard: String, CaseIterable { + case dynamic case jetpackInstall case quickStart + case bloganuaryNudge = "bloganuary_nudge" case prompts case googleDomains case blaze @@ -31,6 +33,8 @@ enum DashboardCard: String, CaseIterable { var cell: DashboardCollectionViewCell.Type { switch self { + case .dynamic: + return BlogDashboardDynamicCardCell.self case .jetpackInstall: return DashboardJetpackInstallCardCell.self case .quickStart: @@ -41,6 +45,8 @@ enum DashboardCard: String, CaseIterable { return DashboardScheduledPostsCardCell.self case .todaysStats: return DashboardStatsCardCell.self + case .bloganuaryNudge: + return DashboardBloganuaryCardCell.self case .prompts: return DashboardPromptsCardCell.self case .ghost: @@ -81,18 +87,20 @@ enum DashboardCard: String, CaseIterable { } } - /// Specifies whether the card settings should be applied across - /// different sites or only to a particular site. - var settingsType: SettingsType { - switch self { - case .googleDomains: - return .siteGeneric - default: - return .siteSpecific - } - } - - func shouldShow(for blog: Blog, apiResponse: BlogDashboardRemoteEntity? = nil) -> Bool { + func shouldShow( + for blog: Blog, + apiResponse: BlogDashboardRemoteEntity? = nil, + // The following three parameter should not have default values. + // Unfortunately, this method is called many times because the type is an enum with many cases^. + // + // At the time of writing, the priority is addressing a test failure and pave the way for better testability. + // As such, we are leaving default values to keep compatibility with the existing code. + // + // ^ – See the following article for a better way to distribute configurations https://www.jessesquires.com/blog/2016/07/31/enums-as-configs/ + isJetpack: Bool = AppConfiguration.isJetpack, + isDotComAvailable: Bool = AccountHelper.isDotcomAvailable(), + shouldShowJetpackFeatures: Bool = JetpackFeaturesRemovalCoordinator.shouldShowJetpackFeatures() + ) -> Bool { switch self { case .jetpackInstall: return JetpackInstallPluginHelper.shouldShowCard(for: blog) @@ -102,6 +110,8 @@ enum DashboardCard: String, CaseIterable { return shouldShowRemoteCard(apiResponse: apiResponse) case .todaysStats: return DashboardStatsCardCell.shouldShowCard(for: blog) && shouldShowRemoteCard(apiResponse: apiResponse) + case .bloganuaryNudge: + return DashboardBloganuaryCardCell.shouldShowCard(for: blog) case .prompts: return DashboardPromptsCardCell.shouldShowCard(for: blog) case .ghost: @@ -109,7 +119,11 @@ enum DashboardCard: String, CaseIterable { case .failure: return blog.dashboardState.isFirstLoadFailure case .jetpackBadge: - return JetpackBrandingVisibility.all.enabled + return JetpackBrandingVisibility.all.isEnabled( + isWordPress: isJetpack == false, + isDotComAvailable: isDotComAvailable, + shouldShowJetpackFeatures: shouldShowJetpackFeatures + ) case .blaze: return BlazeHelper.shouldShowCard(for: blog) case .freeToPaidPlansDashboardCard: @@ -119,7 +133,7 @@ enum DashboardCard: String, CaseIterable { case .empty: return false // Controlled manually based on other cards visibility case .personalize: - return FeatureFlag.personalizeHomeTab.enabled + return true case .pages: return DashboardPagesListCardCell.shouldShowCard(for: blog) && shouldShowRemoteCard(apiResponse: apiResponse) case .activityLog: @@ -127,10 +141,29 @@ enum DashboardCard: String, CaseIterable { case .jetpackSocial: return DashboardJetpackSocialCardCell.shouldShowCard(for: blog) case .googleDomains: - return FeatureFlag.domainFocus.enabled && AppConfiguration.isJetpack + return FeatureFlag.googleDomainsCard.enabled && isJetpack + case .dynamic: + return false } } + static func shouldShowDynamicCard( + for blog: Blog, + payload: DashboardDynamicCardModel.Payload, + remoteFeatureFlagStore: RemoteFeatureFlagStore, + isJetpack: Bool = AppConfiguration.isJetpack + ) -> Bool { + let remoteFeatureFlagEnabled = { + guard let key = payload.remoteFeatureFlag else { + return true + } + return remoteFeatureFlagStore.value(for: key) ?? false + }() + return isJetpack + && RemoteDashboardCard.dynamic.supported(by: blog) + && remoteFeatureFlagEnabled + } + private func shouldShowRemoteCard(apiResponse: BlogDashboardRemoteEntity?) -> Bool { guard let apiResponse = apiResponse else { return false @@ -169,6 +202,7 @@ enum DashboardCard: String, CaseIterable { case posts case pages case activity + case dynamic func supported(by blog: Blog) -> Bool { switch self { @@ -180,14 +214,11 @@ enum DashboardCard: String, CaseIterable { return DashboardPagesListCardCell.shouldShowCard(for: blog) case .activity: return DashboardActivityLogCardCell.shouldShowCard(for: blog) + case .dynamic: + return RemoteFeatureFlag.dynamicDashboardCards.enabled() } } } - - enum SettingsType { - case siteSpecific - case siteGeneric - } } private extension BlogDashboardRemoteEntity { @@ -211,3 +242,12 @@ private extension BlogDashboardRemoteEntity { return (self.activity?.value?.current?.orderedItems?.count ?? 0) > 0 } } + +// MARK: - BlogDashboardAnalyticPropertiesProviding Protocol Conformance + +extension DashboardCard: BlogDashboardAnalyticPropertiesProviding { + + var analyticProperties: [AnyHashable: Any] { + return ["card": rawValue] + } +} diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Models/DashboardCardModel.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Models/DashboardCardModel.swift new file mode 100644 index 000000000000..ecbba65b3935 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Models/DashboardCardModel.swift @@ -0,0 +1,133 @@ +import Foundation + +enum DashboardCardModel: Hashable { + + case normal(DashboardNormalCardModel) + case dynamic(DashboardDynamicCardModel) + + var cardType: DashboardCard { + switch self { + case .normal(let model): return model.cardType + case .dynamic(let model): return model.cardType + } + } + + func normal() -> DashboardNormalCardModel? { + guard case .normal(let model) = self else { + return nil + } + return model + } + + func dynamic() -> DashboardDynamicCardModel? { + guard case .dynamic(let model) = self else { + return nil + } + return model + } +} + +extension DashboardCardModel: BlogDashboardPersonalizable, BlogDashboardAnalyticPropertiesProviding { + + private var card: BlogDashboardPersonalizable & BlogDashboardAnalyticPropertiesProviding { + switch self { + case .normal(let model): return model + case .dynamic(let model): return model + } + } + + var blogDashboardPersonalizationKey: String? { + return card.blogDashboardPersonalizationKey + } + + var blogDashboardPersonalizationSettingsScope: BlogDashboardPersonalizationService.SettingsScope { + return card.blogDashboardPersonalizationSettingsScope + } + + var analyticProperties: [AnyHashable: Any] { + return card.analyticProperties + } +} + +// MARK: - Normal Card Model + +/// Represents a card in the dashboard collection view +struct DashboardNormalCardModel: Hashable { + let cardType: DashboardCard + let dotComID: Int + let apiResponse: BlogDashboardRemoteEntity? + + /** + Initializes a new DashboardCardModel, used as a model for each dashboard card. + + - Parameters: + - id: The `DashboardCard` id of this card + - dotComID: The blog id for the blog associated with this card + - entity: A `BlogDashboardRemoteEntity?` property + + - Returns: A `DashboardCardModel` that is used by the dashboard diffable collection + view. The `id`, `dotComID` and the `entity` is used to differentiate one + card from the other. + */ + init(cardType: DashboardCard, dotComID: Int, entity: BlogDashboardRemoteEntity? = nil) { + self.cardType = cardType + self.dotComID = dotComID + self.apiResponse = entity + } + + static func == (lhs: DashboardNormalCardModel, rhs: DashboardNormalCardModel) -> Bool { + lhs.cardType == rhs.cardType && + lhs.dotComID == rhs.dotComID && + lhs.apiResponse == rhs.apiResponse + } + + func hash(into hasher: inout Hasher) { + hasher.combine(cardType) + hasher.combine(dotComID) + hasher.combine(apiResponse) + } +} + +extension DashboardNormalCardModel: BlogDashboardPersonalizable, BlogDashboardAnalyticPropertiesProviding { + + var blogDashboardPersonalizationKey: String? { + return cardType.blogDashboardPersonalizationKey + } + + var blogDashboardPersonalizationSettingsScope: BlogDashboardPersonalizationService.SettingsScope { + return cardType.blogDashboardPersonalizationSettingsScope + } + + var analyticProperties: [AnyHashable: Any] { + return cardType.analyticProperties + } +} + +// MARK: - Dynamic Card Model + +struct DashboardDynamicCardModel: Hashable { + + typealias Payload = BlogDashboardRemoteEntity.BlogDashboardDynamic + + let cardType: DashboardCard = .dynamic + let payload: Payload + let dotComID: Int +} + +extension DashboardDynamicCardModel: BlogDashboardPersonalizable, BlogDashboardAnalyticPropertiesProviding { + + var blogDashboardPersonalizationKey: String? { + return "dynamic_card_\(payload.id)" + } + + var blogDashboardPersonalizationSettingsScope: BlogDashboardPersonalizationService.SettingsScope { + return .siteGeneric + } + + var analyticProperties: [AnyHashable: Any] { + let properties: [AnyHashable: Any] = ["id": payload.id] + return cardType.analyticProperties.merging(properties, uniquingKeysWith: { first, second in + return first + }) + } +} diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Service/BlogDashboardPersonalizationService.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Service/BlogDashboardPersonalizationService.swift index c8c838ae7e3c..290e2bb01adc 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Service/BlogDashboardPersonalizationService.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Service/BlogDashboardPersonalizationService.swift @@ -1,5 +1,18 @@ import Foundation +/// `BlogDashboardPersonalizable` is a protocol that defines the requirements for personalizing blog dashboard items. +/// It provides properties to access personalization key and settings scope specific to the blog dashboard. +protocol BlogDashboardPersonalizable { + + /// The personalization key for the blog dashboard. + /// This key is used to identify and retrieve personalization settings specific to a dashboard item. + var blogDashboardPersonalizationKey: String? { get } + + /// The scope of the blog dashboard personalization settings. + /// This defines the extent to which the personalization settings are applied, such as site-agnostic or site-specific. + var blogDashboardPersonalizationSettingsScope: BlogDashboardPersonalizationService.SettingsScope { get } +} + /// Manages dashboard settings such as card visibility. struct BlogDashboardPersonalizationService { private enum Constants { @@ -15,6 +28,47 @@ struct BlogDashboardPersonalizationService { self.siteID = String(siteID) } + // MARK: - Core API + + func setEnabled(_ isEnabled: Bool, forKey key: String, scope: SettingsScope) { + var settings = getSettings(for: key) + let lookUpKey = lookUpKey(from: scope) + settings[lookUpKey] = isEnabled + self.repository.set(settings, forKey: key) + NotificationCenter.default.post(name: .blogDashboardPersonalizationSettingsChanged, object: self) + } + + func isEnabled(_ key: String, scope: SettingsScope) -> Bool { + let settings = getSettings(for: key) + let key = lookUpKey(from: scope) + return settings[key, default: true] + } + + func setEnabled(_ isEnabled: Bool, for item: BlogDashboardPersonalizable) { + guard let key = item.blogDashboardPersonalizationKey else { + return + } + self.setEnabled( + isEnabled, + forKey: key, + scope: item.blogDashboardPersonalizationSettingsScope + ) + } + + func isEnabled(_ item: BlogDashboardPersonalizable) -> Bool { + guard let key = item.blogDashboardPersonalizationKey else { + return true + } + return self.isEnabled(key, scope: item.blogDashboardPersonalizationSettingsScope) + } + + private func lookUpKey(from scope: SettingsScope) -> String { + switch scope { + case .siteSpecific: return siteID + case .siteGeneric: return Constants.siteAgnosticVisibilityKey + } + } + // MARK: - Quick Actions func isEnabled(_ action: DashboardQuickAction) -> Bool { @@ -39,8 +93,7 @@ struct BlogDashboardPersonalizationService { // MARK: - Dashboard Cards func isEnabled(_ card: DashboardCard) -> Bool { - let key = lookUpKey(for: card) - return getSettings(for: card)[key] ?? true + return self.isEnabled(card as BlogDashboardPersonalizable) } func hasPreference(for card: DashboardCard) -> Bool { @@ -57,30 +110,25 @@ struct BlogDashboardPersonalizationService { /// - isEnabled: A Boolean value indicating whether the `DashboardCard` should be enabled or disabled. /// - card: The `DashboardCard` whose setting needs to be updated. func setEnabled(_ isEnabled: Bool, for card: DashboardCard) { - guard let key = makeKey(for: card) else { return } - var settings = getSettings(for: card) - let lookUpKey = lookUpKey(for: card) - settings[lookUpKey] = isEnabled - - repository.set(settings, forKey: key) - - DispatchQueue.main.async { - NotificationCenter.default.post(name: .blogDashboardPersonalizationSettingsChanged, object: self) - } + self.setEnabled(isEnabled, for: card as BlogDashboardPersonalizable) } private func getSettings(for card: DashboardCard) -> [String: Bool] { - guard let key = makeKey(for: card) else { return [:] } + guard let key = card.blogDashboardPersonalizationKey else { + return [:] + } return repository.dictionary(forKey: key) as? [String: Bool] ?? [:] } private func lookUpKey(for card: DashboardCard) -> String { - switch card.settingsType { - case .siteSpecific: - return siteID - case .siteGeneric: - return Constants.siteAgnosticVisibilityKey - } + return lookUpKey(from: card.blogDashboardPersonalizationSettingsScope) + } + + // MARK: - Types + + enum SettingsScope { + case siteSpecific + case siteGeneric } } @@ -88,41 +136,6 @@ private func makeKey(for action: DashboardQuickAction) -> String { "quick-action-\(action.rawValue)-hidden" } -private func makeKey(for card: DashboardCard) -> String? { - switch card { - case .todaysStats: - return "todays-stats-card-enabled-site-settings" - case .draftPosts: - return "draft-posts-card-enabled-site-settings" - case .scheduledPosts: - return "scheduled-posts-card-enabled-site-settings" - case .blaze: - return "blaze-card-enabled-site-settings" - case .prompts: - // Warning: there is an irregularity with the prompts key that doesn't - // have a "-card" component in the key name. Keeping it like this to - // avoid having to migrate data. - return "prompts-enabled-site-settings" - case .freeToPaidPlansDashboardCard: - return "free-to-paid-plans-dashboard-card-enabled-site-settings" - case .domainRegistration: - return "register-domain-dashboard-card" - case .googleDomains: - return "google-domains-card-enabled-site-settings" - case .activityLog: - return "activity-log-card-enabled-site-settings" - case .pages: - return "pages-card-enabled-site-settings" - case .quickStart: - // The "Quick Start" cell used to use `BlogDashboardPersonalizationService`. - // It no longer does, but it's important to keep the flag around for - // users that hidden it using this flag. - return "quick-start-card-enabled-site-settings" - case .jetpackBadge, .jetpackInstall, .jetpackSocial, .failure, .ghost, .personalize, .empty: - return nil - } -} - extension NSNotification.Name { /// Sent whenever any of the blog settings managed by ``BlogDashboardPersonalizationService`` /// are changed. diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Service/BlogDashboardRemoteEntity.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Service/BlogDashboardRemoteEntity.swift index ef5f94260f58..864184d32edb 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Service/BlogDashboardRemoteEntity.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Service/BlogDashboardRemoteEntity.swift @@ -6,6 +6,7 @@ struct BlogDashboardRemoteEntity: Decodable, Hashable { var todaysStats: FailableDecodable? var pages: FailableDecodable<[BlogDashboardPage]>? var activity: FailableDecodable? + var dynamic: FailableDecodable<[BlogDashboardDynamic]>? struct BlogDashboardPosts: Decodable, Hashable { var hasPublished: Bool? @@ -45,6 +46,46 @@ struct BlogDashboardRemoteEntity: Decodable, Hashable { case todaysStats = "todays_stats" case pages case activity + case dynamic + } +} + +// MARK: - Dynamic Card + +extension BlogDashboardRemoteEntity { + + struct BlogDashboardDynamic: Decodable, Hashable { + + let id: String + let remoteFeatureFlag: String? + let title: String? + let featuredImage: String? + let url: String? + let action: String? + let order: Order? + let rows: [Row]? + + enum Order: String, Decodable { + case top + case bottom + } + + struct Row: Decodable, Hashable { + let title: String? + let description: String? + let icon: String? + } + + private enum CodingKeys: String, CodingKey { + case id + case title + case remoteFeatureFlag = "remote_feature_flag" + case featuredImage = "featured_image" + case url + case action + case order + case rows + } } } diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Service/BlogDashboardService.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Service/BlogDashboardService.swift index 07a12b8692e5..a60b58666451 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Service/BlogDashboardService.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Service/BlogDashboardService.swift @@ -7,16 +7,30 @@ final class BlogDashboardService { private let persistence: BlogDashboardPersistence private let postsParser: BlogDashboardPostsParser private let repository: UserPersistentRepository - - init(managedObjectContext: NSManagedObjectContext, - remoteService: DashboardServiceRemote? = nil, - persistence: BlogDashboardPersistence = BlogDashboardPersistence(), - repository: UserPersistentRepository = UserDefaults.standard, - postsParser: BlogDashboardPostsParser? = nil) { + private let remoteFeatureFlagStore: RemoteFeatureFlagStore + private let isJetpack: Bool + private let isDotComAvailable: Bool + private let shouldShowJetpackFeatures: Bool + + init( + managedObjectContext: NSManagedObjectContext, + isJetpack: Bool, + isDotComAvailable: Bool, + shouldShowJetpackFeatures: Bool, + remoteService: DashboardServiceRemote? = nil, + persistence: BlogDashboardPersistence = BlogDashboardPersistence(), + repository: UserPersistentRepository = UserDefaults.standard, + postsParser: BlogDashboardPostsParser? = nil, + remoteFeatureFlagStore: RemoteFeatureFlagStore = .init() + ) { + self.isJetpack = isJetpack + self.isDotComAvailable = isDotComAvailable + self.shouldShowJetpackFeatures = shouldShowJetpackFeatures self.remoteService = remoteService ?? DashboardServiceRemote(wordPressComRestApi: WordPressComRestApi.defaultApi(in: managedObjectContext, localeKey: WordPressComRestApi.LocaleKeyV2)) self.persistence = persistence self.repository = repository self.postsParser = postsParser ?? BlogDashboardPostsParser(managedObjectContext: managedObjectContext) + self.remoteFeatureFlagStore = remoteFeatureFlagStore } /// Fetch cards from remote @@ -64,7 +78,7 @@ final class BlogDashboardService { /// Fetch cards from local func fetchLocal(blog: Blog) -> [DashboardCardModel] { - guard let dotComID = blog.dotComID?.intValue else { + guard AccountHelper.isDotcomAvailable(), let dotComID = blog.dotComID?.intValue else { return [] } @@ -86,17 +100,101 @@ private extension BlogDashboardService { func parse(_ entity: BlogDashboardRemoteEntity?, blog: Blog, dotComID: Int) -> [DashboardCardModel] { let personalizationService = BlogDashboardPersonalizationService(repository: repository, siteID: dotComID) - var cards: [DashboardCardModel] = DashboardCard.allCases.compactMap { card in - guard personalizationService.isEnabled(card), - card.shouldShow(for: blog, apiResponse: entity) else { + + // Map `DashboardCard` instances to `DashboardCardModel` + var allCards: [DashboardCardModel] = DashboardCard.allCases.compactMap { card -> DashboardCardModel? in + guard card != .dynamic else { return nil } - return DashboardCardModel(cardType: card, dotComID: dotComID, entity: entity) + return self.dashboardCardModel( + from: card, + entity: entity, + blog: blog, + dotComID: dotComID, + personalizationService: personalizationService + ) + } + + // Maps dynamic cards to `DashboardCardModel`. + if let dynamic = entity?.dynamic?.value { + let cards = dynamic.compactMap { payload in + return self.dashboardCardModel( + for: blog, + payload: payload, + dotComID: dotComID, + personalizationService: personalizationService + ) + } + let cardsByOrder = Dictionary(grouping: cards) { card -> BlogDashboardRemoteEntity.BlogDashboardDynamic.Order in + guard case .dynamic(let model) = card, let order = model.payload.order else { + return .bottom + } + return order + } + let topCards = cardsByOrder[.top, default: []] + let bottomCards = cardsByOrder[.bottom, default: []] + + // Adds "top" cards at the beginning of the list. + allCards = topCards + allCards + + // Adds "bottom" cards at the bottom of the list just before "personalize" card. + if allCards.last?.cardType == .personalize { + allCards.insert(contentsOf: bottomCards, at: allCards.endIndex - 1) + } else { + allCards = allCards + bottomCards + } + } + + // Add "empty" card if the list of cards is empty. + if allCards.isEmpty || allCards.map(\.cardType) == [.personalize] { + let model = DashboardCardModel.normal(.init(cardType: .empty, dotComID: dotComID)) + allCards.insert(model, at: 0) } - if cards.isEmpty || cards.map(\.cardType) == [.personalize] { - cards.insert(DashboardCardModel(cardType: .empty, dotComID: dotComID), at: 0) + + return allCards + } + + func dashboardCardModel( + from card: DashboardCard, + entity: BlogDashboardRemoteEntity?, + blog: Blog, + dotComID: Int, + personalizationService: BlogDashboardPersonalizationService + ) -> DashboardCardModel? { + guard personalizationService.isEnabled(card) else { + return nil + } + + guard card.shouldShow( + for: blog, + apiResponse: entity, + isJetpack: isJetpack, + isDotComAvailable: isDotComAvailable, + shouldShowJetpackFeatures: shouldShowJetpackFeatures + ) else { + return nil + } + + return .normal(.init(cardType: card, dotComID: dotComID, entity: entity)) + } + + func dashboardCardModel( + for blog: Blog, + payload: DashboardDynamicCardModel.Payload, + dotComID: Int, + personalizationService: BlogDashboardPersonalizationService + ) -> DashboardCardModel? { + let model = DashboardDynamicCardModel(payload: payload, dotComID: dotComID) + let shouldShow = DashboardCard.shouldShowDynamicCard( + for: blog, + payload: payload, + remoteFeatureFlagStore: remoteFeatureFlagStore, + isJetpack: isJetpack + ) + guard shouldShow, personalizationService.isEnabled(model) else { + return nil } - return cards + return .dynamic(model) } func decode(_ cardsDictionary: NSDictionary, blog: Blog) -> BlogDashboardRemoteEntity? { diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/ViewModel/BlogDashboardViewModel.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/ViewModel/BlogDashboardViewModel.swift index e00e5e67ff16..d47c8e3f1356 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/ViewModel/BlogDashboardViewModel.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/ViewModel/BlogDashboardViewModel.swift @@ -1,6 +1,7 @@ import Foundation import UIKit import CoreData +import WordPressKit enum DashboardSection: Int, CaseIterable { case migrationSuccess @@ -28,20 +29,25 @@ final class BlogDashboardViewModel { private var currentCards: [DashboardCardModel] = [] - private lazy var draftStatusesToSync: [String] = { - return PostListFilter.draftFilter().statuses.strings + private lazy var draftStatusesToSync: [BasePost.Status] = { + return PostListFilter.draftFilter().statuses }() - private lazy var scheduledStatusesToSync: [String] = { - return PostListFilter.scheduledFilter().statuses.strings + private lazy var scheduledStatusesToSync: [BasePost.Status] = { + return PostListFilter.scheduledFilter().statuses }() - private lazy var pageStatusesToSync: [String] = { - return PostListFilter.allNonTrashedFilter().statuses.strings + private lazy var pageStatusesToSync: [BasePost.Status] = { + return PostListFilter.allNonTrashedFilter().statuses }() private lazy var service: BlogDashboardService = { - return BlogDashboardService(managedObjectContext: managedObjectContext) + return BlogDashboardService( + managedObjectContext: managedObjectContext, + isJetpack: AppConfiguration.isJetpack, + isDotComAvailable: AccountHelper.isDotcomAvailable(), + shouldShowJetpackFeatures: JetpackFeaturesRemovalCoordinator.shouldShowJetpackFeatures() + ) }() private lazy var dataSource: DashboardDataSource? = { @@ -65,7 +71,7 @@ final class BlogDashboardViewModel { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellType.defaultReuseID, for: indexPath) if var cellConfigurable = cell as? BlogDashboardCardConfigurable { cellConfigurable.row = indexPath.row - cellConfigurable.configure(blog: blog, viewController: viewController, apiResponse: cardModel.apiResponse) + cellConfigurable.configure(blog: blog, viewController: viewController, model: cardModel) } (cell as? DashboardBlazeCardCell)?.configure(blazeViewModel) return cell diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/ViewModel/DashboardCardModel.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/ViewModel/DashboardCardModel.swift deleted file mode 100644 index 90631c0b7389..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/ViewModel/DashboardCardModel.swift +++ /dev/null @@ -1,38 +0,0 @@ -import Foundation - -/// Represents a card in the dashboard collection view -struct DashboardCardModel: Hashable { - let cardType: DashboardCard - let dotComID: Int - let apiResponse: BlogDashboardRemoteEntity? - - /** - Initializes a new DashboardCardModel, used as a model for each dashboard card. - - - Parameters: - - id: The `DashboardCard` id of this card - - dotComID: The blog id for the blog associated with this card - - entity: A `BlogDashboardRemoteEntity?` property - - - Returns: A `DashboardCardModel` that is used by the dashboard diffable collection - view. The `id`, `dotComID` and the `entity` is used to differentiate one - card from the other. - */ - init(cardType: DashboardCard, dotComID: Int, entity: BlogDashboardRemoteEntity? = nil) { - self.cardType = cardType - self.dotComID = dotComID - self.apiResponse = entity - } - - static func == (lhs: DashboardCardModel, rhs: DashboardCardModel) -> Bool { - lhs.cardType == rhs.cardType && - lhs.dotComID == rhs.dotComID && - lhs.apiResponse == rhs.apiResponse - } - - func hash(into hasher: inout Hasher) { - hasher.combine(cardType) - hasher.combine(dotComID) - hasher.combine(apiResponse) - } -} diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+DomainCredit.swift b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+DomainCredit.swift deleted file mode 100644 index 023947bda2c6..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+DomainCredit.swift +++ /dev/null @@ -1,72 +0,0 @@ -import Gridicons -import SwiftUI -import WordPressFlux - -extension BlogDetailsViewController { - @objc func domainCreditSectionViewModel() -> BlogDetailsSection { - let image = UIImage.gridicon(.info) - let row = BlogDetailsRow(title: NSLocalizedString("Register Domain", comment: "Action to redeem domain credit."), - accessibilityIdentifier: "Register domain from site dashboard", - image: image, - imageColor: UIColor.warning(.shade20)) { [weak self] in - WPAnalytics.track(.domainCreditRedemptionTapped) - self?.showDomainCreditRedemption() - } - row.showsDisclosureIndicator = false - row.showsSelectionState = false - return BlogDetailsSection(title: nil, - rows: [row], - footerTitle: NSLocalizedString("All WordPress.com plans include a custom domain name. Register your free premium domain now.", comment: "Information about redeeming domain credit on site dashboard."), - category: .domainCredit) - } - - @objc func showDomainCreditRedemption() { - let controller = RegisterDomainSuggestionsViewController - .instance(site: blog, domainSelectionType: .registerWithPaidPlan, domainPurchasedCallback: { [weak self] _, domain in - WPAnalytics.track(.domainCreditRedemptionSuccess) - self?.presentDomainCreditRedemptionSuccess(domain: domain) - }) - let navigationController = UINavigationController(rootViewController: controller) - present(navigationController, animated: true) - } - - private func presentDomainCreditRedemptionSuccess(domain: String) { - let controller = DomainCreditRedemptionSuccessViewController(domain: domain) { [weak self] _ in - self?.dismiss(animated: true) { - guard let email = self?.accountEmail() else { - return - } - let title = String(format: NSLocalizedString("Verify your email address - instructions sent to %@", comment: "Notice displayed after domain credit redemption success."), email) - ActionDispatcher.dispatch(NoticeAction.post(Notice(title: title))) - } - } - present(controller, animated: true) { [weak self] in - self?.updateTableView { - guard - let parent = self?.parent as? MySiteViewController, - let blog = self?.blog - else { - return - } - parent.sitePickerViewController?.blogDetailHeaderView.blog = blog - } - } - } - - private func accountEmail() -> String? { - guard let defaultAccount = try? WPAccount.lookupDefaultWordPressComAccount(in: ContextManager.shared.mainContext) else { - return nil - } - return defaultAccount.email - } -} - -// MARK: - Domains Dashboard access from My Site -extension BlogDetailsViewController { - - @objc func makeDomainsDashboardViewController() -> UIViewController { - let viewController = UIHostingController(rootView: DomainsDashboardView(blog: self.blog)) - viewController.extendedLayoutIncludesOpaqueBars = true - return viewController - } -} diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+FancyAlerts.swift b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+FancyAlerts.swift index 84f9fac07fb9..2a5a60d6992d 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+FancyAlerts.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+FancyAlerts.swift @@ -53,15 +53,6 @@ extension BlogDetailsViewController { alertWorkItem = nil } - private var noPresentedViewControllers: Bool { - guard let window = WordPressAppDelegate.shared?.window, - let rootViewController = window.rootViewController, - rootViewController.presentedViewController != nil else { - return true - } - return false - } - private func showNoticeAsNeeded() { let quickStartGuide = QuickStartTourGuide.shared diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+QuickActions.swift b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+QuickActions.swift deleted file mode 100644 index 3be444542c2f..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+QuickActions.swift +++ /dev/null @@ -1,86 +0,0 @@ -import UIKit - -// TODO: Consider completely removing all Quick Action logic -extension BlogDetailsViewController { - - @objc func quickActionsSectionViewModel() -> BlogDetailsSection { - let row = BlogDetailsRow() - row.callback = {} - return BlogDetailsSection(title: nil, - rows: [row], - footerTitle: nil, - category: .quickAction) - } - - @objc func isAccessibilityCategoryEnabled() -> Bool { - tableView.traitCollection.preferredContentSizeCategory.isAccessibilityCategory - } - - @objc func configureQuickActions(cell: QuickActionsCell) { - let actionItems = createActionItems() - - cell.configure(with: actionItems) - } - - private func createActionItems() -> [ActionRow.Item] { - let actionItems: [ActionRow.Item] = [ - .init(image: .gridicon(.statsAlt), title: NSLocalizedString("Stats", comment: "Noun. Abbv. of Statistics. Links to a blog's Stats screen.")) { [weak self] in - self?.tableView.deselectSelectedRowWithAnimation(false) - self?.showStats(from: .button) - }, - .init(image: .gridicon(.posts), title: NSLocalizedString("Posts", comment: "Noun. Title. Links to the blog's Posts screen.")) { [weak self] in - self?.tableView.deselectSelectedRowWithAnimation(false) - self?.showPostList(from: .button) - }, - .init(image: .gridicon(.image), title: NSLocalizedString("Media", comment: "Noun. Title. Links to the blog's Media library.")) { [weak self] in - self?.tableView.deselectSelectedRowWithAnimation(false) - self?.showMediaLibrary(from: .button) - }, - .init(image: .gridicon(.pages), title: NSLocalizedString("Pages", comment: "Noun. Title. Links to the blog's Pages screen.")) { [weak self] in - self?.tableView.deselectSelectedRowWithAnimation(false) - self?.showPageList(from: .button) - } - ] - - return actionItems - } -} - -@objc class QuickActionsCell: UITableViewCell { - private var actionRow: ActionRow! - - func configure(with items: [ActionRow.Item]) { - guard actionRow == nil else { - return - } - - actionRow = ActionRow(items: items) - contentView.addSubview(actionRow) - - setupConstraints() - setupCell() - } - - private func setupConstraints() { - actionRow.translatesAutoresizingMaskIntoConstraints = false - - let widthConstraint = actionRow.widthAnchor.constraint(equalToConstant: Constants.maxQuickActionsWidth) - widthConstraint.priority = .defaultHigh - - NSLayoutConstraint.activate([ - actionRow.topAnchor.constraint(equalTo: contentView.topAnchor), - actionRow.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), - actionRow.leadingAnchor.constraint(greaterThanOrEqualTo: contentView.leadingAnchor), - actionRow.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), - widthConstraint - ]) - } - - private func setupCell() { - selectionStyle = .none - } - - private enum Constants { - static let maxQuickActionsWidth: CGFloat = 390 - } -} diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.h b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.h index ca33d35310de..794d22b2a3ae 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.h +++ b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.h @@ -7,7 +7,6 @@ @protocol BlogDetailHeader; typedef NS_ENUM(NSUInteger, BlogDetailsSectionCategory) { - BlogDetailsSectionCategoryQuickAction, BlogDetailsSectionCategoryReminders, BlogDetailsSectionCategoryDomainCredit, BlogDetailsSectionCategoryQuickStart, @@ -21,6 +20,7 @@ typedef NS_ENUM(NSUInteger, BlogDetailsSectionCategory) { BlogDetailsSectionCategoryMigrationSuccess, BlogDetailsSectionCategoryJetpackBrandingCard, BlogDetailsSectionCategoryJetpackInstallCard, + BlogDetailsSectionCategorySotW2023Card, BlogDetailsSectionCategoryContent, BlogDetailsSectionCategoryTraffic, BlogDetailsSectionCategoryMaintenance @@ -171,6 +171,7 @@ typedef NS_ENUM(NSUInteger, BlogDetailsNavigationSource) { - (id _Nonnull)init; - (void)showDetailViewForSubsection:(BlogDetailsSubsection)section; +- (void)showDetailViewForSubsection:(BlogDetailsSubsection)section userInfo:(nonnull NSDictionary *)userInfo; - (NSIndexPath * _Nonnull)indexPathForSubsection:(BlogDetailsSubsection)subsection; - (void)reloadTableViewPreservingSelection; - (void)configureTableViewData; @@ -185,4 +186,7 @@ typedef NS_ENUM(NSUInteger, BlogDetailsNavigationSource) { - (void)updateTableView:(nullable void(^)(void))completion; - (void)preloadMetadata; - (void)pulledToRefreshWith:(nonnull UIRefreshControl *)refreshControl onCompletion:(nullable void(^)(void))completion; + ++ (nonnull NSString *)userInfoShowPickerKey; + @end diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.m b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.m index dc2817985e3a..8e8c732616bb 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.m +++ b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.m @@ -24,7 +24,6 @@ static NSString *const BlogDetailsPlanCellIdentifier = @"BlogDetailsPlanCell"; static NSString *const BlogDetailsSettingsCellIdentifier = @"BlogDetailsSettingsCell"; static NSString *const BlogDetailsRemoveSiteCellIdentifier = @"BlogDetailsRemoveSiteCell"; -static NSString *const BlogDetailsQuickActionsCellIdentifier = @"BlogDetailsQuickActionsCell"; static NSString *const BlogDetailsSectionHeaderViewIdentifier = @"BlogDetailsSectionHeaderView"; static NSString *const QuickStartHeaderViewNibName = @"BlogDetailsSectionHeaderView"; static NSString *const BlogDetailsQuickStartCellIdentifier = @"BlogDetailsQuickStartCell"; @@ -32,6 +31,7 @@ static NSString *const BlogDetailsMigrationSuccessCellIdentifier = @"BlogDetailsMigrationSuccessCell"; static NSString *const BlogDetailsJetpackBrandingCardCellIdentifier = @"BlogDetailsJetpackBrandingCardCellIdentifier"; static NSString *const BlogDetailsJetpackInstallCardCellIdentifier = @"BlogDetailsJetpackInstallCardCellIdentifier"; +static NSString *const BlogDetailsSotWCardCellIdentifier = @"BlogDetailsSotWCardCellIdentifier"; NSString * const WPBlogDetailsRestorationID = @"WPBlogDetailsID"; NSString * const WPBlogDetailsBlogKey = @"WPBlogDetailsBlogKey"; @@ -376,7 +376,6 @@ - (void)viewDidLoad [self.tableView registerClass:[WPTableViewCellValue1 class] forCellReuseIdentifier:BlogDetailsPlanCellIdentifier]; [self.tableView registerClass:[WPTableViewCellValue1 class] forCellReuseIdentifier:BlogDetailsSettingsCellIdentifier]; [self.tableView registerClass:[WPTableViewCell class] forCellReuseIdentifier:BlogDetailsRemoveSiteCellIdentifier]; - [self.tableView registerClass:[QuickActionsCell class] forCellReuseIdentifier:BlogDetailsQuickActionsCellIdentifier]; UINib *qsHeaderViewNib = [UINib nibWithNibName:QuickStartHeaderViewNibName bundle:[NSBundle mainBundle]]; [self.tableView registerNib:qsHeaderViewNib forHeaderFooterViewReuseIdentifier:BlogDetailsSectionHeaderViewIdentifier]; [self.tableView registerClass:[QuickStartCell class] forCellReuseIdentifier:BlogDetailsQuickStartCellIdentifier]; @@ -384,7 +383,8 @@ - (void)viewDidLoad [self.tableView registerClass:[MigrationSuccessCell class] forCellReuseIdentifier:BlogDetailsMigrationSuccessCellIdentifier]; [self.tableView registerClass:[JetpackBrandingMenuCardCell class] forCellReuseIdentifier:BlogDetailsJetpackBrandingCardCellIdentifier]; [self.tableView registerClass:[JetpackRemoteInstallTableViewCell class] forCellReuseIdentifier:BlogDetailsJetpackInstallCardCellIdentifier]; - + [self.tableView registerClass:[SotWTableViewCell class] forCellReuseIdentifier:BlogDetailsSotWCardCellIdentifier]; + self.tableView.cellLayoutMarginsFollowReadableWidth = YES; self.hasLoggedDomainCreditPromptShownEvent = NO; @@ -473,7 +473,12 @@ - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection [self reloadTableViewPreservingSelection]; } -- (void)showDetailViewForSubsection:(BlogDetailsSubsection)section +- (void)showDetailViewForSubsection:(BlogDetailsSubsection)section +{ + [self showDetailViewForSubsection:section userInfo:@{}]; +} + +- (void)showDetailViewForSubsection:(BlogDetailsSubsection)section userInfo:(NSDictionary *)userInfo { NSIndexPath *indexPath = [self indexPathForSubsection:section]; @@ -524,7 +529,8 @@ - (void)showDetailViewForSubsection:(BlogDetailsSubsection)section [self.tableView selectRowAtIndexPath:indexPath animated:NO scrollPosition:[self optimumScrollPositionForIndexPath:indexPath]]; - [self showMediaLibraryFromSource:BlogDetailsNavigationSourceLink]; + BOOL showPicker = userInfo[[BlogDetailsViewController userInfoShowPickerKey]] ?: NO; + [self showMediaLibraryFromSource:BlogDetailsNavigationSourceLink showPicker: showPicker]; break; case BlogDetailsSubsectionPages: self.restorableSelectedIndexPath = indexPath; @@ -668,7 +674,6 @@ - (void)setRestorableSelectedIndexPath:(NSIndexPath *)restorableSelectedIndexPat if (restorableSelectedIndexPath != nil && restorableSelectedIndexPath.section < [self.tableSections count]) { BlogDetailsSection *section = [self.tableSections objectAtIndex:restorableSelectedIndexPath.section]; switch (section.category) { - case BlogDetailsSectionCategoryQuickAction: case BlogDetailsSectionCategoryQuickStart: case BlogDetailsSectionCategoryJetpackBrandingCard: case BlogDetailsSectionCategoryDomainCredit: { @@ -977,7 +982,6 @@ - (void)reloadTableViewPreservingSelection // For QuickStart and Use Domain cases we want to select the first row on the next available section switch (section.category) { - case BlogDetailsSectionCategoryQuickAction: case BlogDetailsSectionCategoryQuickStart: case BlogDetailsSectionCategoryJetpackBrandingCard: case BlogDetailsSectionCategoryDomainCredit: { @@ -1015,7 +1019,12 @@ - (UITableViewScrollPosition)optimumScrollPositionForIndexPath:(NSIndexPath *)in - (void)configureTableViewData { NSMutableArray *marr = [NSMutableArray array]; - + + // TODO: Add the SoTW card here. + if ([self shouldShowSotW2023Card]) { + [marr addNullableObject:[self sotw2023SectionViewModel]]; + } + if (MigrationSuccessCardView.shouldShowMigrationSuccessCard == YES) { [marr addNullableObject:[self migrationSuccessSectionViewModel]]; } @@ -1028,15 +1037,6 @@ - (void)configureTableViewData [marr addNullableObject:[self jetpackCardSectionViewModel]]; } - // This code will be removed in a future PR. -// if ([DomainCreditEligibilityChecker canRedeemDomainCreditWithBlog:self.blog]) { -// if (!self.hasLoggedDomainCreditPromptShownEvent) { -// [WPAnalytics track:WPAnalyticsStatDomainCreditPromptShown]; -// self.hasLoggedDomainCreditPromptShownEvent = YES; -// } -// [marr addNullableObject:[self domainCreditSectionViewModel]]; -// } - if ([self shouldShowQuickStartChecklist]) { [marr addNullableObject:[self quickStartSectionViewModel]]; } @@ -1551,12 +1551,6 @@ - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { BlogDetailsSection *detailSection = [self.tableSections objectAtIndex:section]; - - /// For larger texts we don't show the quick actions row - if (detailSection.category == BlogDetailsSectionCategoryQuickAction && self.isAccessibilityCategoryEnabled) { - return 0; - } - return [detailSection.rows count]; } @@ -1578,17 +1572,23 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N { BlogDetailsSection *section = [self.tableSections objectAtIndex:indexPath.section]; + if (section.category == BlogDetailsSectionCategorySotW2023Card) { + SotWTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:BlogDetailsSotWCardCellIdentifier]; + __weak __typeof(self) weakSelf = self; + [cell configureOnCardHidden:^{ + [weakSelf configureTableViewData]; + [weakSelf reloadTableViewPreservingSelection]; + }]; + + return cell; + } + if (section.category == BlogDetailsSectionCategoryJetpackInstallCard) { JetpackRemoteInstallTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:BlogDetailsJetpackInstallCardCellIdentifier]; [cell configureWithBlog:self.blog viewController:self]; return cell; } - if (section.category == BlogDetailsSectionCategoryQuickAction) { - QuickActionsCell *cell = [tableView dequeueReusableCellWithIdentifier:BlogDetailsQuickActionsCellIdentifier]; - [self configureQuickActionsWithCell: cell]; - return cell; - } if (section.category == BlogDetailsSectionCategoryQuickStart) { QuickStartCell *cell = [tableView dequeueReusableCellWithIdentifier:BlogDetailsQuickStartCellIdentifier]; @@ -1740,73 +1740,12 @@ - (void)preloadBlogData // only preload on wifi if (isOnWifi) { - [self preloadPosts]; - [self preloadPages]; [self preloadComments]; [self preloadMetadata]; [self preloadDomains]; } } -- (void)preloadPosts -{ - [self preloadPostsOfType:PostServiceTypePost]; -} - -- (void)preloadPages -{ - [self preloadPostsOfType:PostServiceTypePage]; -} - -// preloads posts or pages. -- (void)preloadPostsOfType:(PostServiceType)postType -{ - // Temporarily disable posts preloading until we can properly resolve the issues on: - // https://github.com/wordpress-mobile/WordPress-iOS/issues/6151 - // Brent C. Nov 3/2016 - BOOL preloadingPostsDisabled = YES; - if (preloadingPostsDisabled) { - return; - } - - NSDate *lastSyncDate; - if ([postType isEqual:PostServiceTypePage]) { - lastSyncDate = self.blog.lastPagesSync; - } else { - lastSyncDate = self.blog.lastPostsSync; - } - NSTimeInterval now = [[NSDate date] timeIntervalSinceReferenceDate]; - NSTimeInterval lastSync = lastSyncDate.timeIntervalSinceReferenceDate; - if (now - lastSync > PreloadingCacheTimeout) { - NSManagedObjectContext *context = [[ContextManager sharedInstance] mainContext]; - PostService *postService = [[PostService alloc] initWithManagedObjectContext:context]; - PostListFilterSettings *filterSettings = [[PostListFilterSettings alloc] initWithBlog:self.blog postType:postType]; - PostListFilter *filter = [filterSettings currentPostListFilter]; - - PostServiceSyncOptions *options = [PostServiceSyncOptions new]; - options.statuses = filter.statuses; - options.authorID = [filterSettings authorIDFilter]; - options.purgesLocalSync = YES; - - if ([postType isEqual:PostServiceTypePage]) { - self.blog.lastPagesSync = [NSDate date]; - } else { - self.blog.lastPostsSync = [NSDate date]; - } - NSError *error = nil; - [self.blog.managedObjectContext save:&error]; - - [postService syncPostsOfType:postType withOptions:options forBlog:self.blog success:nil failure:^(NSError * __unused error) { - NSDate *invalidatedDate = [NSDate dateWithTimeIntervalSince1970:0.0]; - if ([postType isEqual:PostServiceTypePage]) { - self.blog.lastPagesSync = invalidatedDate; - } else { - self.blog.lastPostsSync = invalidatedDate; - } - }]; - } -} - - (void)preloadComments { CommentService *commentService = [[CommentService alloc] initWithCoreDataStack:[ContextManager sharedInstance]]; @@ -1885,18 +1824,15 @@ - (void)showPageListFromSource:(BlogDetailsNavigationSource)source [[QuickStartTourGuide shared] visited:QuickStartTourElementPages]; } -- (void)showMediaLibraryFromSource:(BlogDetailsNavigationSource)source +- (void)showMediaLibraryFromSource:(BlogDetailsNavigationSource)source { + [self showMediaLibraryFromSource:source showPicker:false]; +} + +- (void)showMediaLibraryFromSource:(BlogDetailsNavigationSource)source showPicker:(BOOL)showPicker { [self trackEvent:WPAnalyticsStatOpenedMediaLibrary fromSource:source]; - if ([Feature enabled:FeatureFlagMediaModernization]) { - SiteMediaViewController *controller = [[SiteMediaViewController alloc] initWithBlog:self.blog]; - [self.presentationDelegate presentBlogDetailsViewController:controller]; - } else { - MediaLibraryViewController *controller = [[MediaLibraryViewController alloc] initWithBlog:self.blog]; - controller.navigationItem.largeTitleDisplayMode = UINavigationItemLargeTitleDisplayModeNever; - [self.presentationDelegate presentBlogDetailsViewController:controller]; - } - + SiteMediaViewController *controller = [[SiteMediaViewController alloc] initWithBlog:self.blog showPicker:showPicker]; + [self.presentationDelegate presentBlogDetailsViewController:controller]; [[QuickStartTourGuide shared] visited:QuickStartTourElementMediaScreen]; } @@ -2303,4 +2239,10 @@ - (void)pulledToRefreshWith:(UIRefreshControl *)refreshControl onCompletion:( vo }]; } +#pragma mark - Constants + ++ (NSString *)userInfoShowPickerKey { + return @"show-picker"; +} + @end diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Details/Detail Header/ActionRow.swift b/WordPress/Classes/ViewRelated/Blog/Blog Details/Detail Header/ActionRow.swift deleted file mode 100644 index a05400320d24..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Blog Details/Detail Header/ActionRow.swift +++ /dev/null @@ -1,119 +0,0 @@ -class ActionButton: UIView { - - private enum Constants { - static let maxButtonSize: CGFloat = 56 - static let spacing: CGFloat = 8 - static let borderColor = UIColor.quickActionButtonBorder - static let backgroundColor = UIColor.quickActionButtonBackground - static let selectedBackgroundColor = UIColor.quickActionSelectedBackground - static let iconColor = UIColor.listIcon - } - - private let button: UIButton = { - let button = RoundedButton(type: .custom) - button.isCircular = true - button.borderColor = Constants.borderColor - button.borderWidth = 1 - button.backgroundColor = Constants.backgroundColor - button.selectedBackgroundColor = Constants.selectedBackgroundColor - button.tintColor = Constants.iconColor - button.imageView?.contentMode = .center - button.imageView?.clipsToBounds = false - return button - }() - - private let titleLabel: UILabel = { - let titleLabel = UILabel() - titleLabel.font = UIFont.preferredFont(forTextStyle: .caption1) - titleLabel.textAlignment = .center - titleLabel.adjustsFontForContentSizeCategory = true - return titleLabel - }() - - private var callback: (() -> Void)? - - convenience init(image: UIImage, title: String, tapped: @escaping () -> Void) { - - self.init(frame: .zero) - - button.setImage(image, for: .normal) - titleLabel.text = title - - button.accessibilityLabel = title - accessibilityElements = [button] - - let stackView = UIStackView(arrangedSubviews: [ - button, - titleLabel - ]) - stackView.alignment = .center - stackView.translatesAutoresizingMaskIntoConstraints = false - stackView.spacing = Constants.spacing - stackView.axis = .vertical - - callback = tapped - button.addTarget(self, action: #selector(ActionButton.tapped), for: .touchUpInside) - - NSLayoutConstraint.activate([ - button.heightAnchor.constraint(equalTo: button.widthAnchor, multiplier: 1), - button.heightAnchor.constraint(lessThanOrEqualToConstant: Constants.maxButtonSize) - ]) - - addSubview(stackView) - - pinSubviewToAllEdges(stackView) - } - - @objc func tapped() { - callback?() - } -} - -class ActionRow: UIStackView { - - enum Constants { - static let minimumSpacing: CGFloat = 8 - static let margins = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16) - } - - struct Item { - let image: UIImage - let title: String - let tapped: () -> Void - } - - convenience init(items: [Item]) { - - let buttons = items.map({ item in - return ActionButton(image: item.image, title: item.title, tapped: item.tapped) - }) - - self.init(arrangedSubviews: buttons) - - distribution = .equalCentering - spacing = Constants.minimumSpacing - translatesAutoresizingMaskIntoConstraints = false - refreshStackViewVisibility() - - layoutMargins = Constants.margins - isLayoutMarginsRelativeArrangement = true - } - - // MARK: - Accessibility - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - refreshStackViewVisibility() - } - - private func refreshStackViewVisibility() { - for view in arrangedSubviews { - if traitCollection.preferredContentSizeCategory.isAccessibilityCategory { - view.isHidden = true - } else { - view.isHidden = false - } - } - } -} diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Details/Detail Header/BlogDetailHeaderView.swift b/WordPress/Classes/ViewRelated/Blog/Blog Details/Detail Header/BlogDetailHeaderView.swift index 2643097e1930..b9526c62c861 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Details/Detail Header/BlogDetailHeaderView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Details/Detail Header/BlogDetailHeaderView.swift @@ -1,8 +1,10 @@ import Gridicons import UIKit +import DesignSystem @objc protocol BlogDetailHeaderViewDelegate { func makeSiteIconMenu() -> UIMenu? + func makeSiteActionsMenu() -> UIMenu? func didShowSiteIconMenu() func siteIconReceivedDroppedImage(_ image: UIImage?) func siteIconShouldAllowDroppedImages() -> Bool @@ -15,7 +17,7 @@ class BlogDetailHeaderView: UIView { // MARK: - Child Views - private let titleView: TitleView + let titleView: TitleView // MARK: - Delegate @@ -98,13 +100,13 @@ class BlogDetailHeaderView: UIView { // MARK: - Initializers - required init(items: [ActionRow.Item], delegate: BlogDetailHeaderViewDelegate) { + required init(delegate: BlogDetailHeaderViewDelegate) { titleView = TitleView(frame: .zero) super.init(frame: .zero) self.delegate = delegate - setupChildViews(items: items) + setupChildViews() } required init?(coder: NSCoder) { @@ -113,11 +115,19 @@ class BlogDetailHeaderView: UIView { // MARK: - Child View Initialization - private func setupChildViews(items: [ActionRow.Item]) { + private func setupChildViews() { assert(delegate != nil) - if let menu = delegate?.makeSiteIconMenu() { - titleView.siteIconView.setMenu(menu) { [weak self] in + if let siteActionsMenu = delegate?.makeSiteActionsMenu() { + titleView.siteActionButton.showsMenuAsPrimaryAction = true + titleView.siteActionButton.menu = siteActionsMenu + titleView.siteActionButton.addAction(UIAction { _ in + WPAnalytics.trackEvent(.mySiteHeaderMoreTapped) + }, for: .menuActionTriggered) + } + + if let siteIconMenu = delegate?.makeSiteIconMenu() { + titleView.siteIconView.setMenu(siteIconMenu) { [weak self] in self?.delegate?.didShowSiteIconMenu() WPAnalytics.track(.siteSettingsSiteIconTapped) self?.titleView.siteIconView.spotlightIsShown = false @@ -130,23 +140,20 @@ class BlogDetailHeaderView: UIView { titleView.subtitleButton.addTarget(self, action: #selector(subtitleButtonTapped), for: .touchUpInside) titleView.titleButton.addTarget(self, action: #selector(titleButtonTapped), for: .touchUpInside) - titleView.siteSwitcherButton.addTarget(self, action: #selector(siteSwitcherTapped), for: .touchUpInside) titleView.translatesAutoresizingMaskIntoConstraints = false addSubview(titleView) - let showsActionRow = items.count > 0 - setupConstraintsForChildViews(showsActionRow) + setupConstraintsForChildViews() } // MARK: - Constraints private var topActionRowConstraint: NSLayoutConstraint? - private func setupConstraintsForChildViews(_ showsActionRow: Bool) { + private func setupConstraintsForChildViews() { let constraints = constraintsForTitleView() - NSLayoutConstraint.activate(constraints) } @@ -194,7 +201,7 @@ class BlogDetailHeaderView: UIView { } } -fileprivate extension BlogDetailHeaderView { +extension BlogDetailHeaderView { class TitleView: UIView { private enum Dimensions { static let siteSwitcherHeight: CGFloat = 36 @@ -207,7 +214,7 @@ fileprivate extension BlogDetailHeaderView { let stackView = UIStackView(arrangedSubviews: [ siteIconView, titleStackView, - siteSwitcherButton + siteActionButton ]) stackView.alignment = .center @@ -239,7 +246,7 @@ fileprivate extension BlogDetailHeaderView { button.configuration = configuration button.menu = UIMenu(children: [ - UIAction(title: Strings.openInBrowser, image: UIImage(systemName: "link"), handler: { [weak button] _ in + UIAction(title: Strings.visitSite, image: UIImage(systemName: "safari"), handler: { [weak button] _ in button?.sendActions(for: .touchUpInside) }), UIAction(title: Strings.actionCopyURL, image: UIImage(systemName: "doc.on.doc"), handler: { [weak button] _ in @@ -273,17 +280,17 @@ fileprivate extension BlogDetailHeaderView { return button }() - let siteSwitcherButton: UIButton = { + let siteActionButton: UIButton = { let button = UIButton(frame: .zero) - let image = UIImage(named: "chevron-down-slim")?.withRenderingMode(.alwaysTemplate) + let image = UIImage(named: "more-horizontal-mobile")?.withRenderingMode(.alwaysTemplate) button.setImage(image, for: .normal) button.contentMode = .center button.translatesAutoresizingMaskIntoConstraints = false button.tintColor = .secondaryLabel - button.accessibilityLabel = NSLocalizedString("Switch Site", comment: "Button used to switch site") - button.accessibilityHint = NSLocalizedString("Tap to switch to another site, or add a new site", comment: "Accessibility hint for button used to switch site") - button.accessibilityIdentifier = .switchSiteAccessibilityId + button.accessibilityLabel = NSLocalizedString("mySite.siteActions.button", value: "Site Actions", comment: "Button that reveals more site actions") + button.accessibilityHint = NSLocalizedString("mySite.siteActions.hint", value: "Tap to show more site actions", comment: "Accessibility hint for button used to show more site actions") + button.accessibilityIdentifier = .siteActionAccessibilityId return button }() @@ -337,8 +344,8 @@ fileprivate extension BlogDetailHeaderView { NSLayoutConstraint.activate([ mainStackView.topAnchor.constraint(equalTo: topAnchor, constant: Length.Padding.double), mainStackView.bottomAnchor.constraint(equalTo: bottomAnchor), - mainStackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 4), - mainStackView.trailingAnchor.constraint(equalTo: trailingAnchor) + mainStackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8), + mainStackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12) ]) setupConstraintsForSiteSwitcher() @@ -352,8 +359,8 @@ fileprivate extension BlogDetailHeaderView { private func setupConstraintsForSiteSwitcher() { NSLayoutConstraint.activate([ - siteSwitcherButton.heightAnchor.constraint(equalToConstant: Dimensions.siteSwitcherHeight), - siteSwitcherButton.widthAnchor.constraint(equalToConstant: Dimensions.siteSwitcherWidth) + siteActionButton.heightAnchor.constraint(equalToConstant: Dimensions.siteSwitcherHeight), + siteActionButton.widthAnchor.constraint(equalToConstant: Dimensions.siteSwitcherWidth) ]) } } @@ -363,11 +370,11 @@ private extension String { // MARK: Accessibility Identifiers static let siteTitleAccessibilityId = "site-title-button" static let siteUrlAccessibilityId = "site-url-button" - static let switchSiteAccessibilityId = "switch-site-button" + static let siteActionAccessibilityId = "site-action-button" } private enum Strings { - static let openInBrowser = NSLocalizedString("blogHeader.actionOpenInBrowser", value: "Open in Browser", comment: "Context menu button title") + static let visitSite = NSLocalizedString("blogHeader.actionVisitSite", value: "Visit site", comment: "Context menu button title") static let actionCopyURL = NSLocalizedString("blogHeader.actionCopyURL", value: "Copy URL", comment: "Context menu button title") } diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Details/Detail Header/SiteIconView.swift b/WordPress/Classes/ViewRelated/Blog/Blog Details/Detail Header/SiteIconView.swift index 4f4de144db2e..e3c0ef367d73 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Details/Detail Header/SiteIconView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Details/Detail Header/SiteIconView.swift @@ -33,7 +33,8 @@ class SiteIconView: UIView { }() let activityIndicator: UIActivityIndicatorView = { - let indicatorView = UIActivityIndicatorView(style: .large) + let indicatorView = UIActivityIndicatorView(style: .medium) + indicatorView.color = .white indicatorView.translatesAutoresizingMaskIntoConstraints = false return indicatorView }() diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Details/SoTW 2023/SOTWCardView.swift b/WordPress/Classes/ViewRelated/Blog/Blog Details/SoTW 2023/SOTWCardView.swift new file mode 100644 index 000000000000..2c41276aed37 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Blog/Blog Details/SoTW 2023/SOTWCardView.swift @@ -0,0 +1,176 @@ +/// A seasonal card view shown in the WordPress app to promote State of the Word 2023. +/// +class SotWCardView: UIView { + + var didHideCard: (() -> Void)? + var didTapButton: (() -> Void)? + + private let repository = UserPersistentStoreFactory.instance() + + // MARK: - Views + + private lazy var bodyLabel: UILabel = { + let label = UILabel() + label.font = UIFont.preferredFont(forTextStyle: .subheadline) + label.textColor = .secondaryLabel + label.setText(Strings.body) + label.numberOfLines = 0 + label.adjustsFontForContentSizeCategory = true + return label + }() + + private lazy var watchNowButton: UIButton = { + let button = UIButton() + button.setTitle(Strings.button, for: .normal) + button.setTitleColor(.primary, for: .normal) + button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .subheadline) + button.titleLabel?.adjustsFontForContentSizeCategory = true + button.contentHorizontalAlignment = .leading + button.addTarget(self, action: #selector(onButtonTap), for: .touchUpInside) + return button + }() + + private lazy var contentStackView: UIStackView = { + let stackView = UIStackView(arrangedSubviews: [bodyLabel, watchNowButton]) + stackView.axis = .vertical + stackView.spacing = 8.0 + stackView.isLayoutMarginsRelativeArrangement = true + stackView.directionalLayoutMargins = .init(top: 0, leading: 16, bottom: 4, trailing: 16) + return stackView + }() + + private lazy var cardFrameView: BlogDashboardCardFrameView = { + let frameView = BlogDashboardCardFrameView() + frameView.translatesAutoresizingMaskIntoConstraints = false + frameView.setTitle(Strings.title) + frameView.onEllipsisButtonTap = {} + frameView.ellipsisButton.showsMenuAsPrimaryAction = true + let hideAction = BlogDashboardHelpers.makeHideCardAction { [weak self] in + WPAnalytics.track(.sotw2023NudgePostEventCardHideTapped) + self?.repository.set(true, forKey: SotWConstants.hideCardPreferenceKey) + self?.didHideCard?() + } + frameView.ellipsisButton.menu = UIMenu(title: String(), options: .displayInline, children: [hideAction]) + frameView.add(subview: contentStackView) + + return frameView + }() + + // MARK: Initializers + + init() { + super.init(frame: .zero) + setupView() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Methods + + private func setupView() { + addSubview(cardFrameView) + pinSubviewToAllEdges(cardFrameView) + } + + @objc func onButtonTap() { + didTapButton?() + } + + // NOTE: These strings are purposely not going to be localized because we are specifically + // targeting `en` audiences only. + struct Strings { + static let title = "State of the Word 2023" + static let body = "Check out WordPress co-founder Matt Mullenweg's annual keynote to stay on top of what's coming in 2024 and beyond." + static let button = "Watch now" + } + +} + +// MARK: - Constants + +private struct SotWConstants { + static let hideCardPreferenceKey = "wp_sotw_2023_card_hidden_settings" + static let livestreamURLString = "https://wordpress.org/state-of-the-word/?utm_source=mobile&utm_medium=appnudge&utm_campaign=sotw2023" +} + +// MARK: - UITableViewCell Wrapper + +class SotWTableViewCell: UITableViewCell { + + private lazy var cardView: SotWCardView = { + let cardView = SotWCardView() + cardView.translatesAutoresizingMaskIntoConstraints = false + cardView.didTapButton = { + WPAnalytics.track(.sotw2023NudgePostEventCardCTATapped) + guard let destinationURL = URL(string: SotWConstants.livestreamURLString) else { + return + } + UIApplication.shared.open(destinationURL) + } + + return cardView + }() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setupView() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupView() { + contentView.addSubview(cardView) + contentView.pinSubviewToAllEdges(cardView, priority: .defaultHigh) + WPAnalytics.track(.sotw2023NudgePostEventCardShown) + } + + @objc func configure(onCardHidden: (() -> Void)?) { + cardView.didHideCard = onCardHidden + } +} + +// MARK: - BlogDetailsViewController Section + +extension BlogDetailsViewController { + + @objc func sotw2023SectionViewModel() -> BlogDetailsSection { + let row = BlogDetailsRow() + row.callback = {} + let section = BlogDetailsSection(title: nil, + rows: [row], + footerTitle: nil, + category: .sotW2023Card) + return section + } + + @objc func shouldShowSotW2023Card() -> Bool { + guard AppConfiguration.isWordPress && RemoteFeatureFlag.wordPressSotWCard.enabled() else { + return false + } + + // ensure that the card is not hidden. + let repository = UserPersistentStoreFactory.instance() + let cardIsHidden = repository.bool(forKey: SotWConstants.hideCardPreferenceKey) + + // ensure that the device language is in English. + let usesEnglish = WordPressComLanguageDatabase().deviceLanguageSlugString() == "en" + + // ensure that the card is not displayed before Dec. 11, 2023 where the event takes place. + let dateComponents = Date().dateAndTimeComponents() + let isPostEvent = { + guard let day = dateComponents.day, + let month = dateComponents.month, + let year = dateComponents.year else { + return false + } + return year >= 2024 || (year < 2024 && month == 12 && day > 11) + }() + + return !cardIsHidden && usesEnglish && isPostEvent + } + +} diff --git a/WordPress/Classes/ViewRelated/Blog/Blog List/BlogListConfiguration.swift b/WordPress/Classes/ViewRelated/Blog/Blog List/BlogListConfiguration.swift new file mode 100644 index 000000000000..d07bba707513 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Blog/Blog List/BlogListConfiguration.swift @@ -0,0 +1,43 @@ +import Foundation + +@objc class BlogListConfiguration: NSObject { + @objc var shouldShowCancelButton: Bool + @objc var shouldShowNavBarButtons: Bool + @objc var navigationTitle: String + /// Title shown on next page's back button + @objc var backButtonTitle: String + @objc var shouldHideSelfHostedSites: Bool + @objc var shouldHideBlogsNotSupportingDomains: Bool + @objc var analyticsSource: String? + + init( + shouldShowCancelButton: Bool, + shouldShowNavBarButtons: Bool, + navigationTitle: String, + backButtonTitle: String, + shouldHideSelfHostedSites: Bool, + shouldHideBlogsNotSupportingDomains: Bool, + analyticsSource: String? = nil + ) { + self.shouldShowCancelButton = shouldShowCancelButton + self.shouldShowNavBarButtons = shouldShowNavBarButtons + self.navigationTitle = navigationTitle + self.backButtonTitle = backButtonTitle + self.shouldHideSelfHostedSites = shouldHideSelfHostedSites + self.shouldHideBlogsNotSupportingDomains = shouldHideBlogsNotSupportingDomains + self.analyticsSource = analyticsSource + super.init() + } + + static let defaultConfig: BlogListConfiguration = .init(shouldShowCancelButton: true, + shouldShowNavBarButtons: true, + navigationTitle: Strings.defaultNavigationTitle, + backButtonTitle: Strings.defaultBackButtonTitle, + shouldHideSelfHostedSites: false, + shouldHideBlogsNotSupportingDomains: false) + + private enum Strings { + static let defaultNavigationTitle = NSLocalizedString("My Sites", comment: "Title for site picker screen.") + static let defaultBackButtonTitle = NSLocalizedString("Switch Site", comment: "Title for back button that leads to the site picker screen.") + } +} diff --git a/WordPress/Classes/ViewRelated/Blog/Blog List/BlogListDataSource.swift b/WordPress/Classes/ViewRelated/Blog/Blog List/BlogListDataSource.swift index 80baebf50ed9..c0dbbac84aa1 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog List/BlogListDataSource.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog List/BlogListDataSource.swift @@ -96,6 +96,16 @@ class BlogListDataSource: NSObject { } } + /// If this is set to `true`, blogs that do not support domains management will be hidden + /// + @objc var shouldHideBlogsNotSupportingDomains = false { + didSet { + if shouldHideBlogsNotSupportingDomains != oldValue { + dataChanged?() + } + } + } + // MARK: - Inputs // Pass to the LoggedInDataSource to match a specifc blog. @@ -300,6 +310,9 @@ private extension BlogListDataSource { if shouldHideSelfHostedSites { blogs = blogs.filter { $0.isAccessibleThroughWPCom() } } + if shouldHideBlogsNotSupportingDomains { + blogs = blogs.filter { $0.supports(.domains) } + } guard let account = account else { return blogs } diff --git a/WordPress/Classes/ViewRelated/Blog/Blog List/BlogListViewController.h b/WordPress/Classes/ViewRelated/Blog/Blog List/BlogListViewController.h index 25a66737bd21..4754efeb18fa 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog List/BlogListViewController.h +++ b/WordPress/Classes/ViewRelated/Blog/Blog List/BlogListViewController.h @@ -1,18 +1,26 @@ @import UIKit; @class Blog; +@class BlogListConfiguration; @protocol ScenePresenter; -@interface BlogListViewController : UIViewController +NS_ASSUME_NONNULL_BEGIN -@property (nonatomic, strong) Blog *selectedBlog; -@property (nonatomic, strong) id meScenePresenter; -@property (nonatomic, copy) void (^blogSelected)(BlogListViewController* blogListViewController, Blog* blog); +@interface BlogListViewController: UIViewController -- (id)initWithMeScenePresenter:(id)meScenePresenter; -- (void)setSelectedBlog:(Blog *)selectedBlog animated:(BOOL)animated; +@property (nullable, nonatomic, strong) Blog *selectedBlog; +@property (nonatomic, strong) BlogListConfiguration *configuration; +@property (nullable, nonatomic, strong) id meScenePresenter; +@property (nullable, nonatomic, copy) void (^blogSelected)(BlogListViewController* blogListViewController, Blog* blog); +- (instancetype)initWithConfiguration:(BlogListConfiguration *)configuration + meScenePresenter:(nullable id)meScenePresenter; +- (void)setSelectedBlog:(Blog *)selectedBlog animated:(BOOL)animated; - (void)presentInterfaceForAddingNewSiteFrom:(UIView *)sourceView; +- (void)showLoading; +- (void)hideLoading; @end + +NS_ASSUME_NONNULL_END diff --git a/WordPress/Classes/ViewRelated/Blog/Blog List/BlogListViewController.m b/WordPress/Classes/ViewRelated/Blog/Blog List/BlogListViewController.m index 49d33b30b54d..bd309ecba5f9 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog List/BlogListViewController.m +++ b/WordPress/Classes/ViewRelated/Blog/Blog List/BlogListViewController.m @@ -1,5 +1,6 @@ #import "BlogListViewController.h" #import "WordPress-Swift.h" +#import "SVProgressHUD+Dismiss.h" static CGFloat const BLVCHeaderViewLabelPadding = 10.0; @@ -40,12 +41,8 @@ + (UIViewController *)viewControllerWithRestorationIdentifierPath:(NSArray *)ide return nil; } -- (instancetype)init -{ - return [self initWithMeScenePresenter:[MeScenePresenter new]]; -} - -- (instancetype)initWithMeScenePresenter:(id)meScenePresenter +- (instancetype)initWithConfiguration:(BlogListConfiguration *)configuration + meScenePresenter:(id)meScenePresenter { self = [super init]; @@ -53,6 +50,7 @@ - (instancetype)initWithMeScenePresenter:(id)meScenePresenter self.restorationIdentifier = NSStringFromClass([self class]); self.restorationClass = [self class]; _meScenePresenter = meScenePresenter; + _configuration = configuration; [self configureDataSource]; [self configureNavigationBar]; @@ -66,6 +64,8 @@ - (void)configureDataSource { self.dataSource = [BlogListDataSource new]; self.dataSource.shouldShowDisclosureIndicator = NO; + self.dataSource.shouldHideSelfHostedSites = self.configuration.shouldHideSelfHostedSites; + self.dataSource.shouldHideBlogsNotSupportingDomains = self.configuration.shouldHideBlogsNotSupportingDomains; __weak __typeof(self) weakSelf = self; self.dataSource.visibilityChanged = ^(Blog *blog, BOOL visible) { @@ -80,8 +80,8 @@ - (void)configureDataSource - (void)configureNavigationBar { - // show 'Switch Site' for the next page's back button - UIBarButtonItem *backButton = [[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"Switch Site", @"") + // Configure next page's back button title + UIBarButtonItem *backButton = [[UIBarButtonItem alloc] initWithTitle:self.configuration.backButtonTitle style:UIBarButtonItemStylePlain target:nil action:nil]; @@ -92,10 +92,13 @@ - (void)configureNavigationBar action:@selector(addSite)]; self.addSiteButton.accessibilityIdentifier = @"add-site-button"; - self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel target:self action:@selector(cancelTapped)]; - self.navigationItem.leftBarButtonItem.accessibilityIdentifier = @"my-sites-cancel-button"; + if (self.configuration.shouldShowCancelButton) { + self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel target:self action:@selector(cancelTapped)]; + self.navigationItem.leftBarButtonItem.accessibilityIdentifier = @"my-sites-cancel-button"; + } + - self.navigationItem.title = NSLocalizedString(@"My Sites", @""); + self.navigationItem.title = self.configuration.navigationTitle; } - (void)cancelTapped @@ -188,6 +191,9 @@ - (void)viewWillDisappear:(BOOL)animated [self.searchBar resignFirstResponder]; } self.visible = NO; + + [SVProgressHUD resetOffsetFromCenter]; + [SVProgressHUD setDefaultMaskType:SVProgressHUDMaskTypeNone]; [WPAnalytics trackEvent:WPAnalyticsEventSiteSwitcherDismissed]; } @@ -412,6 +418,21 @@ - (void)removeBlogItemsFromSpotlight:(Blog *)blog { } } +#pragma mark - Tracks + +- (void)trackEvent:(WPAnalyticsEvent)event properties:(NSDictionary * _Nullable)properties +{ + NSMutableDictionary *mergedProperties = [NSMutableDictionary dictionary]; + + if (self.configuration.analyticsSource) { + mergedProperties[WPAppAnalyticsKeySource] = self.configuration.analyticsSource; + } + + [mergedProperties addEntriesFromDictionary:properties ?: @{}]; + + [WPAnalytics trackEvent:event properties:mergedProperties]; +} + #pragma mark - Header methods - (UIView *)headerView @@ -475,6 +496,22 @@ - (void)updateCurrentBlogSelection } } +- (void)showLoading +{ + [SVProgressHUD setDefaultMaskType:SVProgressHUDMaskTypeBlack]; + [SVProgressHUD setContainerView:self.view]; + CGPoint containerCenter = self.view.frame.origin; + [SVProgressHUD setOffsetFromCenter: UIOffsetMake(0, -containerCenter.y)]; + [SVProgressHUD show]; + self.navigationItem.hidesBackButton = true; +} + +- (void)hideLoading +{ + [SVProgressHUD dismiss]; + self.navigationItem.hidesBackButton = false; +} + #pragma mark - Configuration - (UIStatusBarStyle)preferredStatusBarStyle @@ -781,6 +818,7 @@ - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath - (void)setSelectedBlog:(Blog *)selectedBlog { [self setSelectedBlog:selectedBlog animated:[self isViewLoaded]]; + [self trackEvent:WPAnalyticsEventSiteSwitcherSiteSelected properties:nil]; } - (void)setSelectedBlog:(Blog *)selectedBlog animated:(BOOL)animated @@ -885,6 +923,10 @@ - (BOOL)shouldShowEditButton - (void)updateBarButtons { + if (self.configuration.shouldShowNavBarButtons == false) { + return; + } + BOOL showAddSiteButton = [self shouldShowAddSiteButton]; BOOL showEditButton = [self shouldShowEditButton]; diff --git a/WordPress/Classes/ViewRelated/Blog/BlogPersonalization/BlogDashboardPersonalizationViewModel.swift b/WordPress/Classes/ViewRelated/Blog/BlogPersonalization/BlogDashboardPersonalizationViewModel.swift index db88a9e5e8a3..7d073c66da4f 100644 --- a/WordPress/Classes/ViewRelated/Blog/BlogPersonalization/BlogDashboardPersonalizationViewModel.swift +++ b/WordPress/Classes/ViewRelated/Blog/BlogPersonalization/BlogDashboardPersonalizationViewModel.swift @@ -83,7 +83,11 @@ private extension DashboardCard { return NSLocalizedString("personalizeHome.dashboardCard.activityLog", value: "Recent activity", comment: "Card title for the pesonalization menu") case .pages: return NSLocalizedString("personalizeHome.dashboardCard.pages", value: "Pages", comment: "Card title for the pesonalization menu") - case .quickStart, .ghost, .failure, .personalize, .jetpackBadge, .jetpackInstall, .empty, .freeToPaidPlansDashboardCard, .domainRegistration, .jetpackSocial, .googleDomains: + case .dynamic, .quickStart, .ghost, + .failure, .personalize, .jetpackBadge, + .jetpackInstall, .empty, .freeToPaidPlansDashboardCard, + .domainRegistration, .jetpackSocial, .bloganuaryNudge, + .googleDomains: assertionFailure("\(self) card should not appear in the personalization menus") return "" // These cards don't appear in the personalization menus } diff --git a/WordPress/Classes/ViewRelated/Blog/Blogging Prompts/Bloganuary/BloganuaryOverlayViewController.swift b/WordPress/Classes/ViewRelated/Blog/Blogging Prompts/Bloganuary/BloganuaryOverlayViewController.swift new file mode 100644 index 000000000000..aa35fd7aa400 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Blog/Blogging Prompts/Bloganuary/BloganuaryOverlayViewController.swift @@ -0,0 +1,332 @@ +import SwiftUI + +class BloganuaryOverlayViewController: UIViewController { + + private let blogID: Int + + private let promptsEnabled: Bool + + private lazy var viewModel: BloganuaryOverlayViewModel = { + return BloganuaryOverlayViewModel(promptsEnabled: promptsEnabled, orientation: UIDevice.current.orientation) + }() + + // MARK: Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + setupViews() + setupNavigationBar() + + // Make sure we only track this once, regardless of redraws from orientation change, etc. + BloganuaryTracker.trackModalShown(promptsEnabled: promptsEnabled) + } + + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + + // update view model state after the device has finished the orientation change animation. + coordinator.animate(alongsideTransition: nil) { [weak self] _ in + self?.viewModel.orientation = UIDevice.current.orientation + } + } + + init(blogID: Int, promptsEnabled: Bool) { + self.blogID = blogID + self.promptsEnabled = promptsEnabled + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Private Methods + + private func setupViews() { + view.backgroundColor = .systemBackground + + let overlayView = BloganuaryOverlayView(viewModel: viewModel, onPrimaryButtonTapped: { [weak self] in + guard let self else { + return + } + + BloganuaryTracker.trackModalActionTapped(self.promptsEnabled ? .dismiss : .turnPromptsOn) + + self.dismiss(completion: { + if self.promptsEnabled { + return + } + + Task { @MainActor in + // enable prompts card on the dashboard. + BlogDashboardPersonalizationService(siteID: self.blogID).setEnabled(true, for: .prompts) + } + }) + }) + + let swiftUIView = UIView.embedSwiftUIView(overlayView) + view.addSubview(swiftUIView) + view.pinSubviewToAllEdges(swiftUIView) + } + + private func setupNavigationBar() { + let appearance = UINavigationBarAppearance() + appearance.backgroundColor = .systemBackground + appearance.shadowColor = .clear + navigationItem.scrollEdgeAppearance = appearance + navigationItem.compactScrollEdgeAppearance = appearance + + // Set up the close button in the navigation bar. + let dismissAction = UIAction { [weak self] _ in + BloganuaryTracker.trackModalDismissed() + self?.dismiss() + } + navigationItem.rightBarButtonItem = UIBarButtonItem(systemItem: .close, primaryAction: dismissAction) + } + + private func dismiss(completion: (() -> Void)? = nil) { + navigationController?.dismiss(animated: true, completion: completion) + } +} + +// MARK: - SwiftUI + +class BloganuaryOverlayViewModel: ObservableObject { + let promptsEnabled: Bool + @Published var orientation: UIDeviceOrientation + + init(promptsEnabled: Bool, orientation: UIDeviceOrientation) { + self.promptsEnabled = promptsEnabled + self.orientation = orientation + } +} + +private struct BloganuaryOverlayView: View { + + @ObservedObject var viewModel: BloganuaryOverlayViewModel + + @State var scrollViewHeight: CGFloat = 0.0 + + var onPrimaryButtonTapped: (() -> Void)? + + @ScaledMetric(relativeTo: Constants.descriptionTextStyle) + private var descriptionIconSize = 24.0 + + @ScaledMetric(relativeTo: Constants.descriptionTextStyle) + private var descriptionItemHSpacing = 16.0 + + @ScaledMetric(relativeTo: Constants.descriptionTextStyle) + private var descriptionItemVSpacing = 24.0 + + var body: some View { + VStack(spacing: .zero) { + contentScrollView + footerContainer + } + } + + var contentScrollView: some View { + ScrollView { + VStack { + content + Spacer(minLength: 32.0) + Text(stringForFooter) + .font(.footnote) + .foregroundStyle(.secondary) + .padding(.horizontal, Constants.horizontalPadding) + .multilineTextAlignment(.center) + } + .padding(.vertical, 18.0) + .frame(minHeight: scrollViewHeight, maxHeight: .infinity) + } + .layoutPriority(1) // force the scroll view to fill most of the screen space. + .background { + // try to get the scrollView height and use it as the ideal height for its content view. + GeometryReader { geo in + Color.clear + .onAppear { + scrollViewHeight = geo.size.height + } + .onChange(of: viewModel.orientation) { _ in + // since onAppear is only called once, assign the value again every time the orientation changes. + scrollViewHeight = geo.size.height + } + } + } + } + + var content: some View { + VStack(alignment: .center, spacing: 24.0) { + Image(Constants.bloganuaryImageName, bundle: nil) + .resizable() + .renderingMode(.template) + .foregroundStyle(.primary) + .scaledToFit() + .frame(width: Constants.preferredLogoWidth, height: Constants.preferredLogoHeight) + descriptionContainer + } + .padding(.horizontal, Constants.horizontalPadding) + .frame(maxWidth: .infinity, alignment: .topLeading) + } + + var descriptionContainer: some View { + VStack(alignment: .leading, spacing: 32.0) { + Text(Strings.headline) + .font(.largeTitle) + .fontWeight(.bold) + .multilineTextAlignment(.center) + .lineLimit(nil) + .fixedSize(horizontal: false, vertical: true) + .frame(maxWidth: .infinity, alignment: .center) + descriptionList + } + } + + var descriptionList: some View { + HStack(spacing: .zero) { + Spacer(minLength: .zero) + VStack(alignment: .leading, spacing: descriptionItemVSpacing) { + descriptionEntry(iconName: Constants.firstDescriptionIconName, text: Strings.firstDescriptionLine) + descriptionEntry(iconName: Constants.secondDescriptionIconName, text: Strings.secondDescriptionLine) + descriptionEntry(iconName: Constants.thirdDescriptionIconName, text: Strings.thirdDescriptionLine) + } + .padding(.horizontal, 16.0) + .frame(maxWidth: 420.0) + .layoutPriority(1) + Spacer(minLength: .zero) + } + } + + func descriptionEntry(iconName: String, text: String) -> some View { + HStack(alignment: .center, spacing: descriptionItemHSpacing) { + descriptionIconView(for: iconName) + Text(text) + .font(.headline) + .fontWeight(.semibold) + .foregroundStyle(Constants.descriptionItemTextColor) + .lineLimit(nil) + .fixedSize(horizontal: false, vertical: true) + } + } + + var footerContainer: some View { + VStack(spacing: .zero) { + Divider() + .frame(maxWidth: .infinity) + Group { + ctaButton + } + .padding(WPDeviceIdentification.isiPad() ? .vertical : .top, 24.0) + .padding(.horizontal, Constants.horizontalPadding) + } + } + + var ctaButton: some View { + Button { + onPrimaryButtonTapped?() + } label: { + Text(viewModel.promptsEnabled ? Strings.buttonTitleForEnabledPrompts : Strings.buttonTitleForDisabledPrompts) + .multilineTextAlignment(.center) + .lineLimit(nil) + .frame(maxWidth: .infinity) + .fixedSize(horizontal: false, vertical: true) + } + .padding(.vertical, 14.0) + .padding(.horizontal, 20.0) + .frame(maxWidth: .infinity, alignment: .center) + .foregroundStyle(Color(.systemBackground)) + .background(Color(.label)) + .clipShape(RoundedRectangle(cornerRadius: 12.0)) + } + + var stringForFooter: String { + guard viewModel.promptsEnabled else { + return "\(Strings.footer) \(Strings.footerAddition)" + } + return Strings.footer + } + + func descriptionIconView(for iconName: String) -> some View { + Image(iconName, bundle: nil) + .resizable() + .renderingMode(.template) + .foregroundStyle(Constants.descriptionIconColor) + .flipsForRightToLeftLayoutDirection(true) + .frame(width: descriptionIconSize, height: descriptionIconSize) + .padding(12.0) + .background { + Circle().fill(Constants.descriptionIconBackgroundColor) + } + } + + // MARK: Constants + + struct Constants { + static let horizontalPadding: CGFloat = 32.0 + static let preferredLogoWidth: CGFloat = 180.0 + static let preferredLogoHeight: CGFloat = 42.0 + static let bloganuaryImageName = "logo-bloganuary-large" + static let descriptionTextStyle: Font.TextStyle = .footnote + + static let firstDescriptionIconName = "bloganuary-icon-page" + static let secondDescriptionIconName = "bloganuary-icon-verse" + static let thirdDescriptionIconName = "bloganuary-icon-people" + + static let descriptionItemTextColor = Color(.init(light: .label, dark: .secondaryLabel)) + static let descriptionIconColor = Color(.init(light: .systemBackground, dark: .label)) + static let descriptionIconBackgroundColor = Color(.init(light: .label, dark: .tertiaryBackground)) + } + + struct Strings { + static let headline = NSLocalizedString( + "bloganuary.learnMore.modal.headline", + value: "Join our month-long writing challenge", + comment: "The headline text of the Bloganuary modal sheet." + ) + + static let firstDescriptionLine = NSLocalizedString( + "bloganuary.learnMore.modal.descriptions.first", + value: "Receive a new prompt to inspire you each day.", + comment: "The first line of the description shown in the Bloganuary modal sheet." + ) + + static let secondDescriptionLine = NSLocalizedString( + "bloganuary.learnMore.modal.description.second", + value: "Publish your response.", + comment: "The second line of the description shown in the Bloganuary modal sheet." + ) + + static let thirdDescriptionLine = NSLocalizedString( + "bloganuary.learnMore.modal.description.third", + value: "Read other bloggers’ responses to get inspiration and make new connections.", + comment: "The third line of the description shown in the Bloganuary modal sheet." + ) + + static let footer = NSLocalizedString( + "bloganuary.learnMore.modal.footer.text", + value: "Bloganuary will use Daily Blogging Prompts to send you topics for the month of January.", + comment: "An informative excerpt shown in a subtler tone." + ) + + static let footerAddition = NSLocalizedString( + "bloganuary.learnMore.modal.footer.addition", + value: "To join Bloganuary you need to enable Blogging Prompts.", + comment: "An additional piece of information shown in case the user has the Blogging Prompts feature disabled." + ) + + static let buttonTitleForDisabledPrompts = NSLocalizedString( + "bloganuary.learnMore.modal.button.promptsDisabled", + value: "Turn on blogging prompts", + comment: "Title of a button that calls the user to enable the Blogging Prompts feature." + ) + + static let buttonTitleForEnabledPrompts = NSLocalizedString( + "bloganuary.learnMore.modal.button.promptsEnabled", + value: "Let’s go!", + comment: """ + Title of a button that will dismiss the Bloganuary modal when tapped. + Note that the word 'go' here should have a closer meaning to 'start' rather than 'move forward'. + """ + ) + } +} diff --git a/WordPress/Classes/ViewRelated/Blog/Blogging Prompts/Bloganuary/BloganuaryTracker.swift b/WordPress/Classes/ViewRelated/Blog/Blogging Prompts/Bloganuary/BloganuaryTracker.swift new file mode 100644 index 000000000000..343147088b58 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Blog/Blogging Prompts/Bloganuary/BloganuaryTracker.swift @@ -0,0 +1,23 @@ +struct BloganuaryTracker { + + enum ModalAction: String { + case turnPromptsOn = "turn_prompts_on" + case dismiss + } + + static func trackCardLearnMoreTapped(promptsEnabled: Bool) { + WPAnalytics.track(.bloganuaryNudgeCardLearnMoreTapped, properties: ["prompts_enabled": promptsEnabled]) + } + + static func trackModalShown(promptsEnabled: Bool) { + WPAnalytics.track(.bloganuaryNudgeModalShown, properties: ["prompts_enabled": promptsEnabled]) + } + + static func trackModalDismissed() { + WPAnalytics.track(.bloganuaryNudgeModalDismissed) + } + + static func trackModalActionTapped(_ action: ModalAction) { + WPAnalytics.track(.bloganuaryNudgeModalActionTapped, properties: ["action": action.rawValue]) + } +} diff --git a/WordPress/Classes/ViewRelated/Blog/Blogging Prompts/BloggingPromptCoordinator.swift b/WordPress/Classes/ViewRelated/Blog/Blogging Prompts/BloggingPromptCoordinator.swift index d4b383f7ee79..0cc3e988e19f 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blogging Prompts/BloggingPromptCoordinator.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blogging Prompts/BloggingPromptCoordinator.swift @@ -20,7 +20,6 @@ import UIKit case actionSheetHeader case promptNotification case promptStaticNotification - case unknown var editorEntryPoint: PostEditorEntryPoint { switch self { @@ -32,8 +31,6 @@ import UIKit return .bloggingPromptsActionSheetHeader case .promptNotification, .promptStaticNotification: return .bloggingPromptsNotification - default: - return .unknown } } } diff --git a/WordPress/Classes/ViewRelated/Blog/Blogging Prompts/RootViewCoordinator+BloggingPrompt.swift b/WordPress/Classes/ViewRelated/Blog/Blogging Prompts/RootViewCoordinator+BloggingPrompt.swift index 4dd1995138f0..909befd04e43 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blogging Prompts/RootViewCoordinator+BloggingPrompt.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blogging Prompts/RootViewCoordinator+BloggingPrompt.swift @@ -45,9 +45,4 @@ private extension RootViewCoordinator { var accountSites: [Blog]? { try? WPAccount.lookupDefaultWordPressComAccount(in: ContextManager.shared.mainContext)?.visibleBlogs } - - struct Constants { - static let featureIntroDisplayedUDKey = "wp_intro_shown_blogging_prompts" - } - } diff --git a/WordPress/Classes/ViewRelated/Blog/Blogging Reminders/Time Selector/TimeSelectionView.swift b/WordPress/Classes/ViewRelated/Blog/Blogging Reminders/Time Selector/TimeSelectionView.swift index 8bd112f7d8f9..09d2167f73a8 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blogging Reminders/Time Selector/TimeSelectionView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blogging Reminders/Time Selector/TimeSelectionView.swift @@ -19,13 +19,6 @@ class TimeSelectionView: UIView { titleBar.setSelectedTime(timePicker.date.toLocalTime()) } - private lazy var timePickerContainerView: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(timePicker) - return view - }() - private lazy var titleBar: TimeSelectionButton = { let button = TimeSelectionButton(selectedTime: selectedTime.toLocalTime(), insets: Self.titleInsets) button.translatesAutoresizingMaskIntoConstraints = false diff --git a/WordPress/Classes/ViewRelated/Blog/My Site/MySiteViewController.swift b/WordPress/Classes/ViewRelated/Blog/My Site/MySiteViewController.swift index f6842e11cd33..a5843a7f2194 100644 --- a/WordPress/Classes/ViewRelated/Blog/My Site/MySiteViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/My Site/MySiteViewController.swift @@ -124,8 +124,8 @@ final class MySiteViewController: UIViewController, UIScrollViewDelegate, NoSite let configuration = AddNewSiteConfiguration( canCreateWPComSite: viewModel.defaultAccount != nil, canAddSelfHostedSite: AppConfiguration.showAddSelfHostedSiteButton, - launchSiteCreation: self.launchSiteCreationFromNoSites, - launchLoginForSelfHostedSite: self.launchLoginForSelfHostedSite + launchSiteCreation: { [weak self] in self?.launchSiteCreationFromNoSites() }, + launchLoginForSelfHostedSite: { [weak self] in self?.launchLoginForSelfHostedSite() } ) let noSiteView = NoSitesView( viewModel: noSitesViewModel, @@ -159,13 +159,13 @@ final class MySiteViewController: UIViewController, UIScrollViewDelegate, NoSite showBlogDetailsForMainBlogOrNoSites() } - configureNavBarAppearance(animated: false) + configureNavBarAppearance(animated: animated) } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) - resetNavBarAppearance() + resetNavBarAppearance(animated: animated) createButtonCoordinator?.hideCreateButton() } @@ -190,7 +190,7 @@ final class MySiteViewController: UIViewController, UIScrollViewDelegate, NoSite fetchPrompt(for: blog) complianceCoordinator = CompliancePopoverCoordinator() - complianceCoordinator?.presentIfNeeded(on: self) + complianceCoordinator?.presentIfNeeded() } override func viewDidLayoutSubviews() { @@ -301,8 +301,8 @@ final class MySiteViewController: UIViewController, UIScrollViewDelegate, NoSite } } - private func resetNavBarAppearance() { - navigationController?.setNavigationBarHidden(false, animated: false) + private func resetNavBarAppearance(animated: Bool) { + navigationController?.setNavigationBarHidden(false, animated: animated) isNavigationBarHidden = false } @@ -888,6 +888,10 @@ extension MySiteViewController: BlogDetailsPresentationDelegate { blogDetailsViewController?.showDetailView(for: subsection) } + func showBlogDetailsSubsection(_ subsection: BlogDetailsSubsection, userInfo: [AnyHashable: Any]) { + blogDetailsViewController?.showDetailView(for: subsection, userInfo: userInfo) + } + // TODO: Refactor presentation from routes // More context: https://github.com/wordpress-mobile/WordPress-iOS/issues/21759 func presentBlogDetailsViewController(_ viewController: UIViewController) { diff --git a/WordPress/Classes/ViewRelated/Blog/My Site/NoSitesView.swift b/WordPress/Classes/ViewRelated/Blog/My Site/NoSitesView.swift index 55f637ef1553..f09d6dd9e263 100644 --- a/WordPress/Classes/ViewRelated/Blog/My Site/NoSitesView.swift +++ b/WordPress/Classes/ViewRelated/Blog/My Site/NoSitesView.swift @@ -1,4 +1,5 @@ import SwiftUI +import DesignSystem protocol NoSitesViewDelegate: AnyObject { func didTapAccountAndSettingsButton() @@ -28,12 +29,12 @@ struct NoSitesView: View { .edgesIgnoringSafeArea(.all) mainView - .padding(.horizontal, Length.Padding.medium) + .padding(.horizontal, Length.Padding.large) if viewModel.isShowingAccountAndSettings { accountAndSettingsButton - .padding(.horizontal, Length.Padding.medium) - .padding(.bottom, Length.Padding.small) + .padding(.horizontal, Length.Padding.large) + .padding(.bottom, Length.Padding.medium) } } .frame(maxWidth: .infinity, maxHeight: .infinity) @@ -70,7 +71,7 @@ struct NoSitesView: View { handleAddNewSiteButtonTapped() } label: { Text(Strings.addNewSite) - .padding(.horizontal, Length.Padding.small) + .padding(.horizontal, Length.Padding.medium) .padding(.vertical, Length.Padding.single) .foregroundColor(.white) .background(colorScheme == .dark ? Color(uiColor: .listForeground) : .black) @@ -144,11 +145,7 @@ extension NoSitesView { func handleAddNewSiteButtonTapped() { WPAnalytics.track(.mySiteNoSitesViewActionTapped) - guard addNewSiteConfiguration.canCreateWPComSite else { - return - } - - guard addNewSiteConfiguration.canAddSelfHostedSite else { + if addNewSiteConfiguration.canCreateWPComSite && !addNewSiteConfiguration.canAddSelfHostedSite { addNewSiteConfiguration.launchSiteCreation() return } @@ -176,18 +173,3 @@ extension NoSitesView { static let addSelfHostedSite = NSLocalizedString("mySite.noSites.actionSheet.addSelfHostedSite", value: "Add self-hosted site", comment: "Action sheet button title. Launches the flow to a add self-hosted site.") } } - -struct NoSitesView_Previews: PreviewProvider { - static var previews: some View { - let configuration = AddNewSiteConfiguration( - canCreateWPComSite: true, - canAddSelfHostedSite: true, - launchSiteCreation: {}, - launchLoginForSelfHostedSite: {} - ) - NoSitesView( - viewModel: NoSitesViewModel(appUIType: .simplified, account: nil), - addNewSiteConfiguration: configuration - ) - } -} diff --git a/WordPress/Classes/ViewRelated/Blog/QuickStartChecklistViewController.swift b/WordPress/Classes/ViewRelated/Blog/QuickStartChecklistViewController.swift index dd0b957235fb..07c11a274576 100644 --- a/WordPress/Classes/ViewRelated/Blog/QuickStartChecklistViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/QuickStartChecklistViewController.swift @@ -134,17 +134,6 @@ private extension QuickStartChecklistViewController { } } -private struct TasksCompleteScreenConfiguration { - var title: String - var subtitle: String - var imageName: String -} - -private struct QuickStartChecklistConfiguration { - var title: String - var tours: [QuickStartTour] -} - private enum Constants { static let analyticsTypeKey = "type" static let closeButtonRadius: CGFloat = 30 diff --git a/WordPress/Classes/ViewRelated/Blog/QuickStartNavigationSettings.swift b/WordPress/Classes/ViewRelated/Blog/QuickStartNavigationSettings.swift index 971967a1bca1..3c7cc0051bc4 100644 --- a/WordPress/Classes/ViewRelated/Blog/QuickStartNavigationSettings.swift +++ b/WordPress/Classes/ViewRelated/Blog/QuickStartNavigationSettings.swift @@ -1,7 +1,4 @@ class QuickStartNavigationSettings: NSObject { - private weak var readerNav: UINavigationController? - private var spotlightView: QuickStartSpotlightView? - func updateWith(navigationController: UINavigationController, andViewController viewController: UIViewController) { switch viewController { @@ -13,33 +10,3 @@ class QuickStartNavigationSettings: NSObject { } } } - -private extension QuickStartNavigationSettings { - - func spotlightReaderBackButton() { - guard let readerNav = readerNav else { - return - } - - let newSpotlightView = QuickStartSpotlightView() - newSpotlightView.translatesAutoresizingMaskIntoConstraints = false - - readerNav.navigationBar.addSubview(newSpotlightView) - readerNav.navigationBar.addConstraints([ - newSpotlightView.leadingAnchor.constraint(equalTo: readerNav.navigationBar.leadingAnchor, constant: 30.0), - newSpotlightView.topAnchor.constraint(equalTo: readerNav.navigationBar.topAnchor, constant: 15.0), - ]) - - spotlightView = newSpotlightView - } - - func removeReaderSpotlight() { - guard let spotlight = spotlightView else { - return - } - - spotlight.removeFromSuperview() - spotlightView = nil - } - -} diff --git a/WordPress/Classes/ViewRelated/Blog/QuickStartSettings.swift b/WordPress/Classes/ViewRelated/Blog/QuickStartSettings.swift index caa6bbf01ec2..5afbcd039af0 100644 --- a/WordPress/Classes/ViewRelated/Blog/QuickStartSettings.swift +++ b/WordPress/Classes/ViewRelated/Blog/QuickStartSettings.swift @@ -10,14 +10,6 @@ final class QuickStartSettings { self.userDefaults = userDefaults } - // MARK: - Quick Start availability - - func isQuickStartAvailable(for blog: Blog) -> Bool { - return blog.isUserCapableOf(.ManageOptions) && - blog.isUserCapableOf(.EditThemeOptions) && - !blog.isWPForTeams() - } - // MARK: - User Defaults Storage func promptWasDismissed(for blog: Blog) -> Bool { diff --git a/WordPress/Classes/ViewRelated/Blog/QuickStartTourGuide.swift b/WordPress/Classes/ViewRelated/Blog/QuickStartTourGuide.swift index 6ffeff6add46..88dba8124653 100644 --- a/WordPress/Classes/ViewRelated/Blog/QuickStartTourGuide.swift +++ b/WordPress/Classes/ViewRelated/Blog/QuickStartTourGuide.swift @@ -507,7 +507,6 @@ private extension QuickStartTourGuide { } private struct Constants { - static let maxSkippedTours = 3 static let suggestionTimeout = 10.0 static let quickStartDelay: DispatchTimeInterval = .milliseconds(500) static let nextStepDelay: DispatchTimeInterval = .milliseconds(1000) diff --git a/WordPress/Classes/ViewRelated/Blog/QuickStartTours.swift b/WordPress/Classes/ViewRelated/Blog/QuickStartTours.swift index 306454eb9c85..a2db082e3aaa 100644 --- a/WordPress/Classes/ViewRelated/Blog/QuickStartTours.swift +++ b/WordPress/Classes/ViewRelated/Blog/QuickStartTours.swift @@ -69,27 +69,6 @@ struct QuickStartSiteMenu { static let waypoint = QuickStartTour.WayPoint(element: .siteMenu, description: descriptionBase.highlighting(phrase: descriptionTarget, icon: nil)) } -struct QuickStartChecklistTour: QuickStartTour { - let key = "quick-start-checklist-tour" - let analyticsKey = "view_list" - let title = NSLocalizedString("Continue with site setup", comment: "Title of a Quick Start Tour") - let titleMarkedCompleted = NSLocalizedString("Completed: Continue with site setup", comment: "The Quick Start Tour title after the user finished the step.") - let description = NSLocalizedString("Time to finish setting up your site! Our checklist walks you through the next steps.", comment: "Description of a Quick Start Tour") - let icon = UIImage.gridicon(.external) - let iconColor = UIColor.systemGray4 - let suggestionNoText = Strings.notNow - let suggestionYesText = Strings.yesShowMe - let possibleEntryPoints: Set = [.blogDetails, .blogDashboard] - - var waypoints: [WayPoint] = { - let descriptionBase = NSLocalizedString("Select %@ to see your checklist", comment: "A step in a guided tour for quick start. %@ will be the name of the item to select.") - let descriptionTarget = NSLocalizedString("Quick Start", comment: "The menu item to select during a guided tour.") - return [(element: .checklist, description: descriptionBase.highlighting(phrase: descriptionTarget, icon: .gridicon(.listCheckmark)))] - }() - - let accessibilityHintText = NSLocalizedString("Guides you through the process of setting up your site.", comment: "This value is used to set the accessibility hint text for setting up the user's site.") -} - struct QuickStartCreateTour: QuickStartTour { let key = "quick-start-create-tour" let analyticsKey = "create_site" @@ -134,27 +113,6 @@ struct QuickStartViewTour: QuickStartTour { } } -struct QuickStartThemeTour: QuickStartTour { - let key = "quick-start-theme-tour" - let analyticsKey = "browse_themes" - let title = NSLocalizedString("Choose a theme", comment: "Title of a Quick Start Tour") - let titleMarkedCompleted = NSLocalizedString("Completed: Choose a theme", comment: "The Quick Start Tour title after the user finished the step.") - let description = NSLocalizedString("Browse all our themes to find your perfect fit.", comment: "Description of a Quick Start Tour") - let icon = UIImage.gridicon(.themes) - let iconColor = UIColor.systemGray4 - let suggestionNoText = Strings.notNow - let suggestionYesText = Strings.yesShowMe - let possibleEntryPoints: Set = [.blogDetails] - - var waypoints: [WayPoint] = { - let descriptionBase = NSLocalizedString("Select %@ to discover new themes", comment: "A step in a guided tour for quick start. %@ will be the name of the item to select.") - let descriptionTarget = NSLocalizedString("Themes", comment: "The menu item to select during a guided tour.") - return [(element: .themes, description: descriptionBase.highlighting(phrase: descriptionTarget, icon: .gridicon(.themes)))] - }() - - let accessibilityHintText = NSLocalizedString("Guides you through the process of choosing a theme for your site.", comment: "This value is used to set the accessibility hint text for choosing a theme for the user's site.") -} - struct QuickStartShareTour: QuickStartTour { let key = "quick-start-share-tour" let analyticsKey = "share_site" @@ -342,27 +300,6 @@ struct QuickStartCheckStatsTour: QuickStartTour { let accessibilityHintText = NSLocalizedString("Guides you through the process of reviewing statistics for your site.", comment: "This value is used to set the accessibility hint text for viewing Stats on the user's site.") } -struct QuickStartExplorePlansTour: QuickStartTour { - let key = "quick-start-explore-plans-tour" - let analyticsKey = "explore_plans" - let title = NSLocalizedString("Explore plans", comment: "Title of a Quick Start Tour") - let titleMarkedCompleted = NSLocalizedString("Completed: Explore plans", comment: "The Quick Start Tour title after the user finished the step.") - let description = NSLocalizedString("Learn about the marketing and SEO tools in our paid plans.", comment: "Description of a Quick Start Tour") - let icon = UIImage.gridicon(.plans) - let iconColor = UIColor.systemGray4 - let suggestionNoText = Strings.notNow - let suggestionYesText = Strings.yesShowMe - let possibleEntryPoints: Set = [.blogDetails] - - var waypoints: [WayPoint] = { - let descriptionBase = NSLocalizedString("Select %@ to see your current plan and other available plans.", comment: "A step in a guided tour for quick start. %@ will be the name of the item to select.") - let descriptionTarget = NSLocalizedString("Plan", comment: "The item to select during a guided tour.") - return [(element: .plans, description: descriptionBase.highlighting(phrase: descriptionTarget, icon: .gridicon(.plans)))] - }() - - let accessibilityHintText = NSLocalizedString("Guides you through the process of exploring plans for your site.", comment: "This value is used to set the accessibility hint text for exploring plans on the user's site.") -} - struct QuickStartNotificationsTour: QuickStartTour { let key = "quick-start-notifications-tour" let analyticsKey = "notifications" diff --git a/WordPress/Classes/ViewRelated/Blog/SharingAuthorizationWebViewController.swift b/WordPress/Classes/ViewRelated/Blog/SharingAuthorizationWebViewController.swift index cf8d2ddd801b..4977c79a89c9 100644 --- a/WordPress/Classes/ViewRelated/Blog/SharingAuthorizationWebViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/SharingAuthorizationWebViewController.swift @@ -114,10 +114,6 @@ class SharingAuthorizationWebViewController: WPWebViewController { // Delegates should expect to handle a false positive. delegate?.authorizeDidSucceed(publicizer) } - - private func displayLoadError(error: NSError) { - delegate?.authorize(self.publicizer, didFailWithError: error) - } } // MARK: - WKNavigationDelegate diff --git a/WordPress/Classes/ViewRelated/Blog/Site Management/SiteTagsViewController.swift b/WordPress/Classes/ViewRelated/Blog/Site Management/SiteTagsViewController.swift index 7f226c051459..585f6669579d 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Management/SiteTagsViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Management/SiteTagsViewController.swift @@ -6,7 +6,6 @@ final class SiteTagsViewController: UITableViewController { private struct TableConstants { static let cellIdentifier = "TitleBadgeDisclosureCell" static let accesibilityIdentifier = "SiteTagsList" - static let numberOfSections = 1 } private let blog: Blog diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteIconPickerView.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteIconPickerView.swift index 8caa77a1aa57..f85d1d78b917 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteIconPickerView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteIconPickerView.swift @@ -9,7 +9,6 @@ struct SiteIconPickerView: View { @SwiftUI.State private var currentIcon: String? = nil @SwiftUI.State private var currentBackgroundColor: UIColor = .init(hexString: "#969CA1") ?? .gray - @SwiftUI.State private var scrollOffsetColumn: Int? = nil private var hasMadeSelection: Bool { currentIcon != nil diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController+SiteActions.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController+SiteActions.swift new file mode 100644 index 000000000000..4f6ada3551cb --- /dev/null +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController+SiteActions.swift @@ -0,0 +1,170 @@ +import UIKit +import SwiftUI +import WordPressAuthenticator + +extension SitePickerViewController { + + func makeSiteActionsMenu() -> UIMenu? { + UIMenu(options: .displayInline, children: [ + UIDeferredMenuElement.uncached { [weak self] in + $0(self?.makeSections() ?? []) + } + ]) + } + + private func makeSections() -> [UIMenu] { + [ + makePrimarySection(), + makeSecondarySection(), + makeTertiarySection() + ] + } + + private func makePrimarySection() -> UIMenu { + var menuItems = [ + MenuItem.visitSite({ [weak self] in self?.visitSiteTapped() }), + MenuItem.addSite({ [weak self] in self?.addSiteTapped()} ), + ] + if numberOfBlogs() > 1 { + menuItems.append(MenuItem.switchSite({ [weak self] in self?.siteSwitcherTapped() })) + } + return UIMenu(options: .displayInline, children: menuItems.map { $0.toAction }) + } + + private func makeSecondarySection() -> UIMenu { + UIMenu(options: .displayInline, children: [ + MenuItem.siteTitle({ [weak self] in self?.siteTitleTapped() }).toAction, + UIMenu(title: Strings.siteIcon, image: UIImage(systemName: "photo.circle"), children: [ + makeSiteIconMenu() ?? UIMenu() + ]) + ]) + } + + private func makeTertiarySection() -> UIMenu { + UIMenu(options: .displayInline, children: [ + MenuItem.personalizeHome({ [weak self] in self?.personalizeHomeTapped() }).toAction + ]) + } + + // MARK: - Add site + + private func addSiteTapped() { + let canCreateWPComSite = defaultAccount() != nil + let canAddSelfHostedSite = AppConfiguration.showAddSelfHostedSiteButton + + // Launch wp.com site creation if the user can't add self-hosted sites + if canCreateWPComSite && !canAddSelfHostedSite { + launchSiteCreation() + return + } + + showAddSiteActionSheet(from: blogDetailHeaderView.titleView.siteActionButton, + canCreateWPComSite: canCreateWPComSite, + canAddSelfHostedSite: canAddSelfHostedSite) + + WPAnalytics.trackEvent(.mySiteHeaderAddSiteTapped) + } + + private func showAddSiteActionSheet(from sourceView: UIView, canCreateWPComSite: Bool, canAddSelfHostedSite: Bool) { + let actionSheet = AddSiteAlertFactory().makeAddSiteAlert( + source: "my_site", + canCreateWPComSite: canCreateWPComSite, + createWPComSite: { [weak self] in self?.launchSiteCreation() }, + canAddSelfHostedSite: canAddSelfHostedSite, + addSelfHostedSite: { [weak self] in self?.launchLoginForSelfHostedSite() } + ) + + actionSheet.popoverPresentationController?.sourceView = sourceView + actionSheet.popoverPresentationController?.sourceRect = sourceView.bounds + actionSheet.popoverPresentationController?.permittedArrowDirections = .up + + parent?.present(actionSheet, animated: true) + } + + private func launchSiteCreation() { + guard let parent = parent as? MySiteViewController else { + return + } + parent.launchSiteCreation(source: "my_site") + } + + private func launchLoginForSelfHostedSite() { + WordPressAuthenticator.showLoginForSelfHostedSite(self) + } + + // MARK: - Personalize home + + private func personalizeHomeTapped() { + guard let siteID = blog.dotComID?.intValue else { + return DDLogError("Failed to show dashboard personalization screen: siteID is missing") + } + + let viewController = UIHostingController(rootView: NavigationView { + BlogDashboardPersonalizationView(viewModel: .init(blog: self.blog, service: .init(siteID: siteID))) + }.navigationViewStyle(.stack)) // .stack is required for iPad + if UIDevice.isPad() { + viewController.modalPresentationStyle = .formSheet + } + present(viewController, animated: true) + + WPAnalytics.trackEvent(.mySiteHeaderPersonalizeHomeTapped) + } + + // MARK: - Helpers + + private func numberOfBlogs() -> Int { + defaultAccount()?.blogs?.count ?? 0 + } + + private func defaultAccount() -> WPAccount? { + try? WPAccount.lookupDefaultWordPressComAccount(in: ContextManager.shared.mainContext) + } +} + +private enum MenuItem { + case visitSite(_ handler: () -> Void) + case addSite(_ handler: () -> Void) + case switchSite(_ handler: () -> Void) + case siteTitle(_ handler: () -> Void) + case personalizeHome(_ handler: () -> Void) + + var title: String { + switch self { + case .visitSite: return Strings.visitSite + case .addSite: return Strings.addSite + case .switchSite: return Strings.switchSite + case .siteTitle: return Strings.siteTitle + case .personalizeHome: return Strings.personalizeHome + } + } + + var icon: UIImage? { + switch self { + case .visitSite: return UIImage(systemName: "safari") + case .addSite: return UIImage(systemName: "plus") + case .switchSite: return UIImage(systemName: "arrow.triangle.swap") + case .siteTitle: return UIImage(systemName: "character") + case .personalizeHome: return UIImage(systemName: "slider.horizontal.3") + } + } + + var toAction: UIAction { + switch self { + case .visitSite(let handler), + .addSite(let handler), + .switchSite(let handler), + .siteTitle(let handler), + .personalizeHome(let handler): + return UIAction(title: title, image: icon) { _ in handler() } + } + } +} + +private enum Strings { + static let visitSite = NSLocalizedString("mySite.siteActions.visitSite", value: "Visit site", comment: "Menu title for the visit site option") + static let addSite = NSLocalizedString("mySite.siteActions.addSite", value: "Add site", comment: "Menu title for the add site option") + static let switchSite = NSLocalizedString("mySite.siteActions.switchSite", value: "Switch site", comment: "Menu title for the switch site option") + static let siteTitle = NSLocalizedString("mySite.siteActions.siteTitle", value: "Change site title", comment: "Menu title for the change site title option") + static let siteIcon = NSLocalizedString("mySite.siteActions.siteIcon", value: "Change site icon", comment: "Menu title for the change site icon option") + static let personalizeHome = NSLocalizedString("mySite.siteActions.personalizeHome", value: "Personalize home", comment: "Menu title for the personalize home option") +} diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController+SiteIcon.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController+SiteIcon.swift index 7a204ed7a9b1..f011ffc888ff 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController+SiteIcon.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController+SiteIcon.swift @@ -9,7 +9,7 @@ import PhotosUI extension SitePickerViewController { func makeSiteIconMenu() -> UIMenu? { - return UIMenu(children: [ + UIMenu(options: .displayInline, children: [ UIDeferredMenuElement.uncached { [weak self] in $0(self?.makeUpdateSiteIconActions() ?? []) } @@ -42,7 +42,7 @@ extension SitePickerViewController { var actions = [ mediaMenu.makePhotosAction(delegate: presenter), mediaMenu.makeCameraAction(delegate: presenter), - mediaMenu.makeMediaAction(blog: blog, delegate: presenter) + mediaMenu.makeSiteMediaAction(blog: blog, delegate: presenter) ] if FeatureFlag.siteIconCreator.enabled { actions.append(UIAction( diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController.swift index d53b82a8878a..8b915c5acbb9 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController.swift @@ -12,7 +12,6 @@ final class SitePickerViewController: UIViewController { } } - var siteIconPresenter: SiteIconPickerPresenter? var siteIconPickerPresenter: SiteIconPickerPresenter? var onBlogSwitched: ((Blog) -> Void)? var onBlogListDismiss: (() -> Void)? @@ -22,7 +21,7 @@ final class SitePickerViewController: UIViewController { let mediaService: MediaService private(set) lazy var blogDetailHeaderView: BlogDetailHeaderView = { - let headerView = BlogDetailHeaderView(items: [], delegate: self) + let headerView = BlogDetailHeaderView(delegate: self) headerView.translatesAutoresizingMaskIntoConstraints = false return headerView }() @@ -94,17 +93,13 @@ extension SitePickerViewController: BlogDetailHeaderViewDelegate { } func siteSwitcherTapped() { - guard let blogListController = BlogListViewController(meScenePresenter: meScenePresenter) else { - return - } + let blogListController = BlogListViewController(configuration: .defaultConfig, meScenePresenter: meScenePresenter) blogListController.blogSelected = { [weak self] controller, selectedBlog in - guard let blog = selectedBlog else { - return - } - self?.switchToBlog(blog) - controller?.dismiss(animated: true) { - self?.onBlogListDismiss?() + guard let self else { return } + self.switchToBlog(selectedBlog) + controller.dismiss(animated: true) { + self.onBlogListDismiss?() } } diff --git a/WordPress/Classes/ViewRelated/Blog/Site Settings/HomepageSettingsViewController.swift b/WordPress/Classes/ViewRelated/Blog/Site Settings/HomepageSettingsViewController.swift index eb0a1d14b6a0..56e5ed9e5ee7 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Settings/HomepageSettingsViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Settings/HomepageSettingsViewController.swift @@ -2,7 +2,7 @@ import UIKit import WordPressFlux import WordPressShared -@objc open class HomepageSettingsViewController: UITableViewController { +@objc class HomepageSettingsViewController: UITableViewController { fileprivate enum PageSelectionType { case homepage @@ -44,13 +44,15 @@ import WordPressShared /// /// - Parameter blog: The blog for which we want to configure Homepage settings /// - @objc public convenience init(blog: Blog) { - self.init(style: .insetGrouped) - + @objc init(blog: Blog) { + self.coreDataStack = ContextManager.shared self.blog = blog + self.postRepository = PostRepository(coreDataStack: self.coreDataStack) + super.init(style: .insetGrouped) + } - let context = blog.managedObjectContext ?? ContextManager.shared.mainContext - postService = PostService(managedObjectContext: context) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") } open override func viewDidLoad() { @@ -66,18 +68,7 @@ import WordPressShared ImmuTable.registerRows([CheckmarkRow.self, NavigationItemRow.self, ActivityIndicatorRow.self], tableView: tableView) reloadViewModel() - fetchAllPages() - } - - private func fetchAllPages() { - let options = PostServiceSyncOptions() - options.number = 20 - - postService.syncPosts(ofType: .page, with: options, for: blog, success: { [weak self] posts in - self?.reloadViewModel() - }, failure: { _ in - - }) + fetchAllPagesTask = postRepository.fetchAllPages(statuses: [], in: TaggedManagedObjectID(blog)) } open override func viewWillAppear(_ animated: Bool) { @@ -85,6 +76,15 @@ import WordPressShared animateDeselectionInteractively() } + open override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + + if self.navigationController == nil { + fetchAllPagesTask?.cancel() + fetchAllPagesTask = nil + } + } + // MARK: - Model fileprivate func reloadViewModel() { @@ -151,23 +151,23 @@ import WordPressShared var selectedPagesRows: [ImmuTableRow] { let homepageID = blog.homepagePageID - let homepage = homepageID.flatMap { blog.lookupPost(withID: $0, in: postService.managedObjectContext) } + let homepage = homepageID.flatMap { blog.lookupPost(withID: $0, in: coreDataStack.mainContext) } let homepageTitle = homepage?.titleForDisplay() ?? "" let postsPageID = blog.homepagePostsPageID - let postsPage = postsPageID.flatMap { blog.lookupPost(withID: $0, in: postService.managedObjectContext) } + let postsPage = postsPageID.flatMap { blog.lookupPost(withID: $0, in: coreDataStack.mainContext) } let postsPageTitle = postsPage?.titleForDisplay() ?? "" let homepageRow = pageSelectionRow(selectionType: .homepage, detail: homepageTitle, - selectedPostID: blog?.homepagePageID, - hiddenPostID: blog?.homepagePostsPageID, + selectedPostID: blog.homepagePageID, + hiddenPostID: blog.homepagePostsPageID, isInProgress: HomepageChange.isSelectedHomepage, changeForPost: { .selectedHomepage($0) }) let postsPageRow = pageSelectionRow(selectionType: .postsPage, detail: postsPageTitle, - selectedPostID: blog?.homepagePostsPageID, - hiddenPostID: blog?.homepagePageID, + selectedPostID: blog.homepagePostsPageID, + hiddenPostID: blog.homepagePageID, isInProgress: HomepageChange.isSelectedPostsPage, changeForPost: { .selectedPostsPage($0) }) return [homepageRow, postsPageRow] @@ -304,9 +304,11 @@ import WordPressShared } // MARK: - Private Properties - fileprivate var blog: Blog! + private let coreDataStack: CoreDataStackSwift + private let blog: Blog + private let postRepository: PostRepository - fileprivate var postService: PostService! + private var fetchAllPagesTask: Task<[TaggedManagedObjectID], Error>? /// Are we currently updating the homepage type? private var inProgressChange: HomepageChange? = nil diff --git a/WordPress/Classes/ViewRelated/Blog/Site Settings/SiteIconPickerPresenter.swift b/WordPress/Classes/ViewRelated/Blog/Site Settings/SiteIconPickerPresenter.swift index 1d096b62b0e5..1e4a25b4759b 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Settings/SiteIconPickerPresenter.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Settings/SiteIconPickerPresenter.swift @@ -1,6 +1,5 @@ import Foundation import SVProgressHUD -import WPMediaPicker import WordPressShared import MobileCoreServices import UniformTypeIdentifiers @@ -21,7 +20,6 @@ final class SiteIconPickerPresenter: NSObject { // MARK: - Private Properties private var dataSource: AnyObject? - private var mediaCapturePresenter: AnyObject? // MARK: - Public methods @@ -138,34 +136,25 @@ extension SiteIconPickerPresenter: ImagePickerControllerDelegate { } } -extension SiteIconPickerPresenter: MediaPickerViewControllerDelegate { - - func mediaPickerControllerDidCancel(_ picker: WPMediaPickerViewController) { - onCompletion?(nil, nil) - } - - /// Retrieves the chosen image and triggers the ImageCropViewController display. - /// - func mediaPickerController(_ picker: WPMediaPickerViewController, didFinishPicking assets: [WPMediaAsset]) { - dataSource = nil - - guard let asset = assets.first else { - return - } - guard let media = asset as? Media else { - assertionFailure("Unsupported asset: \(asset)") +extension SiteIconPickerPresenter: SiteMediaPickerViewControllerDelegate { + func siteMediaPickerViewController(_ viewController: SiteMediaPickerViewController, didFinishWithSelection selection: [Media]) { + guard let media = selection.first else { + onCompletion?(nil, nil) return } + WPAnalytics.track(.siteSettingsSiteIconGalleryPicked) showLoadingMessage() originalMedia = media - MediaThumbnailCoordinator.shared.thumbnail(for: media, with: CGSize.zero, onCompletion: { [weak self] (image, error) in - guard let image = image else { + + Task { [weak self] in + do { + let image = try await MediaImageService.shared.image(for: media, size: .original) + self?.showImageCropViewController(image, presentingViewController: viewController) + } catch { self?.showErrorLoadingImageMessage() - return } - self?.showImageCropViewController(image, presentingViewController: picker) - }) + } } } diff --git a/WordPress/Classes/ViewRelated/Blog/Site Settings/SiteVisibility+Extensions.swift b/WordPress/Classes/ViewRelated/Blog/Site Settings/SiteVisibility+Extensions.swift index 69971a5b513c..188fb8b664a4 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Settings/SiteVisibility+Extensions.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Settings/SiteVisibility+Extensions.swift @@ -35,7 +35,7 @@ extension SiteVisibility { case .private: return NSLocalizedString("siteVisibility.private.hint", value: "Your site is only visible to you and users you approve.", comment: "Hint for users when private privacy setting is set") case .hidden: - return NSLocalizedString("siteVisibility.hidden.hint", value: "Your site is visible to everyone, but asks search engines not to index your site.", comment: "Hint for users when hidden privacy setting is set") + return NSLocalizedString("siteVisibility.hidden.hint", value: "Your site is hidden from visitors behind a \"Coming Soon\" notice until it is ready for viewing.", comment: "Hint for users when hidden privacy setting is set") case .public: return NSLocalizedString("siteVisibility.public.hint", value: "Your site is visible to everyone, and it may be indexed by search engines.", comment: "Hint for users when public privacy setting is set") case .unknown: diff --git a/WordPress/Classes/ViewRelated/Cells/MediaItemHeaderView.swift b/WordPress/Classes/ViewRelated/Cells/MediaItemHeaderView.swift new file mode 100644 index 000000000000..ac46e9f1aac6 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Cells/MediaItemHeaderView.swift @@ -0,0 +1,165 @@ +import UIKit +import Gridicons +import WordPressShared + +final class MediaItemHeaderView: UIView { + private let imageView = CachedAnimatedImageView() + private let errorView = UIImageView() + private let videoIconView = PlayIconView() + private let loadingIndicator = UIActivityIndicatorView(style: .large) + private var aspectRatioConstraint: NSLayoutConstraint? + private var imageSizeConstraints: [NSLayoutConstraint] = [] + + override init(frame: CGRect) { + super.init(frame: frame) + + setupImageView() + setupVideoIconView() + setupLoadingIndicator() + setupErrorView() + setupAccessibility() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("Not implemented") + } + + private func setupImageView() { + addSubview(imageView) + imageView.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + imageView.centerXAnchor.constraint(equalTo: centerXAnchor), + imageView.centerYAnchor.constraint(equalTo: centerYAnchor), + imageView.topAnchor.constraint(greaterThanOrEqualTo: topAnchor, constant: 20), + imageView.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor, constant: -24), + imageView.leadingAnchor.constraint(greaterThanOrEqualTo: readableContentGuide.leadingAnchor, constant: 0), + imageView.trailingAnchor.constraint(lessThanOrEqualTo: readableContentGuide.trailingAnchor, constant: 0), + imageView.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor, constant: 20), + imageView.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor, constant: 20) + ]) + + imageView.layer.cornerRadius = 12 + imageView.layer.masksToBounds = true + + imageView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + imageView.setContentCompressionResistancePriority(.defaultLow, for: .vertical) + } + + private func setupVideoIconView() { + addSubview(videoIconView) + videoIconView.isHidden = true + } + + private func setupLoadingIndicator() { + addSubview(loadingIndicator) + loadingIndicator.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + loadingIndicator.centerXAnchor.constraint(equalTo: centerXAnchor), + loadingIndicator.centerYAnchor.constraint(equalTo: centerYAnchor) + ]) + } + + private func setupErrorView() { + let configuration = UIImage.SymbolConfiguration(pointSize: 42) + errorView.image = UIImage(systemName: "exclamationmark.triangle", withConfiguration: configuration) + errorView.tintColor = .secondaryLabel + errorView.isHidden = true + + addSubview(errorView) + errorView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + errorView.centerXAnchor.constraint(equalTo: centerXAnchor), + errorView.centerYAnchor.constraint(equalTo: centerYAnchor) + ]) + } + + private func setupAccessibility() { + accessibilityTraits = .button + accessibilityLabel = Strings.accessibilityLabel + accessibilityHint = Strings.accessibilityHint + } + + override func layoutSubviews() { + super.layoutSubviews() + + videoIconView.center = center // `PlayIconView` doesn't support constraints + } + + // MARK: - Media + + func configure(with media: Media) { + NSLayoutConstraint.deactivate(imageSizeConstraints) + imageSizeConstraints = [] + + switch media.mediaType { + case .image, .video: + setImageConstraints(with: media) + + loadingIndicator.startAnimating() + errorView.isHidden = true + + Task { + let image = try? await MediaImageService.shared.image(for: media, size: .large) + loadingIndicator.stopAnimating() + + if let gif = image as? AnimatedImage, let data = gif.gifData { + imageView.animate(withGIFData: data) + } else { + imageView.image = image + } + + errorView.isHidden = image != nil + } + + videoIconView.isHidden = !(media.mediaType == .video) + case .document: + setDocumentTypeIcon(.pages) + case .audio: + setDocumentTypeIcon(.audio) + default: + break + } + } + + private func setDocumentTypeIcon(_ icon: GridiconType) { + let image = UIImage.gridicon(icon, size: CGSize(width: 96, height: 96)) + setAspectRatio(image.size.height / image.size.width) + imageView.image = image + } + + private func setImageConstraints(with media: Media) { + guard let width = media.width?.floatValue, + let height = media.height?.floatValue, + width > 0 else { + return + } + + // Configure before the image is loaded to ensure the header + // size is set to its final size before the image is loaded + imageSizeConstraints = [ + imageView.widthAnchor.constraint(equalToConstant: CGFloat(width)), + imageView.heightAnchor.constraint(equalToConstant: CGFloat(height)) + ] + for constraint in imageSizeConstraints { + constraint.priority = .defaultHigh + constraint.isActive = true + } + + // Prevent the image view from losing the aspect ratio when scaled down + setAspectRatio(CGFloat(height / width)) + } + + private func setAspectRatio(_ ratio: CGFloat) { + if let aspectRatioConstraint = aspectRatioConstraint { + imageView.removeConstraint(aspectRatioConstraint) + } + aspectRatioConstraint = imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: ratio, constant: 1.0) + aspectRatioConstraint?.isActive = true + } +} + +private enum Strings { + static let accessibilityLabel = NSLocalizedString("siteMediaItem.contentViewAccessibilityLabel", value: "Preview media", comment: "Accessibility label for media item preview for user's viewing an item in their media library") + static let accessibilityHint = NSLocalizedString("siteMediaItem.contentViewAccessibilityHint", value: "Tap to view media in full screen", comment: "Accessibility hint for media item preview for user's viewing an item in their media library") +} diff --git a/WordPress/Classes/ViewRelated/Cells/MediaItemTableViewCells.swift b/WordPress/Classes/ViewRelated/Cells/MediaItemTableViewCells.swift deleted file mode 100644 index 04d3b4af2f33..000000000000 --- a/WordPress/Classes/ViewRelated/Cells/MediaItemTableViewCells.swift +++ /dev/null @@ -1,255 +0,0 @@ -import UIKit -import Gridicons -import WordPressShared - -class MediaItemImageTableViewCell: WPTableViewCell { - - @objc let customImageView = CachedAnimatedImageView() - @objc let videoIconView = PlayIconView() - - @objc lazy var imageLoader: ImageLoader = { - return ImageLoader(imageView: customImageView, gifStrategy: .largeGIFs) - }() - - @objc var isVideo: Bool { - set { - videoIconView.isHidden = !newValue - } - get { - return !videoIconView.isHidden - } - } - - // MARK: - Initializers - public required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - commonInit() - } - - public required override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - commonInit() - } - - public convenience init() { - self.init(style: .default, reuseIdentifier: nil) - } - - @objc func commonInit() { - setupImageView() - setupVideoIconView() - } - - private func setupImageView() { - contentView.addSubview(customImageView) - customImageView.translatesAutoresizingMaskIntoConstraints = false - customImageView.contentMode = .scaleAspectFit - - NSLayoutConstraint.activate([ - customImageView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), - customImageView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), - customImageView.topAnchor.constraint(equalTo: contentView.topAnchor), - customImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) - ]) - - customImageView.setContentHuggingPriority(.defaultLow, for: .horizontal) - } - - private func setupVideoIconView() { - contentView.addSubview(videoIconView) - videoIconView.isHidden = true - } - - private var aspectRatioConstraint: NSLayoutConstraint? = nil - - @objc var targetAspectRatio: CGFloat { - set { - if let aspectRatioConstraint = aspectRatioConstraint { - customImageView.removeConstraint(aspectRatioConstraint) - } - - aspectRatioConstraint = customImageView.heightAnchor.constraint(equalTo: customImageView.widthAnchor, multiplier: newValue, constant: 1.0) - aspectRatioConstraint?.priority = .defaultHigh - aspectRatioConstraint?.isActive = true - } - get { - return aspectRatioConstraint?.multiplier ?? 0 - } - } - - override func setHighlighted(_ highlighted: Bool, animated: Bool) { - super.setHighlighted(highlighted, animated: animated) - - resetBackgroundColors() - - if animated { - UIView.animate(withDuration: 0.2) { - self.videoIconView.isHighlighted = highlighted - } - } else { - videoIconView.isHighlighted = highlighted - } - } - - override func setSelected(_ selected: Bool, animated: Bool) { - super.setSelected(selected, animated: animated) - - resetBackgroundColors() - } - - private func resetBackgroundColors() { - contentView.backgroundColor = .listForeground - } - - override func layoutSubviews() { - super.layoutSubviews() - - videoIconView.center = contentView.center - } - - override func prepareForReuse() { - super.prepareForReuse() - imageLoader.prepareForReuse() - } -} - -class MediaItemDocumentTableViewCell: WPTableViewCell { - @objc let customImageView = UIImageView() - - // MARK: - Initializers - public required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - commonInit() - } - - public required override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - commonInit() - } - - public convenience init() { - self.init(style: .default, reuseIdentifier: nil) - } - - @objc func commonInit() { - setupImageView() - } - - private func setupImageView() { - customImageView.backgroundColor = .clear - - contentView.addSubview(customImageView) - customImageView.translatesAutoresizingMaskIntoConstraints = false - customImageView.contentMode = .center - - NSLayoutConstraint.activate([ - customImageView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), - customImageView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), - customImageView.topAnchor.constraint(equalTo: contentView.topAnchor), - customImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) - ]) - } - - @objc func showIconForMedia(_ media: Media) { - let dimension = CGFloat(MediaDocumentRow.customHeight! / 2) - let size = CGSize(width: dimension, height: dimension) - - if media.mediaType == .audio { - customImageView.image = .gridicon(.audio, size: size) - } else { - customImageView.image = .gridicon(.pages, size: size) - } - } -} - -struct MediaImageRow: ImmuTableRow { - static let cell = ImmuTableCell.class(MediaItemImageTableViewCell.self) - - let media: Media - let action: ImmuTableAction? - - func configureCell(_ cell: UITableViewCell) { - WPStyleGuide.configureTableViewCell(cell) - - if let cell = cell as? MediaItemImageTableViewCell { - setAspectRatioFor(cell) - loadImageFor(cell) - cell.isVideo = media.mediaType == .video - cell.accessibilityTraits = .button - cell.accessibilityLabel = NSLocalizedString("Preview media", comment: "Accessibility label for media item preview for user's viewing an item in their media library") - cell.accessibilityHint = NSLocalizedString("Tap to view media in full screen", comment: "Accessibility hint for media item preview for user's viewing an item in their media library") - } - } - - private func setAspectRatioFor(_ cell: MediaItemImageTableViewCell) { - guard let width = media.width, let height = media.height, width.floatValue > 0 else { - return - } - - let mediaAspectRatio = CGFloat(height.floatValue) / CGFloat(width.floatValue) - - // Set a maximum aspect ratio for videos - if media.mediaType == .video { - cell.targetAspectRatio = min(mediaAspectRatio, 0.75) - } else { - cell.targetAspectRatio = mediaAspectRatio - } - } - - private func addPlaceholderImageFor(_ cell: MediaItemImageTableViewCell) { - if let url = media.absoluteLocalURL, - let image = UIImage(contentsOfFile: url.path) { - cell.customImageView.image = image - } else if let url = media.absoluteThumbnailLocalURL, - let image = UIImage(contentsOfFile: url.path) { - cell.customImageView.image = image - } - } - - private var placeholderImage: UIImage? { - if let url = media.absoluteLocalURL { - return UIImage(contentsOfFile: url.path) - } else if let url = media.absoluteThumbnailLocalURL { - return UIImage(contentsOfFile: url.path) - } - return nil - } - - private func loadImageFor(_ cell: MediaItemImageTableViewCell) { - let isBlogAtomic = media.blog.isAtomic() - cell.imageLoader.loadImage(media: media, placeholder: placeholderImage, isBlogAtomic: isBlogAtomic, success: nil) { (error) in - self.show(error) - } - } - - private func show(_ error: Error?) { - let alertController = UIAlertController(title: nil, message: NSLocalizedString("There was a problem loading the media item.", - comment: "Error message displayed when the Media Library is unable to load a full sized preview of an item."), preferredStyle: .alert) - alertController.addCancelActionWithTitle(NSLocalizedString( - "mediaItemTable.errorAlert.dismissButton", - value: "Dismiss", - comment: "Verb. User action to dismiss error alert when failing to load media item." - )) - alertController.presentFromRootViewController() - } -} - -struct MediaDocumentRow: ImmuTableRow { - static let cell = ImmuTableCell.class(MediaItemDocumentTableViewCell.self) - static let customHeight: Float? = 96.0 - - let media: Media - let action: ImmuTableAction? - - func configureCell(_ cell: UITableViewCell) { - WPStyleGuide.configureTableViewCell(cell) - - if let cell = cell as? MediaItemDocumentTableViewCell { - cell.customImageView.tintColor = cell.textLabel?.textColor - cell.showIconForMedia(media) - cell.accessibilityTraits = .button - cell.accessibilityLabel = NSLocalizedString("Preview media", comment: "Accessibility label for media item preview for user's viewing an item in their media library") - cell.accessibilityHint = NSLocalizedString("Tap to view media in full screen", comment: "Accessibility hint for media item preview for user's viewing an item in their media library") - } - } -} diff --git a/WordPress/Classes/ViewRelated/Cells/MediaSizeSliderCell.swift b/WordPress/Classes/ViewRelated/Cells/MediaSizeSliderCell.swift index 85cf21673a52..f1948374375a 100644 --- a/WordPress/Classes/ViewRelated/Cells/MediaSizeSliderCell.swift +++ b/WordPress/Classes/ViewRelated/Cells/MediaSizeSliderCell.swift @@ -124,7 +124,7 @@ struct ImageSizeModel: MediaSizeModel { if value == maxValue { return NSLocalizedString("Original", comment: "Indicates an image will use its original size when uploaded.") } - let format = NSLocalizedString("%dx%dpx", comment: "Max image size in pixels (e.g. 300x300px)") + let format = NSLocalizedString("mediaSizeSlider.valueFormat", value: "%d × %d px", comment: "Max image size in pixels (e.g. 300x300px)") return String(format: format, value, value) } diff --git a/WordPress/Classes/ViewRelated/Comments/CommentContentTableViewCell.swift b/WordPress/Classes/ViewRelated/Comments/CommentContentTableViewCell.swift index 4de6ed7cc5f2..7453cf4fbb53 100644 --- a/WordPress/Classes/ViewRelated/Comments/CommentContentTableViewCell.swift +++ b/WordPress/Classes/ViewRelated/Comments/CommentContentTableViewCell.swift @@ -9,12 +9,12 @@ class CommentContentTableViewCell: UITableViewCell, NibReusable { case info } - enum RenderMethod { + enum RenderMethod: Equatable { /// Uses WebKit to render the comment body. case web /// Uses WPRichContent to render the comment body. - case richContent + case richContent(NSAttributedString) } // MARK: - Public Properties @@ -87,7 +87,6 @@ class CommentContentTableViewCell: UITableViewCell, NibReusable { // MARK: Constants - private let customBottomSpacing: CGFloat = 10 private let contentButtonsTopSpacing: CGFloat = 15 // MARK: Outlets @@ -151,10 +150,6 @@ class CommentContentTableViewCell: UITableViewCell, NibReusable { } } - private var isReactionBarVisible: Bool { - return isCommentReplyEnabled || isCommentLikesEnabled - } - var shouldHideSeparator = false { didSet { separatorView.isHidden = shouldHideSeparator @@ -243,12 +238,14 @@ class CommentContentTableViewCell: UITableViewCell, NibReusable { } @objc func ensureRichContentTextViewLayout() { - guard renderMethod == .richContent, - let richContentTextView = contentContainerView.subviews.first as? WPRichContentView else { - return - } - - richContentTextView.updateLayoutForAttachments() + switch renderMethod { + case .richContent: + if let richContentTextView = contentContainerView.subviews.first as? WPRichContentView { + richContentTextView.updateLayoutForAttachments() + } + default: + return + } } } @@ -473,9 +470,10 @@ private extension CommentContentTableViewCell { switch renderMethod { case .web: return WebCommentContentRenderer(comment: comment) - case .richContent: + case .richContent(let attributedText): let renderer = RichCommentContentRenderer(comment: comment) renderer.richContentDelegate = self.richContentDelegate + renderer.attributedText = attributedText return renderer } }() @@ -528,7 +526,4 @@ private extension String { + "%1$d is a placeholder for the number of Likes.") static let pluralLikesFormat = NSLocalizedString("%1$d Likes", comment: "Plural button title to Like a comment. " + "%1$d is a placeholder for the number of Likes.") - - // pattern that detects empty HTML elements (including HTML comments within). - static let emptyElementRegexPattern = "<[a-z]+>()+<\\/[a-z]+>" } diff --git a/WordPress/Classes/ViewRelated/Comments/CommentDetailViewController.swift b/WordPress/Classes/ViewRelated/Comments/CommentDetailViewController.swift index f9364fc5275d..199fe09c9cf1 100644 --- a/WordPress/Classes/ViewRelated/Comments/CommentDetailViewController.swift +++ b/WordPress/Classes/ViewRelated/Comments/CommentDetailViewController.swift @@ -191,14 +191,6 @@ class CommentDetailViewController: UIViewController, NoResultsViewHost { return appearance }() - /// opaque navigation bar style. - /// this is used for iOS 14 and below, since scrollEdgeAppearance only applies for large title bars, except on iOS 15 where it applies for all navbars. - private lazy var opaqueBarAppearance: UINavigationBarAppearance = { - let appearance = UINavigationBarAppearance() - appearance.configureWithOpaqueBackground() - return appearance - }() - // MARK: Nav Bar Buttons private(set) lazy var editBarButtonItem: UIBarButtonItem = { @@ -351,18 +343,6 @@ private extension CommentDetailViewController { return .init(top: 0, left: -tableView.separatorInset.left, bottom: 0, right: tableView.frame.size.width) } - /// returns the height of the navigation bar + the status bar. - var topBarHeight: CGFloat { - return (view.window?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0.0) + - (navigationController?.navigationBar.frame.height ?? 0.0) - } - - /// determines the threshold for the content offset on whether the content has scrolled. - /// for translucent navigation bars, the content view spans behind the status bar and navigation bar so we'd have to account for that. - var contentScrollThreshold: CGFloat { - (navigationController?.navigationBar.isTranslucent ?? false) ? -topBarHeight : 0 - } - func configureView() { containerStackView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(containerStackView) diff --git a/WordPress/Classes/ViewRelated/Comments/ContentRenderer/RichCommentContentRenderer.swift b/WordPress/Classes/ViewRelated/Comments/ContentRenderer/RichCommentContentRenderer.swift index cd5e31923d26..02fb46d2d800 100644 --- a/WordPress/Classes/ViewRelated/Comments/ContentRenderer/RichCommentContentRenderer.swift +++ b/WordPress/Classes/ViewRelated/Comments/ContentRenderer/RichCommentContentRenderer.swift @@ -4,6 +4,7 @@ class RichCommentContentRenderer: NSObject, CommentContentRenderer { weak var delegate: CommentContentRendererDelegate? weak var richContentDelegate: WPRichContentViewDelegate? = nil + var attributedText: NSAttributedString? private let comment: Comment @@ -13,7 +14,7 @@ class RichCommentContentRenderer: NSObject, CommentContentRenderer { func render() -> UIView { let textView = newRichContentView() - textView.attributedText = WPRichContentView.formattedAttributedStringForString(comment.content) + textView.attributedText = attributedText textView.delegate = self return textView diff --git a/WordPress/Classes/ViewRelated/Comments/FullScreenCommentReplyViewController.swift b/WordPress/Classes/ViewRelated/Comments/FullScreenCommentReplyViewController.swift index b0dc2f2c782d..1884b296112c 100644 --- a/WordPress/Classes/ViewRelated/Comments/FullScreenCommentReplyViewController.swift +++ b/WordPress/Classes/ViewRelated/Comments/FullScreenCommentReplyViewController.swift @@ -50,7 +50,7 @@ public class FullScreenCommentReplyViewController: EditCommentViewController, Su public override func viewDidLoad() { super.viewDidLoad() placeholderLabel.text = placeholder - setupNavigationItems() + setupStandaloneEditor() configureNavigationAppearance() } @@ -174,7 +174,7 @@ public class FullScreenCommentReplyViewController: EditCommentViewController, Su } /// Creates the `leftBarButtonItem` and the `rightBarButtonItem` - private func setupNavigationItems() { + private func setupStandaloneEditor() { navigationItem.leftBarButtonItem = ({ let image = UIImage.gridicon(.chevronDown).imageWithTintColor(.primary) let leftItem = UIBarButtonItem(image: image, diff --git a/WordPress/Classes/ViewRelated/Comments/WPStyleGuide+CommentDetail.swift b/WordPress/Classes/ViewRelated/Comments/WPStyleGuide+CommentDetail.swift index 9216689530ad..e8a5c8421e2f 100644 --- a/WordPress/Classes/ViewRelated/Comments/WPStyleGuide+CommentDetail.swift +++ b/WordPress/Classes/ViewRelated/Comments/WPStyleGuide+CommentDetail.swift @@ -5,7 +5,6 @@ import UIKit extension WPStyleGuide { public struct CommentDetail { static let tintColor: UIColor = .primary - static let externalIconImage: UIImage = .gridicon(.external).imageFlippedForRightToLeftLayoutDirection() static let textFont = WPStyleGuide.fontForTextStyle(.body) static let textColor = UIColor.text diff --git a/WordPress/Classes/ViewRelated/Comments/WPStyleGuide+Comments.swift b/WordPress/Classes/ViewRelated/Comments/WPStyleGuide+Comments.swift index 1d9d9bdc0261..94f6fa41625f 100644 --- a/WordPress/Classes/ViewRelated/Comments/WPStyleGuide+Comments.swift +++ b/WordPress/Classes/ViewRelated/Comments/WPStyleGuide+Comments.swift @@ -8,23 +8,6 @@ extension WPStyleGuide { public struct Comments { static let gravatarPlaceholderImage = UIImage(named: "gravatar") ?? UIImage() - static let backgroundColor = UIColor.listForeground static let pendingIndicatorColor = UIColor.muriel(color: MurielColor(name: .yellow, shade: .shade20)) - - static let detailFont = WPStyleGuide.fontForTextStyle(.subheadline, fontWeight: .regular) - static let detailTextColor = UIColor.textSubtle - - private static let titleTextColor = UIColor.text - private static let titleTextStyle = UIFont.TextStyle.headline - - static let titleBoldAttributes: [NSAttributedString.Key: Any] = [ - .font: WPStyleGuide.fontForTextStyle(titleTextStyle, fontWeight: .semibold), - .foregroundColor: titleTextColor - ] - - static let titleRegularAttributes: [NSAttributedString.Key: Any] = [ - .font: WPStyleGuide.fontForTextStyle(titleTextStyle, fontWeight: .regular), - .foregroundColor: titleTextColor - ] } } diff --git a/WordPress/Classes/ViewRelated/Domains/Domain registration/CheckoutViewController.swift b/WordPress/Classes/ViewRelated/Domains/Domain registration/CheckoutViewController.swift index c7202961949d..6b422fe95117 100644 --- a/WordPress/Classes/ViewRelated/Domains/Domain registration/CheckoutViewController.swift +++ b/WordPress/Classes/ViewRelated/Domains/Domain registration/CheckoutViewController.swift @@ -2,6 +2,10 @@ import UIKit struct CheckoutViewModel { let url: URL + + enum Strings { + static let title = NSLocalizedString("checkout.title", value: "Checkout", comment: "Title for the checkout view") + } } final class CheckoutViewController: WebKitViewController { @@ -12,13 +16,14 @@ final class CheckoutViewController: WebKitViewController { private var webViewURLChangeObservation: NSKeyValueObservation? - init(viewModel: CheckoutViewModel, purchaseCallback: PurchaseCallback?) { + init(viewModel: CheckoutViewModel, customTitle: String?, purchaseCallback: PurchaseCallback?) { self.viewModel = viewModel self.purchaseCallback = purchaseCallback let configuration = WebViewControllerConfiguration(url: viewModel.url) configuration.authenticateWithDefaultAccount() configuration.secureInteraction = true + configuration.customTitle = customTitle ?? CheckoutViewModel.Strings.title super.init(configuration: configuration) } diff --git a/WordPress/Classes/ViewRelated/Domains/Domain registration/DomainListCard.swift b/WordPress/Classes/ViewRelated/Domains/Domain registration/DomainListCard.swift deleted file mode 100644 index 4d46469daf69..000000000000 --- a/WordPress/Classes/ViewRelated/Domains/Domain registration/DomainListCard.swift +++ /dev/null @@ -1,259 +0,0 @@ -import SwiftUI - -struct DomainListCard: View { - struct DomainInfo { - let domainName: String - let domainHeadline: String - let state: State - let description: String? - let date: String? - } - - private let domainInfo: DomainInfo - - init(domainInfo: DomainInfo) { - self.domainInfo = domainInfo - } - - var body: some View { - VStack { - HStack(spacing: Length.Padding.double) { - textContainerVStack - chevronIcon - } - - if let description = domainInfo.description { - Divider() - .padding(.bottom, Length.Padding.double) - descriptionText(description) - } - } - .padding() - } - - private var textContainerVStack: some View { - VStack(alignment: .leading, spacing: Length.Padding.single) { - domainText - domainHeadline - .padding(.bottom, Length.Padding.single) - statusHStack - .padding(.bottom, Length.Padding.double) - } - } - - private var domainText: some View { - Text(domainInfo.domainName) - .font(.callout) - .foregroundColor(.primary) - } - - private var domainHeadline: some View { - Text(domainInfo.domainHeadline) - .font(.subheadline) - .foregroundColor(.secondary) - } - - private var statusHStack: some View { - HStack(spacing: Length.Padding.double) { - statusText - Spacer() - expirationText - } - } - - private var statusText: some View { - HStack(spacing: Length.Padding.single) { - Circle() - .fill(domainInfo.state.indicatorColor) - .frame( - width: Length.Padding.single, - height: Length.Padding.single - ) - Text(domainInfo.state.text) - .foregroundColor(domainInfo.state.textColor) - .font(.subheadline.weight(domainInfo.state.fontWeight)) - } - } - - private var expirationText: some View { - Text(domainInfo.date ?? "—") - .font(.subheadline) - .foregroundColor(domainInfo.state.expireTextColor) - } - - private func descriptionText(_ description: String) -> some View { - Text(description) - .font(.subheadline) - .foregroundColor(.secondary) - } - - private var chevronIcon: some View { - Image(systemName: "chevron.right") - .foregroundColor(.DS.Foreground.secondary) - } -} - -extension DomainListCard { - enum State { - case completeSetup - case failed - case error - case inProgress - case actionRequired - case expired - case expiringSoon - case renew - case verifying - case verifyEmail - case active - - fileprivate var text: String { - switch self { - case .completeSetup: - return NSLocalizedString( - "domain.status.complete.setup", - value: "Complete Setup", - comment: "Status of a domain in `Complete Setup` state" - ) - case .failed: - return NSLocalizedString( - "domain.status.failed", - value: "Failed", - comment: "Status of a domain in `Failed` state" - ) - case .error: - return NSLocalizedString( - "domain.status.error", - value: "Error", - comment: "Status of a domain in `Error` state" - ) - case .inProgress: - return NSLocalizedString( - "domain.status.in.progress", - value: "In Progress", - comment: "Status of a domain in `In Progress` state" - ) - case .actionRequired: - return NSLocalizedString( - "domain.status.action.required", - value: "Action Required", - comment: "Status of a domain in `Action Required` state" - ) - case .expired: - return NSLocalizedString( - "domain.status.expired", - value: "Expired", - comment: "Status of a domain in `Expired` state" - ) - case .expiringSoon: - return NSLocalizedString( - "domain.status.expiring.soon", - value: "Expiring Soon", - comment: "Status of a domain in `Expiring Soon` state" - ) - case .renew: - return NSLocalizedString( - "domain.status.renew", - value: "Renew", - comment: "Status of a domain in `Renew` state" - ) - case .verifying: - return NSLocalizedString( - "domain.status.verifying", - value: "Verifying", - comment: "Status of a domain in `Verifying` state" - ) - case .verifyEmail: - return NSLocalizedString( - "domain.status.verify.email", - value: "Verify Email", - comment: "Status of a domain in `Verify Email` state" - ) - case .active: - return NSLocalizedString( - "domain.status.active", - value: "Active", - comment: "Status of a domain in `Active` state" - ) - } - } - - fileprivate var fontWeight: Font.Weight { - switch self { - case .error, - .expired, - .expiringSoon: - return .bold - default: - return .regular - } - } - - fileprivate var indicatorColor: Color { - switch self { - case .active: - return Color.DS.Foreground.success - case .completeSetup, - .actionRequired, - .expired, - .expiringSoon: - return Color.DS.Foreground.warning - case .failed, - .error: - return Color.DS.Foreground.error - case .inProgress, - .renew, - .verifying, - .verifyEmail: - return Color.DS.Foreground.secondary - } - } - - fileprivate var textColor: Color { - switch self { - case .completeSetup, - .actionRequired, - .expired, - .expiringSoon: - return Color.DS.Foreground.warning - case .failed, - .error: - return Color.DS.Foreground.error - case .inProgress, - .renew, - .verifying, - .verifyEmail, - .active: - return Color.DS.Foreground.primary - } - } - - fileprivate var expireTextColor: Color { - switch self { - case .expired: - return Color.DS.Foreground.warning - default: - return Color.DS.Foreground.secondary - } - } - } -} - -struct DomainListCard_Previews: PreviewProvider { - static var previews: some View { - ZStack { - Color(.systemBackground) - DomainListCard( - domainInfo: .init( - domainName: "domain.cool.cool", - domainHeadline: "A Cool Website", - state: .actionRequired, - description: "This domain requires explicit user consent to complete the registration. Please check the email sent for further details.", - date: "Expires Aug 15 2004" - ) - ) - } - .ignoresSafeArea() - .environment(\.colorScheme, .light) - } -} diff --git a/WordPress/Classes/ViewRelated/Domains/Domain registration/PlanSelectionViewController.swift b/WordPress/Classes/ViewRelated/Domains/Domain registration/PlanSelectionViewController.swift index 663ba63f3c0e..60c05f1c7338 100644 --- a/WordPress/Classes/ViewRelated/Domains/Domain registration/PlanSelectionViewController.swift +++ b/WordPress/Classes/ViewRelated/Domains/Domain registration/PlanSelectionViewController.swift @@ -34,6 +34,10 @@ struct PlanSelectionViewModel { static let domainAndPlanPackageParameter = "domainAndPlanPackage" static let jetpackAppPlansParameter = "jetpackAppPlans" } + + enum Strings { + static let title = NSLocalizedString("planSelection.title", value: "Plans", comment: "Title for the plan selection view") + } } final class PlanSelectionViewController: WebKitViewController { @@ -44,12 +48,14 @@ final class PlanSelectionViewController: WebKitViewController { private var webViewURLChangeObservation: NSKeyValueObservation? - init(viewModel: PlanSelectionViewModel) { + init(viewModel: PlanSelectionViewModel, customTitle: String?, analyticsSource: String? = nil) { self.viewModel = viewModel let configuration = WebViewControllerConfiguration(url: viewModel.url) configuration.authenticateWithDefaultAccount() configuration.secureInteraction = true + configuration.customTitle = customTitle ?? PlanSelectionViewModel.Strings.title + configuration.analyticsSource = analyticsSource ?? "" super.init(configuration: configuration) } diff --git a/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomain.storyboard b/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomain.storyboard deleted file mode 100644 index fbc60f124944..000000000000 --- a/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomain.storyboard +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainDetails/ViewController/RegisterDomainDetailsViewController+LocalizedStrings.swift b/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainDetails/ViewController/RegisterDomainDetailsViewController+LocalizedStrings.swift index 0ff0f036b69c..a72b44a5906c 100644 --- a/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainDetails/ViewController/RegisterDomainDetailsViewController+LocalizedStrings.swift +++ b/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainDetails/ViewController/RegisterDomainDetailsViewController+LocalizedStrings.swift @@ -49,10 +49,6 @@ enum RegisterDomainDetails { static let redemptionError = NSLocalizedString("Problem purchasing your domain. Please try again.", comment: "Register Domain - error displayed when there's a problem when purchasing the domain." ) - static let changingPrimaryDomainError = NSLocalizedString("We've had problems changing the primary domain on your site — but don't worry, your domain was successfully purchased.", - comment: "Register Domain - error displayed when a domain was purchased succesfully, but there was a problem setting it to a primary domain for the site" - ) - static let statesFetchingError = NSLocalizedString( "Error occurred fetching states", diff --git a/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainDetails/ViewController/RegisterDomainDetailsViewController.swift b/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainDetails/ViewController/RegisterDomainDetailsViewController.swift index 21e029be3b26..1b47aa94e855 100644 --- a/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainDetails/ViewController/RegisterDomainDetailsViewController.swift +++ b/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainDetails/ViewController/RegisterDomainDetailsViewController.swift @@ -58,7 +58,7 @@ class RegisterDomainDetailsViewController: UITableViewController { .domainsRegistrationFormViewed, properties: WPAnalytics.domainsProperties( usingCredit: true, - origin: .menu + origin: DomainsAnalyticsWebViewOrigin.menu.rawValue ) ) } diff --git a/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainDetails/ViewModel/RegisterDomainDetailsServiceProxy.swift b/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainDetails/ViewModel/RegisterDomainDetailsServiceProxy.swift index 55fc74ae22cf..c7155b937d15 100644 --- a/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainDetails/ViewModel/RegisterDomainDetailsServiceProxy.swift +++ b/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainDetails/ViewModel/RegisterDomainDetailsServiceProxy.swift @@ -34,13 +34,13 @@ protocol RegisterDomainDetailsServiceProxyProtocol { failure: @escaping (Error) -> Void) func createTemporaryDomainShoppingCart( - siteID: Int, + siteID: Int?, domainSuggestion: DomainSuggestion, privacyProtectionEnabled: Bool, success: @escaping (CartResponseProtocol) -> Void, failure: @escaping (Error) -> Void) - func createPersistentDomainShoppingCart(siteID: Int, + func createPersistentDomainShoppingCart(siteID: Int?, domainSuggestion: DomainSuggestion, privacyProtectionEnabled: Bool, success: @escaping (CartResponseProtocol) -> Void, @@ -164,7 +164,7 @@ class RegisterDomainDetailsServiceProxy: RegisterDomainDetailsServiceProxyProtoc } func createTemporaryDomainShoppingCart( - siteID: Int, + siteID: Int?, domainSuggestion: DomainSuggestion, privacyProtectionEnabled: Bool, success: @escaping (CartResponseProtocol) -> Void, @@ -177,7 +177,7 @@ class RegisterDomainDetailsServiceProxy: RegisterDomainDetailsServiceProxyProtoc failure: failure) } - func createPersistentDomainShoppingCart(siteID: Int, + func createPersistentDomainShoppingCart(siteID: Int?, domainSuggestion: DomainSuggestion, privacyProtectionEnabled: Bool, success: @escaping (CartResponseProtocol) -> Void, diff --git a/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainDetails/ViewModel/RegisterDomainDetailsViewModel+RowDefinitions.swift b/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainDetails/ViewModel/RegisterDomainDetailsViewModel+RowDefinitions.swift index a4c6db1f1756..e1a9f7718943 100644 --- a/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainDetails/ViewModel/RegisterDomainDetailsViewModel+RowDefinitions.swift +++ b/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainDetails/ViewModel/RegisterDomainDetailsViewModel+RowDefinitions.swift @@ -133,12 +133,6 @@ extension RegisterDomainDetailsViewModel { validationRules.forEach { $0.validate(text: value) } } - func validate(forContext context: ValidationRule.Context) { - validationRules - .filter { $0.context == context } - .forEach { $0.validate(text: value) } - } - func firstRule(forContext context: ValidationRule.Context) -> ValidationRule? { return validationRules.first { $0.context == context } } diff --git a/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainDetails/ViewModel/RegisterDomainDetailsViewModel.swift b/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainDetails/ViewModel/RegisterDomainDetailsViewModel.swift index 58d4f8ac4b50..22395ad43482 100644 --- a/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainDetails/ViewModel/RegisterDomainDetailsViewModel.swift +++ b/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainDetails/ViewModel/RegisterDomainDetailsViewModel.swift @@ -47,7 +47,7 @@ class RegisterDomainDetailsViewModel { var registerDomainDetailsService: RegisterDomainDetailsServiceProxyProtocol = RegisterDomainDetailsServiceProxy() - let domain: FullyQuotedDomainSuggestion + let domain: DomainSuggestion let siteID: Int let domainPurchasedCallback: ((String) -> Void) @@ -68,7 +68,7 @@ class RegisterDomainDetailsViewModel { } } - init(siteID: Int, domain: FullyQuotedDomainSuggestion, domainPurchasedCallback: @escaping ((String) -> Void)) { + init(siteID: Int, domain: DomainSuggestion, domainPurchasedCallback: @escaping ((String) -> Void)) { self.siteID = siteID self.domain = domain self.domainPurchasedCallback = domainPurchasedCallback @@ -193,7 +193,7 @@ class RegisterDomainDetailsViewModel { registerDomainService.purchaseDomainUsingCredits( siteID: siteID, - domainSuggestion: domainSuggestion.remoteSuggestion(), + domainSuggestion: domainSuggestion, domainContactInformation: contactInformation, privacyProtectionEnabled: privacyEnabled, success: { domain in diff --git a/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainSuggestions/DomainSuggestionViewControllerWrapper.swift b/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainSuggestions/DomainSuggestionViewControllerWrapper.swift index 433a64a4a9d1..4750cb35847b 100644 --- a/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainSuggestions/DomainSuggestionViewControllerWrapper.swift +++ b/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainSuggestions/DomainSuggestionViewControllerWrapper.swift @@ -2,14 +2,14 @@ import SwiftUI import UIKit import WordPressKit -/// Makes RegisterDomainSuggestionsViewController available to SwiftUI +/// Makes DomainSelectionViewController available to SwiftUI struct DomainSuggestionViewControllerWrapper: UIViewControllerRepresentable { private let blog: Blog private let domainSelectionType: DomainSelectionType private let onDismiss: () -> Void - private var domainSuggestionViewController: RegisterDomainSuggestionsViewController + private var domainSuggestionViewController: DomainSelectionViewController init(blog: Blog, domainSelectionType: DomainSelectionType, onDismiss: @escaping () -> Void) { self.blog = blog diff --git a/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainSuggestions/DomainSuggestionsTableViewController.swift b/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainSuggestions/DomainSuggestionsTableViewController.swift deleted file mode 100644 index 4d162ff529be..000000000000 --- a/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainSuggestions/DomainSuggestionsTableViewController.swift +++ /dev/null @@ -1,540 +0,0 @@ -import UIKit -import SVProgressHUD -import WordPressAuthenticator - - -protocol DomainSuggestionsTableViewControllerDelegate: AnyObject { - func domainSelected(_ domain: FullyQuotedDomainSuggestion) - func newSearchStarted() -} - -/// This class provides domain suggestions based on keyword searches -/// performed by the user. -/// -class DomainSuggestionsTableViewController: UITableViewController { - - // MARK: - Fonts - - private let domainBaseFont = WPStyleGuide.fontForTextStyle(.body, fontWeight: .regular) - private let domainTLDFont = WPStyleGuide.fontForTextStyle(.body, fontWeight: .semibold) - private let saleCostFont = WPStyleGuide.fontForTextStyle(.subheadline, fontWeight: .regular) - private let suggestionCostFont = WPStyleGuide.fontForTextStyle(.subheadline, fontWeight: .regular) - private let perYearPostfixFont = WPStyleGuide.fontForTextStyle(.footnote, fontWeight: .regular) - private let freeForFirstYearFont = WPStyleGuide.fontForTextStyle(.subheadline, fontWeight: .regular) - - // MARK: - Cell Identifiers - - private static let suggestionCellIdentifier = "org.wordpress.domainsuggestionstable.suggestioncell" - - // MARK: - Properties - - var blog: Blog? - var siteName: String? - weak var delegate: DomainSuggestionsTableViewControllerDelegate? - var domainSuggestionType: DomainsServiceRemote.DomainSuggestionType = .noWordpressDotCom - var domainSelectionType: DomainSelectionType? - var freeSiteAddress: String = "" - - var useFadedColorForParentDomains: Bool { - return false - } - - var searchFieldPlaceholder: String { - return NSLocalizedString( - "Type to get more suggestions", - comment: "Register domain - Search field placeholder for the Suggested Domain screen" - ) - } - - private var noResultsViewController: NoResultsViewController? - private var searchSuggestions: [FullyQuotedDomainSuggestion] = [] { - didSet { - tableView.reloadSections(IndexSet(integer: Sections.suggestions.rawValue), with: .automatic) - } - } - private var isSearching: Bool = false - private var selectedCell: UITableViewCell? - - // API returned no domain suggestions. - private var noSuggestions: Bool = false - - fileprivate enum ViewPadding: CGFloat { - case noResultsView = 60 - } - - private var parentDomainColor: UIColor { - return useFadedColorForParentDomains ? .neutral(.shade30) : .neutral(.shade70) - } - - private let searchDebouncer = Debouncer(delay: 0.5) - - // MARK: - Init - - required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - override func awakeFromNib() { - super.awakeFromNib() - - let bundle = WordPressAuthenticator.bundle - tableView.register(UINib(nibName: "SearchTableViewCell", bundle: bundle), forCellReuseIdentifier: SearchTableViewCell.reuseIdentifier) - tableView.accessibilityIdentifier = "DomainSuggestionsTable" - setupBackgroundTapGestureRecognizer() - } - - // MARK: - View - - override func viewDidLoad() { - super.viewDidLoad() - - WPStyleGuide.configureColors(view: view, tableView: tableView) - tableView.layoutMargins = WPStyleGuide.edgeInsetForLoginTextFields() - - navigationItem.title = NSLocalizedString("Create New Site", comment: "Title for the site creation flow.") - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - // only procede with initial search if we don't have site title suggestions yet - // (hopefully only the first time) - guard searchSuggestions.count < 1, - let nameToSearch = siteName else { - return - } - - suggestDomains(for: nameToSearch) - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - SVProgressHUD.dismiss() - } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - if traitCollection.userInterfaceStyle != previousTraitCollection?.userInterfaceStyle { - tableView.reloadData() - } - } - - /// Fetches new domain suggestions based on the provided string - /// - /// - Parameters: - /// - searchTerm: string to base suggestions on - /// - addSuggestions: function to call when results arrive - private func suggestDomains(for searchTerm: String) { - guard !isSearching else { - return - } - - isSearching = true - - let account = try? WPAccount.lookupDefaultWordPressComAccount(in: ContextManager.shared.mainContext) - let api = account?.wordPressComRestApi ?? WordPressComRestApi.defaultApi(oAuthToken: "") - - let service = DomainsService(coreDataStack: ContextManager.sharedInstance(), remote: DomainsServiceRemote(wordPressComRestApi: api)) - - SVProgressHUD.setContainerView(tableView) - SVProgressHUD.show(withStatus: NSLocalizedString("Loading domains", comment: "Shown while the app waits for the domain suggestions web service to return during the site creation process.")) - - service.getFullyQuotedDomainSuggestions(query: searchTerm, - domainSuggestionType: domainSuggestionType, - success: handleGetDomainSuggestionsSuccess, - failure: handleGetDomainSuggestionsFailure) - } - - private func handleGetDomainSuggestionsSuccess(_ suggestions: [FullyQuotedDomainSuggestion]) { - isSearching = false - noSuggestions = false - SVProgressHUD.dismiss() - tableView.separatorStyle = .singleLine - - searchSuggestions = suggestions - } - - private func handleGetDomainSuggestionsFailure(_ error: Error) { - DDLogError("Error getting Domain Suggestions: \(error.localizedDescription)") - isSearching = false - noSuggestions = true - SVProgressHUD.dismiss() - tableView.separatorStyle = .none - - // Dismiss the keyboard so the full no results view can be seen. - view.endEditing(true) - - // Add no suggestions to display the no results view. - searchSuggestions = [] - } - - // MARK: background gesture recognizer - - /// Sets up a gesture recognizer to detect taps on the view, but not its content. - /// - func setupBackgroundTapGestureRecognizer() { - let gestureRecognizer = UITapGestureRecognizer() - gestureRecognizer.on { [weak self](gesture) in - self?.view.endEditing(true) - } - gestureRecognizer.cancelsTouchesInView = false - view.addGestureRecognizer(gestureRecognizer) - } -} - -// MARK: - UITableViewDataSource - -extension DomainSuggestionsTableViewController { - fileprivate enum Sections: Int, CaseIterable { - case topBanner - case searchField - case suggestions - - static var count: Int { - return allCases.count - } - } - - override func numberOfSections(in tableView: UITableView) -> Int { - return Sections.count - } - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - switch section { - case Sections.topBanner.rawValue: - return shouldShowTopBanner ? 1 : 0 - case Sections.searchField.rawValue: - return 1 - case Sections.suggestions.rawValue: - if noSuggestions == true { - return 1 - } - return searchSuggestions.count - default: - return 0 - } - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell: UITableViewCell - switch indexPath.section { - case Sections.topBanner.rawValue: - cell = topBannerCell() - case Sections.searchField.rawValue: - cell = searchFieldCell() - case Sections.suggestions.rawValue: - fallthrough - default: - if noSuggestions == true { - cell = noResultsCell() - } else { - let suggestion: FullyQuotedDomainSuggestion - suggestion = searchSuggestions[indexPath.row] - cell = suggestionCell(suggestion) - } - } - return cell - } - - override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - - if indexPath.section == Sections.suggestions.rawValue && noSuggestions == true { - // Calculate the height of the no results cell from the bottom of - // the search field to the screen bottom, minus some padding. - let searchFieldRect = tableView.rect(forSection: Sections.searchField.rawValue) - let searchFieldBottom = searchFieldRect.origin.y + searchFieldRect.height - let screenBottom = UIScreen.main.bounds.height - return screenBottom - searchFieldBottom - ViewPadding.noResultsView.rawValue - } - - return super.tableView(tableView, heightForRowAt: indexPath) - } - - override func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { - if section == Sections.suggestions.rawValue { - let footer = UIView() - footer.backgroundColor = .neutral(.shade10) - return footer - } - return nil - } - - override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { - if section == Sections.suggestions.rawValue { - return 0.5 - } - return 0 - } - - override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - if section == Sections.searchField.rawValue { - let header = UIView() - header.backgroundColor = tableView.backgroundColor - return header - } - return nil - } - - override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - if section == Sections.searchField.rawValue { - return 10 - } - return 0 - } - - // MARK: table view cells - - private func topBannerCell() -> UITableViewCell { - let cell = UITableViewCell() - guard let textLabel = cell.textLabel else { - return cell - } - - textLabel.font = UIFont.preferredFont(forTextStyle: .body) - textLabel.numberOfLines = 3 - textLabel.lineBreakMode = .byTruncatingTail - textLabel.adjustsFontForContentSizeCategory = true - textLabel.adjustsFontSizeToFitWidth = true - textLabel.minimumScaleFactor = 0.5 - - let template = NSLocalizedString("Domains purchased on this site will redirect to %@", comment: "Description for the first domain purchased with a free plan.") - let formatted = String(format: template, freeSiteAddress) - let attributed = NSMutableAttributedString(string: formatted, attributes: [:]) - - if let range = formatted.range(of: freeSiteAddress) { - attributed.addAttributes([.font: textLabel.font.bold()], range: NSRange(range, in: formatted)) - } - - textLabel.attributedText = attributed - - return cell - } - - private var shouldShowTopBanner: Bool { - if domainSelectionType == .purchaseSeparately { - return true - } - - return false - } - - private func searchFieldCell() -> SearchTableViewCell { - guard let cell = tableView.dequeueReusableCell(withIdentifier: SearchTableViewCell.reuseIdentifier) as? SearchTableViewCell else { - fatalError() - } - - cell.allowSpaces = false - cell.liveSearch = true - cell.placeholder = searchFieldPlaceholder - cell.reloadTextfieldStyle() - cell.delegate = self - cell.selectionStyle = .none - cell.backgroundColor = .clear - return cell - } - - private func noResultsCell() -> UITableViewCell { - let cell = UITableViewCell() - addNoResultsTo(cell: cell) - cell.isUserInteractionEnabled = false - return cell - } - - // MARK: - Suggestion Cell - - private func suggestionCell(_ suggestion: FullyQuotedDomainSuggestion) -> UITableViewCell { - let cell = UITableViewCell(style: .subtitle, reuseIdentifier: Self.suggestionCellIdentifier) - - cell.textLabel?.attributedText = attributedDomain(suggestion.domainName) - cell.textLabel?.textColor = parentDomainColor - cell.indentationWidth = 20.0 - cell.indentationLevel = 1 - - cell.detailTextLabel?.attributedText = attributedCostInformation(for: suggestion) - - return cell - } - - private func attributedDomain(_ domain: String) -> NSAttributedString { - let attributedDomain = NSMutableAttributedString(string: domain, attributes: [.font: domainBaseFont]) - - guard let dotPosition = domain.firstIndex(of: ".") else { - return attributedDomain - } - - let tldRange = dotPosition ..< domain.endIndex - let nsRange = NSRange(tldRange, in: domain) - - attributedDomain.addAttribute(.font, - value: domainTLDFont, - range: nsRange) - - return attributedDomain - } - - private func attributedCostInformation(for suggestion: FullyQuotedDomainSuggestion) -> NSAttributedString { - let attributedString = NSMutableAttributedString() - - let hasDomainCredit = blog?.hasDomainCredit ?? false - let freeForFirstYear = hasDomainCredit || domainSelectionType == .purchaseWithPaidPlan - - if freeForFirstYear { - attributedString.append(attributedFreeForTheFirstYear()) - } else if let saleCost = attributedSaleCost(for: suggestion) { - attributedString.append(saleCost) - } - - attributedString.append(attributedSuggestionCost(for: suggestion, freeForFirstYear: freeForFirstYear)) - attributedString.append(attributedPerYearPostfix(for: suggestion, freeForFirstYear: freeForFirstYear)) - - return attributedString - } - - // MARK: - Attributed partial strings - - private func attributedFreeForTheFirstYear() -> NSAttributedString { - NSAttributedString( - string: NSLocalizedString("Free for the first year ", comment: "Label shown for domains that will be free for the first year due to the user having a premium plan with available domain credit."), - attributes: [.font: freeForFirstYearFont, .foregroundColor: UIColor.muriel(name: .green, .shade50)]) - } - - private func attributedSaleCost(for suggestion: FullyQuotedDomainSuggestion) -> NSAttributedString? { - guard let saleCostString = suggestion.saleCostString else { - return nil - } - - return NSAttributedString( - string: saleCostString + " ", - attributes: suggestionSaleCostAttributes()) - } - - private func attributedSuggestionCost(for suggestion: FullyQuotedDomainSuggestion, freeForFirstYear: Bool) -> NSAttributedString { - NSAttributedString( - string: suggestion.costString, - attributes: suggestionCostAttributes(striked: mustStrikeRegularPrice(suggestion, freeForFirstYear: freeForFirstYear))) - } - - private func attributedPerYearPostfix(for suggestion: FullyQuotedDomainSuggestion, freeForFirstYear: Bool) -> NSAttributedString { - NSAttributedString( - string: NSLocalizedString(" / year", comment: "Per-year postfix shown after a domain's cost."), - attributes: perYearPostfixAttributes(striked: mustStrikeRegularPrice(suggestion, freeForFirstYear: freeForFirstYear))) - } - - // MARK: - Attributed partial string attributes - - private func mustStrikeRegularPrice(_ suggestion: FullyQuotedDomainSuggestion, freeForFirstYear: Bool) -> Bool { - suggestion.saleCostString != nil || freeForFirstYear - } - - private func suggestionSaleCostAttributes() -> [NSAttributedString.Key: Any] { - [.font: suggestionCostFont, - .foregroundColor: UIColor.muriel(name: .orange, .shade50)] - } - - private func suggestionCostAttributes(striked: Bool) -> [NSAttributedString.Key: Any] { - [.font: suggestionCostFont, - .foregroundColor: striked ? UIColor.secondaryLabel : UIColor.label, - .strikethroughStyle: striked ? 1 : 0] - } - - private func perYearPostfixAttributes(striked: Bool) -> [NSAttributedString.Key: Any] { - [.font: perYearPostfixFont, - .foregroundColor: UIColor.secondaryLabel, - .strikethroughStyle: striked ? 1 : 0] - } -} - -// MARK: - NoResultsViewController Extension - -private extension DomainSuggestionsTableViewController { - - func addNoResultsTo(cell: UITableViewCell) { - if noResultsViewController == nil { - instantiateNoResultsViewController() - } - - guard let noResultsViewController = noResultsViewController else { - return - } - - noResultsViewController.view.frame = cell.frame - cell.contentView.addSubview(noResultsViewController.view) - - addChild(noResultsViewController) - noResultsViewController.didMove(toParent: self) - } - - func removeNoResultsFromView() { - noSuggestions = false - tableView.reloadSections(IndexSet(integer: Sections.suggestions.rawValue), with: .automatic) - noResultsViewController?.removeFromView() - } - - func instantiateNoResultsViewController() { - let title = NSLocalizedString("We couldn't find any available address with the words you entered - let's try again.", comment: "Primary message shown when there are no domains that match the user entered text.") - let subtitle = NSLocalizedString("Enter different words above and we'll look for an address that matches it.", comment: "Secondary message shown when there are no domains that match the user entered text.") - - noResultsViewController = NoResultsViewController.controllerWith(title: title, buttonTitle: nil, subtitle: subtitle) - } - -} - -// MARK: - UITableViewDelegate - -extension DomainSuggestionsTableViewController { - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let selectedDomain: FullyQuotedDomainSuggestion - - switch indexPath.section { - case Sections.suggestions.rawValue: - selectedDomain = searchSuggestions[indexPath.row] - default: - return - } - - delegate?.domainSelected(selectedDomain) - - tableView.deselectSelectedRowWithAnimation(true) - - // Uncheck the previously selected cell. - if let selectedCell = selectedCell { - selectedCell.accessoryType = .none - } - - // Check the currently selected cell. - if let cell = self.tableView.cellForRow(at: indexPath) { - cell.accessoryType = .checkmark - selectedCell = cell - } - } -} - -// MARK: - SearchTableViewCellDelegate - -extension DomainSuggestionsTableViewController: SearchTableViewCellDelegate { - func startSearch(for searchTerm: String) { - searchDebouncer.call { [weak self] in - self?.search(for: searchTerm) - } - } - - private func search(for searchTerm: String) { - removeNoResultsFromView() - delegate?.newSearchStarted() - - guard searchTerm.count > 0 else { - searchSuggestions = [] - return - } - - suggestDomains(for: searchTerm) - } -} - -extension SearchTableViewCell { - fileprivate func reloadTextfieldStyle() { - textField.textColor = .text - textField.leftViewImage = UIImage(named: "icon-post-search") - } -} diff --git a/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainSuggestions/RegisterDomainCoordinator.swift b/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainSuggestions/RegisterDomainCoordinator.swift new file mode 100644 index 000000000000..4af7f4766a1e --- /dev/null +++ b/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainSuggestions/RegisterDomainCoordinator.swift @@ -0,0 +1,295 @@ +import Foundation +import AutomatticTracks +import WordPressKit + +class RegisterDomainCoordinator { + + enum Error: Swift.Error { + case noDomainWhenCreatingCart + } + + // MARK: Type Aliases + + typealias DomainPurchasedCallback = ((UIViewController, String) -> Void) + typealias DomainAddedToCartCallback = ((UIViewController, String, Blog) -> Void) + + // MARK: Variables + + private let crashLogger: CrashLogging + + let analyticsSource: String + + var site: Blog? + var domainPurchasedCallback: DomainPurchasedCallback? + var domainAddedToCartAndLinkedToSiteCallback: DomainAddedToCartCallback? + var domain: DomainSuggestion? + + private var webViewURLChangeObservation: NSKeyValueObservation? + + /// Initializes a `RegisterDomainCoordinator` with the specified parameters. + /// + /// - Parameters: + /// - site: An optional `Blog` object representing the blog associated with the domain registration. + /// - domainPurchasedCallback: An optional closure to be called when a domain is successfully purchased. + /// - analyticsSource: A string representing the source for analytics tracking. Defaults to `domains_register` if not provided. + /// - crashLogger: An instance of `CrashLogging` to handle crash logging. Defaults to `.main` if not provided. + init(site: Blog?, + domainPurchasedCallback: RegisterDomainCoordinator.DomainPurchasedCallback? = nil, + analyticsSource: String = "domains_register", + crashLogger: CrashLogging = .main) { + self.site = site + self.domainPurchasedCallback = domainPurchasedCallback + self.crashLogger = crashLogger + self.analyticsSource = analyticsSource + } + + // MARK: Public Functions + + /// Adds the selected domain to the cart then launches the checkout webview. + /// This flow support purchasing domains only, without plans. + func handlePurchaseDomainOnly(on viewController: UIViewController, + onSuccess: @escaping () -> (), + onFailure: @escaping () -> ()) { + createCart { [weak self] result in + switch result { + case .success: + guard let self else { return } + self.presentCheckoutWebview(on: viewController, title: nil) + onSuccess() + case .failure: + onFailure() + } + } + } + + /// Adds the selected domain to the cart then executes `domainAddedToCartAndLinkedToSiteCallback` if set. + func addDomainToCartLinkedToCurrentSite(on viewController: UIViewController, + onSuccess: @escaping () -> (), + onFailure: @escaping () -> ()) { + guard let blog = site else { + return + } + createCart { [weak self] result in + switch result { + case .success(let domain): + self?.domainAddedToCartAndLinkedToSiteCallback?(viewController, domain.domainName, blog) + onSuccess() + case .failure: + onFailure() + } + } + } + + /// Related to the `purchaseFromDomainManagement` Domain selection type. + /// Adds the selected domain to the cart then launches the checkout webview + /// The checkout webview is configured for the domain management flow + func handleNoSiteChoice(on viewController: UIViewController, + choicesViewModel: DomainPurchaseChoicesViewModel?) { + createCart { [weak self] result in + switch result { + case .success: + self?.presentCheckoutWebview(on: viewController, title: TextContent.checkoutTitle) + choicesViewModel?.isGetDomainLoading = false + + case .failure: + viewController.displayActionableNotice(title: TextContent.errorTitle, actionTitle: TextContent.errorDismiss) + choicesViewModel?.isGetDomainLoading = false + } + } + } + + /// Related to the `purchaseFromDomainManagement` Domain selection type. + /// Adds the selected domain to the cart then presents a site picker view. + func handleExistingSiteChoice(on viewController: UIViewController) { + let config = BlogListConfiguration( + shouldShowCancelButton: false, + shouldShowNavBarButtons: false, + navigationTitle: TextContent.sitePickerNavigationTitle, + backButtonTitle: TextContent.sitePickerNavigationTitle, + shouldHideSelfHostedSites: true, + shouldHideBlogsNotSupportingDomains: true, + analyticsSource: analyticsSource + ) + let blogListViewController = BlogListViewController(configuration: config, meScenePresenter: nil) + + blogListViewController.blogSelected = { [weak self] controller, selectedBlog in + guard let self else { + return + } + controller.showLoading() + self.createCart { [weak self] result in + guard let self else { + return + } + switch result { + case .success(let domain): + self.site = selectedBlog + self.domainAddedToCartAndLinkedToSiteCallback?(controller, domain.domainName, selectedBlog) + case .failure: + controller.displayActionableNotice(title: TextContent.errorTitle, actionTitle: TextContent.errorDismiss) + } + controller.hideLoading() + } + } + + viewController.navigationController?.pushViewController(blogListViewController, animated: true) + } + + func trackDomainPurchasingCompleted() { + self.track(.purchaseDomainCompleted) + } + + // MARK: Helpers + + private func createCart(completion: @escaping (Result) -> Void) { + guard let domain else { + completion(.failure(Error.noDomainWhenCreatingCart)) + return + } + let siteID = site?.dotComID?.intValue + let proxy = RegisterDomainDetailsServiceProxy() + proxy.createPersistentDomainShoppingCart(siteID: siteID, + domainSuggestion: domain, + privacyProtectionEnabled: domain.supportsPrivacy ?? false, + success: { _ in + completion(.success(domain)) + }, + failure: { error in + completion(.failure(error)) + }) + } + + private func presentCheckoutWebview(on viewController: UIViewController, + title: String?) { + guard let domain, + let url = checkoutURL() else { + crashLogger.logMessage("Failed to present domain checkout webview.", + level: .error) + return + } + + let webViewController = WebViewControllerFactory.controllerWithDefaultAccountAndSecureInteraction( + url: url, + source: analyticsSource, + title: title + ) + + // WORKAROUND: The reason why we have to use this mechanism to detect success and failure conditions + // for domain registration is because our checkout process (for some unknown reason) doesn't trigger + // call to WKWebViewDelegate methods. + // + // This was last checked by @diegoreymendez on 2021-09-22. + // + webViewURLChangeObservation = webViewController.webView.observe(\.url, options: .new) { [weak self] _, change in + guard let self = self, + let newURL = change.newValue as? URL else { + return + } + + self.handleWebViewURLChange(newURL, domain: domain.domainName, onCancel: { + viewController.dismiss(animated: true) + }) { domain in + self.domainPurchasedCallback?(viewController, domain) + self.trackDomainPurchasingCompleted() + } + } + + let properties: [AnyHashable: Any] = { + if let site { + return WPAnalytics.domainsProperties(for: site, origin: nil as String?) + } else { + return WPAnalytics.domainsProperties(usingCredit: false, origin: nil, domainOnly: true) + } + }() + self.track(.domainsPurchaseWebviewViewed, properties: properties) + + webViewController.configureSandboxStore { + viewController.navigationController?.pushViewController(webViewController, animated: true) + } + } + + private func checkoutURL() -> URL? { + if let site { + guard let homeURL = site.homeURL, + let siteUrl = URL(string: homeURL as String), let host = siteUrl.host, + let url = URL(string: Constants.checkoutWebAddress + host) else { + return nil + } + return url + } else { + return URL(string: Constants.noSiteCheckoutWebAddress) + } + } + + /// Handles URL changes in the web view. We only allow the user to stay within certain URLs. Falling outside these URLs + /// results in the web view being dismissed. This method also handles the success condition for a successful domain registration + /// through said web view. + /// + /// - Parameters: + /// - newURL: the newly set URL for the web view. + /// - domain: the domain the user is purchasing. + /// - onCancel: the closure that will be executed if we detect the conditions for cancelling the registration were met. + /// - onSuccess: the closure that will be executed if we detect a successful domain registration. + /// + private func handleWebViewURLChange( + _ newURL: URL, + domain: String, + onCancel: () -> Void, + onSuccess: (String) -> Void) { + + let canOpenNewURL = newURL.absoluteString.starts(with: Constants.checkoutWebAddress) + + guard canOpenNewURL else { + onCancel() + return + } + + let domainRegistrationSucceeded = newURL.absoluteString.starts(with: Constants.checkoutSuccessURLPrefix) + + if domainRegistrationSucceeded { + onSuccess(domain) + + } + } + + // MARK: - Tracks + + private func track(_ event: WPAnalyticsEvent, properties: [AnyHashable: Any]? = nil) { + let defaultProperties: [AnyHashable: Any] = [WPAppAnalyticsKeySource: analyticsSource] + + let properties = defaultProperties.merging(properties ?? [:]) { first, second in + return first + } + + if let blog = self.site { + WPAnalytics.track(event, properties: properties, blog: blog) + } else { + WPAnalytics.track(event, properties: properties) + } + } +} + +// MARK: - Constants +extension RegisterDomainCoordinator { + + enum TextContent { + static let errorTitle = NSLocalizedString("domains.failure.title", + value: "Sorry, the domain you are trying to add cannot be bought on the Jetpack app at this time.", + comment: "Content show when the domain selection action fails.") + static let errorDismiss = NSLocalizedString("domains.failure.dismiss", + value: "Dismiss", + comment: "Action shown in a bottom notice to dismiss it.") + static let checkoutTitle = NSLocalizedString("domains.checkout.title", + value: "Checkout", + comment: "Title for the checkout screen.") + static let sitePickerNavigationTitle = NSLocalizedString("domains.sitePicker.title", + value: "Choose Site", + comment: "Title of screen where user chooses a site to connect to their selected domain") + } + + enum Constants { + static let checkoutWebAddress = "https://wordpress.com/checkout/" + static let noSiteCheckoutWebAddress = "https://wordpress.com/checkout/no-site?isDomainOnly=1" + static let checkoutSuccessURLPrefix = "https://wordpress.com/checkout/thank-you/" + } +} diff --git a/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainSuggestions/RegisterDomainSuggestionsViewController.swift b/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainSuggestions/RegisterDomainSuggestionsViewController.swift index 526b589fa840..130ec20efa35 100644 --- a/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainSuggestions/RegisterDomainSuggestionsViewController.swift +++ b/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainSuggestions/RegisterDomainSuggestionsViewController.swift @@ -3,43 +3,43 @@ import UIKit import WebKit import WordPressAuthenticator import WordPressFlux +import Combine enum DomainSelectionType { case registerWithPaidPlan case purchaseWithPaidPlan case purchaseSeparately + case purchaseFromDomainManagement } -class RegisterDomainSuggestionsViewController: UIViewController { - typealias DomainPurchasedCallback = ((RegisterDomainSuggestionsViewController, String) -> Void) - typealias DomainAddedToCartCallback = ((RegisterDomainSuggestionsViewController, String) -> Void) +class DomainPurchaseChoicesViewModel: ObservableObject { + @Published var isGetDomainLoading: Bool = false +} +class RegisterDomainSuggestionsViewController: UIViewController { @IBOutlet weak var buttonContainerBottomConstraint: NSLayoutConstraint! @IBOutlet weak var buttonContainerViewHeightConstraint: NSLayoutConstraint! private var constraintsInitialized = false - private var site: Blog! - var domainPurchasedCallback: DomainPurchasedCallback! - var domainAddedToCartCallback: DomainAddedToCartCallback? - - private var domain: FullyQuotedDomainSuggestion? + private var coordinator: RegisterDomainCoordinator? private var siteName: String? private var domainsTableViewController: DomainSuggestionsTableViewController? private var domainSelectionType: DomainSelectionType = .registerWithPaidPlan private var includeSupportButton: Bool = true - - private var webViewURLChangeObservation: NSKeyValueObservation? - - override func viewDidLoad() { - super.viewDidLoad() - configure() - hideButton() + private var navBarTitle: String = TextContent.title + private var analyticsSource: String? { + return coordinator?.analyticsSource } @IBOutlet private var buttonViewContainer: UIView! { didSet { - buttonViewController.move(to: self, into: buttonViewContainer) + guard let view = buttonViewContainer else { + return + } + view.addTopBorder(withColor: .divider) + view.backgroundColor = UIColor(light: .systemBackground, dark: .secondarySystemBackground) + buttonViewController.move(to: self, into: view) } } @@ -53,22 +53,49 @@ class RegisterDomainSuggestionsViewController: UIViewController { return buttonViewController }() - static func instance(site: Blog, + private lazy var transferFooterView: RegisterDomainTransferFooterView = { + let configuration = RegisterDomainTransferFooterView.Configuration { [weak self] in + guard let self else { + return + } + let destination = TransferDomainsWebViewController(source: self.analyticsSource) + self.present(UINavigationController(rootViewController: destination), animated: true) + self.track(.domainsSearchTransferDomainTapped) + } + return .init(configuration: configuration, analyticsSource: self.analyticsSource) + }() + + /// Represents the layout constraints for the transfer footer view in its visible and hidden states. + private lazy var transferFooterViewConstraints: (visible: [NSLayoutConstraint], hidden: [NSLayoutConstraint]) = { + let base = [ + transferFooterView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + transferFooterView.trailingAnchor.constraint(equalTo: view.trailingAnchor) + ] + let visible = base + [transferFooterView.bottomAnchor.constraint(equalTo: view.bottomAnchor)] + let hidden = base + [transferFooterView.topAnchor.constraint(equalTo: view.bottomAnchor)] + return (visible: visible, hidden: hidden) + }() + + static func instance(coordinator: RegisterDomainCoordinator, domainSelectionType: DomainSelectionType = .registerWithPaidPlan, includeSupportButton: Bool = true, - domainPurchasedCallback: DomainPurchasedCallback? = nil) -> RegisterDomainSuggestionsViewController { + title: String = TextContent.title) -> RegisterDomainSuggestionsViewController { let storyboard = UIStoryboard(name: Constants.storyboardIdentifier, bundle: Bundle.main) let controller = storyboard.instantiateViewController(withIdentifier: Constants.viewControllerIdentifier) as! RegisterDomainSuggestionsViewController - controller.site = site + controller.coordinator = coordinator controller.domainSelectionType = domainSelectionType - controller.domainPurchasedCallback = domainPurchasedCallback controller.includeSupportButton = includeSupportButton - controller.siteName = siteNameForSuggestions(for: site) + controller.siteName = siteNameForSuggestions(for: coordinator.site) + controller.navBarTitle = title return controller } - private static func siteNameForSuggestions(for site: Blog) -> String? { + private static func siteNameForSuggestions(for site: Blog?) -> String? { + guard let site else { + return nil + } + if let siteTitle = site.settings?.name?.nonEmptyString() { return siteTitle } @@ -83,8 +110,44 @@ class RegisterDomainSuggestionsViewController: UIViewController { return nil } + // MARK: - View Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + configure() + hideButton() + setupTransferFooterView() + track(.domainsDashboardDomainsSearchShown) + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + if let domainsTableViewController { + let insets = UIEdgeInsets( + top: 0, + left: 0, + bottom: transferFooterView.isHidden ? 0 : transferFooterView.bounds.height, + right: 0 + ) + if insets != domainsTableViewController.additionalSafeAreaInsets { + domainsTableViewController.additionalSafeAreaInsets = insets + } + } + } + + // MARK: - Setup subviews + + private func setupTransferFooterView() { + guard domainSelectionType == .purchaseFromDomainManagement else { + return + } + self.view.addSubview(transferFooterView) + self.transferFooterView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate(transferFooterViewConstraints.visible) + } + private func configure() { - title = TextContent.title + title = navBarTitle WPStyleGuide.configureColors(view: view, tableView: nil) /// If this is the first view controller in the navigation controller - show the cancel button @@ -95,6 +158,8 @@ class RegisterDomainSuggestionsViewController: UIViewController { navigationItem.leftBarButtonItem = cancelButton } + navigationItem.backButtonTitle = TextContent.searchBackButtonTitle + guard includeSupportButton else { return } @@ -106,6 +171,39 @@ class RegisterDomainSuggestionsViewController: UIViewController { navigationItem.rightBarButtonItem = supportButton } + // MARK: - Show / Hide Transfer Footer + + /// Updates transfer footer view constraints to either hide or show the view. + private func updateTransferFooterViewConstraints(hidden: Bool, animated: Bool = true) { + guard transferFooterView.superview != nil else { + return + } + + let constraints = transferFooterViewConstraints + let duration = animated ? WPAnimationDurationDefault : 0 + + NSLayoutConstraint.deactivate(hidden ? constraints.visible : constraints.hidden) + NSLayoutConstraint.activate(hidden ? constraints.hidden : constraints.visible) + + if !hidden { + self.transferFooterView.isHidden = false + } + UIView.animate(withDuration: duration) { + self.view.layoutIfNeeded() + } completion: { _ in + self.transferFooterView.isHidden = hidden + self.view.setNeedsLayout() + } + } + + private func showTransferFooterView(animated: Bool = true) { + self.updateTransferFooterViewConstraints(hidden: false, animated: animated) + } + + private func hideTransferFooterView(animated: Bool = true) { + self.updateTransferFooterViewConstraints(hidden: true, animated: animated) + } + // MARK: - Bottom Hideable Button /// Shows the domain picking button @@ -173,11 +271,11 @@ class RegisterDomainSuggestionsViewController: UIViewController { if let vc = segue.destination as? DomainSuggestionsTableViewController { vc.delegate = self vc.siteName = siteName - vc.blog = site + vc.blog = coordinator?.site vc.domainSelectionType = domainSelectionType - vc.freeSiteAddress = site.freeSiteAddress + vc.primaryDomainAddress = coordinator?.site?.primaryDomainAddress - if site.hasBloggerPlan { + if coordinator?.site?.hasBloggerPlan == true { vc.domainSuggestionType = .allowlistedTopLevelDomains(["blog"]) } @@ -196,20 +294,49 @@ class RegisterDomainSuggestionsViewController: UIViewController { supportVC.showFromTabBar() } + // MARK: - Tracks + + private func track(_ event: WPAnalyticsEvent, properties: [AnyHashable: Any] = [:], blog: Blog? = nil) { + let defaultProperties = { () -> [AnyHashable: Any] in + if let blog { + return WPAnalytics.domainsProperties(for: blog, origin: self.analyticsSource) + } else { + return WPAnalytics.domainsProperties(origin: self.analyticsSource) + } + }() + + let properties = properties.merging(defaultProperties) { current, _ in + return current + } + + if let blog { + WPAnalytics.track(event, properties: properties, blog: blog) + } else { + WPAnalytics.track(event, properties: properties) + } + } } // MARK: - DomainSuggestionsTableViewControllerDelegate extension RegisterDomainSuggestionsViewController: DomainSuggestionsTableViewControllerDelegate { - func domainSelected(_ domain: FullyQuotedDomainSuggestion) { - WPAnalytics.track(.automatedTransferCustomDomainSuggestionSelected) - self.domain = domain - showButton(animated: true) + func domainSelected(_ domain: FullyQuotedDomainSuggestion?) { + self.coordinator?.domain = domain + + if domain != nil { + WPAnalytics.track(.automatedTransferCustomDomainSuggestionSelected) + showButton(animated: true) + hideTransferFooterView(animated: true) + } else { + hideButton(animated: true) + showTransferFooterView(animated: true) + } } func newSearchStarted() { WPAnalytics.track(.automatedTransferCustomDomainSuggestionQueried) hideButton(animated: true) + showTransferFooterView(animated: true) } } @@ -217,11 +344,18 @@ extension RegisterDomainSuggestionsViewController: DomainSuggestionsTableViewCon extension RegisterDomainSuggestionsViewController: NUXButtonViewControllerDelegate { func primaryButtonPressed() { - guard let domain = domain else { + guard let coordinator else { return } - WPAnalytics.track(.domainsSearchSelectDomainTapped, properties: WPAnalytics.domainsProperties(for: site), blog: site) + let properties: [AnyHashable: Any] = { + guard let domainName = self.coordinator?.domain?.domainName else { + return [:] + } + return ["domain_name": domainName] + }() + + self.track(.domainsSearchSelectDomainTapped, properties: properties, blog: coordinator.site) let onFailure: () -> () = { [weak self] in self?.displayActionableNotice(title: TextContent.errorTitle, actionTitle: TextContent.errorDismiss) @@ -230,31 +364,26 @@ extension RegisterDomainSuggestionsViewController: NUXButtonViewControllerDelega switch domainSelectionType { case .registerWithPaidPlan: - pushRegisterDomainDetailsViewController(domain) + pushRegisterDomainDetailsViewController() case .purchaseSeparately: setPrimaryButtonLoading(true) - createCart( - domain, + coordinator.handlePurchaseDomainOnly( + on: self, onSuccess: { [weak self] in - self?.presentWebViewForCurrentSite(domainSuggestion: domain) self?.setPrimaryButtonLoading(false, afterDelay: 0.25) }, - onFailure: onFailure - ) + onFailure: onFailure) case .purchaseWithPaidPlan: setPrimaryButtonLoading(true) - createCart( - domain, + coordinator.addDomainToCartLinkedToCurrentSite( + on: self, onSuccess: { [weak self] in - guard let self = self else { - return - } - - self.domainAddedToCartCallback?(self, domain.domainName) - self.setPrimaryButtonLoading(false, afterDelay: 0.25) + self?.setPrimaryButtonLoading(false, afterDelay: 0.25) }, onFailure: onFailure ) + case .purchaseFromDomainManagement: + pushPurchaseDomainChoiceScreen() } } @@ -267,120 +396,40 @@ extension RegisterDomainSuggestionsViewController: NUXButtonViewControllerDelega } } - private func pushRegisterDomainDetailsViewController(_ domain: FullyQuotedDomainSuggestion) { - guard let siteID = site.dotComID?.intValue else { + private func pushRegisterDomainDetailsViewController() { + guard let siteID = coordinator?.site?.dotComID?.intValue else { DDLogError("Cannot register domains for sites without a dotComID") return } + guard let domain = coordinator?.domain else { + return + } + let controller = RegisterDomainDetailsViewController() controller.viewModel = RegisterDomainDetailsViewModel(siteID: siteID, domain: domain) { [weak self] name in - guard let self = self else { + guard let self = self, let coordinator else { return } - - self.domainPurchasedCallback?(self, name) + coordinator.domainPurchasedCallback?(self, name) + coordinator.trackDomainPurchasingCompleted() } self.navigationController?.pushViewController(controller, animated: true) } - private func createCart(_ domain: FullyQuotedDomainSuggestion, - onSuccess: @escaping () -> (), - onFailure: @escaping () -> ()) { - guard let siteID = site.dotComID?.intValue else { - DDLogError("Cannot register domains for sites without a dotComID") - return - } - - let proxy = RegisterDomainDetailsServiceProxy() - proxy.createPersistentDomainShoppingCart(siteID: siteID, - domainSuggestion: domain.remoteSuggestion(), - privacyProtectionEnabled: domain.supportsPrivacy ?? false, - success: { _ in - onSuccess() - }, - failure: { _ in - onFailure() - }) - } - - static private let checkoutURLPrefix = "https://wordpress.com/checkout" - static private let checkoutSuccessURLPrefix = "https://wordpress.com/checkout/thank-you/" - - /// Handles URL changes in the web view. We only allow the user to stay within certain URLs. Falling outside these URLs - /// results in the web view being dismissed. This method also handles the success condition for a successful domain registration - /// through said web view. - /// - /// - Parameters: - /// - newURL: the newly set URL for the web view. - /// - siteID: the ID of the site we're trying to register the domain against. - /// - domain: the domain the user is purchasing. - /// - onCancel: the closure that will be executed if we detect the conditions for cancelling the registration were met. - /// - onSuccess: the closure that will be executed if we detect a successful domain registration. - /// - private func handleWebViewURLChange( - _ newURL: URL, - siteID: Int, - domain: String, - onCancel: () -> Void, - onSuccess: (String) -> Void) { - - let canOpenNewURL = newURL.absoluteString.starts(with: Self.checkoutURLPrefix) - - guard canOpenNewURL else { - onCancel() - return - } - - let domainRegistrationSucceeded = newURL.absoluteString.starts(with: Self.checkoutSuccessURLPrefix) - - if domainRegistrationSucceeded { - onSuccess(domain) - - } - } - - private func presentWebViewForCurrentSite(domainSuggestion: FullyQuotedDomainSuggestion) { - guard let homeURL = site.homeURL, - let siteUrl = URL(string: homeURL as String), let host = siteUrl.host, - let url = URL(string: Constants.checkoutWebAddress + host), - let siteID = site.dotComID?.intValue else { - return - } - - let webViewController = WebViewControllerFactory.controllerWithDefaultAccountAndSecureInteraction(url: url, source: "domains_register") - let navController = LightNavigationController(rootViewController: webViewController) - - // WORKAROUND: The reason why we have to use this mechanism to detect success and failure conditions - // for domain registration is because our checkout process (for some unknown reason) doesn't trigger - // call to WKWebViewDelegate methods. - // - // This was last checked by @diegoreymendez on 2021-09-22. - // - webViewURLChangeObservation = webViewController.webView.observe(\.url, options: .new) { [weak self] _, change in - guard let self = self, - let newURL = change.newValue as? URL else { - return - } - - self.handleWebViewURLChange(newURL, siteID: siteID, domain: domainSuggestion.domainName, onCancel: { - navController.dismiss(animated: true) - }) { domain in - self.dismiss(animated: true, completion: { [weak self] in - guard let self = self else { - return - } - - self.domainPurchasedCallback(self, domain) - }) - } - } - - WPAnalytics.track(.domainsPurchaseWebviewViewed, properties: WPAnalytics.domainsProperties(for: site), blog: site) - - webViewController.configureSandboxStore { [weak self] in - self?.present(navController, animated: true) + private func pushPurchaseDomainChoiceScreen() { + @ObservedObject var choicesViewModel = DomainPurchaseChoicesViewModel() + let view = DomainPurchaseChoicesView(viewModel: choicesViewModel, analyticsSource: analyticsSource) { [weak self] in + guard let self else { return } + choicesViewModel.isGetDomainLoading = true + self.coordinator?.handleNoSiteChoice(on: self, choicesViewModel: choicesViewModel) + } chooseSiteAction: { [weak self] in + guard let self else { return } + self.coordinator?.handleExistingSiteChoice(on: self) } + let hostingController = UIHostingController(rootView: view) + hostingController.title = TextContent.domainChoiceTitle + self.navigationController?.pushViewController(hostingController, animated: true) } } @@ -401,16 +450,17 @@ extension RegisterDomainSuggestionsViewController { static let errorDismiss = NSLocalizedString("domains.failure.dismiss", value: "Dismiss", comment: "Action shown in a bottom notice to dismiss it.") + static let domainChoiceTitle = NSLocalizedString("domains.purchase.choice.title", + value: "Purchase Domain", + comment: "Title for the screen where the user can choose how to use the domain they're end up purchasing.") + static let searchBackButtonTitle = NSLocalizedString("domains.search.backButton.title", + value: "Search", + comment: "Back button title that navigates back to the search domains screen.") } enum Constants { // storyboard identifiers static let storyboardIdentifier = "RegisterDomain" static let viewControllerIdentifier = "RegisterDomainSuggestionsViewController" - - static let checkoutWebAddress = "https://wordpress.com/checkout/" - // store sandbox cookie - static let storeSandboxCookieName = "store_sandbox" - static let storeSandboxCookieDomain = ".wordpress.com" } } diff --git a/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainSuggestions/RegisterDomainTransferFooterView.swift b/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainSuggestions/RegisterDomainTransferFooterView.swift new file mode 100644 index 000000000000..aaa8b6d7fc4a --- /dev/null +++ b/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainSuggestions/RegisterDomainTransferFooterView.swift @@ -0,0 +1,109 @@ +import SwiftUI +import UIKit +import WordPressUI +import DesignSystem + +final class RegisterDomainTransferFooterView: UIView { + + // MARK: - Types + + struct Configuration { + + let title: String + let buttonTitle: String + let buttonAction: () -> Void + + init( + title: String = Strings.title, + buttonTitle: String = Strings.buttonTitle, + buttonAction: @escaping () -> Void + ) { + self.title = title + self.buttonTitle = buttonTitle + self.buttonAction = buttonAction + } + } + + struct Strings { + + static let title = NSLocalizedString( + "register.domain.transfer.title", + value: "Looking to transfer a domain you already own?", + comment: "The title for the transfer footer view in Register Domain screen" + ) + static let buttonTitle = NSLocalizedString( + "register.domain.transfer.button.title", + value: "Transfer domain", + comment: "The button title for the transfer footer view in Register Domain screen" + ) + } + + // MARK: - Properties + + private let analyticsSource: String? + + // MARK: - Views + + private let titleLabel: UILabel = { + let label = UILabel() + label.font = WPStyleGuide.fontForTextStyle(.body, fontWeight: .regular) + label.textColor = UIColor.DS.Foreground.primary + label.numberOfLines = 2 + label.adjustsFontForContentSizeCategory = true + return label + }() + + private let primaryButton: UIButton = { + let button = FancyButton() + button.titleLabel?.font = WPStyleGuide.fontForTextStyle(.headline, fontWeight: .regular) + button.isPrimary = true + return button + }() + + // MARK: - Init + + init(configuration: Configuration, analyticsSource: String? = nil) { + self.analyticsSource = analyticsSource + super.init(frame: .zero) + self.backgroundColor = UIColor(light: .systemBackground, dark: .secondarySystemBackground) + self.addTopBorder(withColor: .divider) + self.setup(with: configuration) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Setup + + private func setup(with configuration: Configuration) { + let stackView = UIStackView(arrangedSubviews: [titleLabel, primaryButton]) + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.axis = .vertical + stackView.spacing = Length.Padding.double + stackView.distribution = .fill + + self.addSubview(stackView) + + self.directionalLayoutMargins = NSDirectionalEdgeInsets( + top: Length.Padding.double, + leading: Length.Padding.double, + bottom: Length.Padding.double, + trailing: Length.Padding.double + ) + + NSLayoutConstraint.activate([ + stackView.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor), + stackView.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor), + readableContentGuide.trailingAnchor.constraint(equalTo: stackView.trailingAnchor), + layoutMarginsGuide.bottomAnchor.constraint(equalTo: stackView.bottomAnchor) + ]) + + let action = UIAction { _ in + configuration.buttonAction() + } + self.titleLabel.text = configuration.title + self.primaryButton.setTitle(configuration.buttonTitle, for: .normal) + self.primaryButton.addAction(action, for: .touchUpInside) + } +} diff --git a/WordPress/Classes/ViewRelated/Domains/Domain registration/SiteCreationPurchasingWebFlowController.swift b/WordPress/Classes/ViewRelated/Domains/Domain registration/SiteCreationPurchasingWebFlowController.swift index c918c982f0ec..546533b8c8c9 100644 --- a/WordPress/Classes/ViewRelated/Domains/Domain registration/SiteCreationPurchasingWebFlowController.swift +++ b/WordPress/Classes/ViewRelated/Domains/Domain registration/SiteCreationPurchasingWebFlowController.swift @@ -27,7 +27,7 @@ final class SiteCreationPurchasingWebFlowController { /// The domain purchasing web view can be presented from multiple sources. /// This property represents this source, and it's used mostly for analytics. - private let origin: SiteCreationWebViewViewOrigin? + private let origin: DomainsAnalyticsWebViewOrigin? // MARK: - Execution Variables @@ -42,7 +42,7 @@ final class SiteCreationPurchasingWebFlowController { init(viewController: UIViewController, shoppingCartService: ShoppingCartServiceProtocol = ShoppingCartService(), crashLogger: CrashLogging = .main, - origin: SiteCreationWebViewViewOrigin? = nil) { + origin: DomainsAnalyticsWebViewOrigin? = nil) { self.presentingViewController = viewController self.shoppingCartService = shoppingCartService self.crashLogger = crashLogger diff --git a/WordPress/Classes/ViewRelated/Site Creation/Web Address/WebAddressWizardContent.swift b/WordPress/Classes/ViewRelated/Domains/DomainSelectionViewController.swift similarity index 54% rename from WordPress/Classes/ViewRelated/Site Creation/Web Address/WebAddressWizardContent.swift rename to WordPress/Classes/ViewRelated/Domains/DomainSelectionViewController.swift index aa973f4663fa..c1423edb1389 100644 --- a/WordPress/Classes/ViewRelated/Site Creation/Web Address/WebAddressWizardContent.swift +++ b/WordPress/Classes/ViewRelated/Domains/DomainSelectionViewController.swift @@ -2,37 +2,44 @@ import UIKit import WordPressAuthenticator import SwiftUI +enum DomainSelectionType: Int, Identifiable { + var id: Int { rawValue } + case siteCreation + case registerWithPaidPlan + case purchaseWithPaidPlan + case purchaseSeparately + case purchaseFromDomainManagement +} + /// Contains the UI corresponding to the list of Domain suggestions. /// -final class WebAddressWizardContent: CollapsableHeaderViewController { +final class DomainSelectionViewController: CollapsableHeaderViewController { static let noMatchCellReuseIdentifier = "noMatchCellReuseIdentifier" // MARK: Properties private struct Metrics { static let maxLabelWidth = CGFloat(290) static let noResultsTopInset = CGFloat(64) - static let sitePromptEdgeMargin = CGFloat(50) - static let sitePromptBottomMargin = CGFloat(10) - static let sitePromptTopMargin = CGFloat(25) + static let sitePromptTopMargin = CGFloat(4) } override var separatorStyle: SeparatorStyle { return .hidden } - /// Checks if the Domain Purchasing Feature Flag and AB Experiment are enabled - private var domainPurchasingEnabled: Bool { - return siteCreator.domainPurchasingEnabled - } - /// The creator collects user input as they advance through the wizard flow. - private let siteCreator: SiteCreator private let service: SiteAddressService - private let selection: (DomainSuggestion) -> Void + private let selection: ((DomainSuggestion) -> Void)? + private let coordinator: RegisterDomainCoordinator? /// Tracks the site address selected by users private var selectedDomain: DomainSuggestion? { didSet { + coordinator?.domain = selectedDomain + if selectedDomain != nil { + trackDomainSelected() + hideTransferFooterView() + } itemSelectionChanged(selectedDomain != nil) } } @@ -42,9 +49,10 @@ final class WebAddressWizardContent: CollapsableHeaderViewController { private let searchHeader: UIView private let searchTextField: SearchTextField private let searchBar = UISearchBar() - private var sitePromptView: SitePromptView! private let siteCreationEmptyTemplate = SiteCreationEmptySiteTemplate() private lazy var siteTemplateHostingController = UIHostingController(rootView: siteCreationEmptyTemplate) + private let domainSelectionType: DomainSelectionType + private let includeSupportButton: Bool /// The underlying data represented by the provider var data: [DomainSuggestion] { @@ -97,12 +105,46 @@ final class WebAddressWizardContent: CollapsableHeaderViewController { } } + // MARK: - Transfer Footer Views + + private lazy var transferFooterView: RegisterDomainTransferFooterView = { + let configuration = RegisterDomainTransferFooterView.Configuration { [weak self] in + guard let self else { + return + } + let destination = TransferDomainsWebViewController(source: self.coordinator?.analyticsSource) + self.present(UINavigationController(rootViewController: destination), animated: true) + } + return .init(configuration: configuration) + }() + + /// Represents the layout constraints for the transfer footer view in its visible and hidden states. + private lazy var transferFooterViewConstraints: (visible: [NSLayoutConstraint], hidden: [NSLayoutConstraint]) = { + let base = [ + transferFooterView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + transferFooterView.trailingAnchor.constraint(equalTo: view.trailingAnchor) + ] + + let visible = base + [transferFooterView.bottomAnchor.constraint(equalTo: view.bottomAnchor)] + let hidden = base + [transferFooterView.topAnchor.constraint(equalTo: view.bottomAnchor)] + return (visible: visible, hidden: hidden) + }() + // MARK: WebAddressWizardContent - init(creator: SiteCreator, service: SiteAddressService, selection: @escaping (DomainSuggestion) -> Void) { - self.siteCreator = creator + init( + service: SiteAddressService, + domainSelectionType: DomainSelectionType, + primaryActionTitle: String = Strings.selectDomain, + includeSupportButton: Bool = false, + selection: ((DomainSuggestion) -> Void)? = nil, + coordinator: RegisterDomainCoordinator? = nil + ) { self.service = service + self.domainSelectionType = domainSelectionType + self.includeSupportButton = includeSupportButton self.selection = selection + self.coordinator = coordinator self.data = [] self.noResultsLabel = { let label = UILabel() @@ -126,9 +168,9 @@ final class WebAddressWizardContent: CollapsableHeaderViewController { searchHeader = UIView(frame: .zero) table = UITableView(frame: .zero, style: .grouped) super.init(scrollableView: table, - mainTitle: Strings.mainTitle, - prompt: Strings.prompt, - primaryActionTitle: creator.domainPurchasingEnabled ? Strings.selectDomain : Strings.createSite, + mainTitle: domainSelectionType == .siteCreation ? Strings.mainTitle : Strings.alternativeTitle, + prompt: Strings.prompt(domainSelectionType, coordinator?.site), + primaryActionTitle: primaryActionTitle, accessoryView: searchHeader) } @@ -141,18 +183,16 @@ final class WebAddressWizardContent: CollapsableHeaderViewController { override func viewDidLoad() { super.viewDidLoad() setupTable() - WPAnalytics.track(.enhancedSiteCreationDomainsAccessed) + trackViewDidLoad() loadHeaderView() addAddressHintView() configureUIIfNeeded() - navigationItem.backButtonTitle = Strings.backButtonTitle + setupBackButton() + setupTransferFooterView() + includeSupportButtonIfNeeded() } private func configureUIIfNeeded() { - guard domainPurchasingEnabled else { - return - } - NSLayoutConstraint.activate([ largeTitleView.widthAnchor.constraint(equalTo: headerStackView.widthAnchor) ]) @@ -162,33 +202,20 @@ final class WebAddressWizardContent: CollapsableHeaderViewController { } private func loadHeaderView() { + searchBar.searchBarStyle = UISearchBar.Style.default + searchBar.translatesAutoresizingMaskIntoConstraints = false + WPStyleGuide.configureSearchBar(searchBar, backgroundColor: .clear, returnKeyType: .search) + searchBar.layer.borderWidth = 0 + searchHeader.addSubview(searchBar) + searchBar.delegate = self + headerView.backgroundColor = .basicBackground - if domainPurchasingEnabled { - searchBar.searchBarStyle = UISearchBar.Style.default - searchBar.translatesAutoresizingMaskIntoConstraints = false - WPStyleGuide.configureSearchBar(searchBar, backgroundColor: .clear, returnKeyType: .search) - searchBar.layer.borderWidth = 0 - searchHeader.addSubview(searchBar) - searchBar.delegate = self - headerView.backgroundColor = .basicBackground - - NSLayoutConstraint.activate([ - searchBar.leadingAnchor.constraint(equalTo: searchHeader.leadingAnchor, constant: 8), - searchHeader.trailingAnchor.constraint(equalTo: searchBar.trailingAnchor, constant: 8), - searchBar.topAnchor.constraint(equalTo: searchHeader.topAnchor, constant: 1), - searchHeader.bottomAnchor.constraint(equalTo: searchBar.bottomAnchor, constant: 1) - ]) - } else { - searchHeader.addSubview(searchTextField) - searchHeader.backgroundColor = searchTextField.backgroundColor - let top = NSLayoutConstraint(item: searchTextField, attribute: .top, relatedBy: .equal, toItem: searchHeader, attribute: .top, multiplier: 1, constant: 0) - let bottom = NSLayoutConstraint(item: searchTextField, attribute: .bottom, relatedBy: .equal, toItem: searchHeader, attribute: .bottom, multiplier: 1, constant: 0) - let leading = NSLayoutConstraint(item: searchTextField, attribute: .leading, relatedBy: .equal, toItem: searchHeader, attribute: .leadingMargin, multiplier: 1, constant: 0) - let trailing = NSLayoutConstraint(item: searchTextField, attribute: .trailing, relatedBy: .equal, toItem: searchHeader, attribute: .trailingMargin, multiplier: 1, constant: 0) - searchHeader.addConstraints([top, bottom, leading, trailing]) - searchHeader.addTopBorder(withColor: .divider) - searchHeader.addBottomBorder(withColor: .divider) - } + NSLayoutConstraint.activate([ + searchBar.leadingAnchor.constraint(equalTo: searchHeader.leadingAnchor, constant: 8), + searchHeader.trailingAnchor.constraint(equalTo: searchBar.trailingAnchor, constant: 8), + searchBar.topAnchor.constraint(equalTo: searchHeader.topAnchor, constant: 1), + searchHeader.bottomAnchor.constraint(equalTo: searchBar.bottomAnchor, constant: 1) + ]) } override func viewWillAppear(_ animated: Bool) { @@ -221,14 +248,8 @@ final class WebAddressWizardContent: CollapsableHeaderViewController { coordinator.animate(alongsideTransition: nil) { [weak self] (_) in guard let self else { return } - if self.domainPurchasingEnabled { - if !self.siteTemplateHostingController.view.isHidden { - self.updateTitleViewVisibility(true) - } - } else { - if !self.sitePromptView.isHidden { - self.updateTitleViewVisibility(true) - } + if !self.siteTemplateHostingController.view.isHidden { + self.updateTitleViewVisibility(true) } } } @@ -256,7 +277,20 @@ final class WebAddressWizardContent: CollapsableHeaderViewController { private func fetchAddresses(_ searchTerm: String) { isShowingError = false updateIcon(isLoading: true) - service.addresses(for: searchTerm) { [weak self] results in + + let type: DomainsServiceRemote.DomainSuggestionType + switch domainSelectionType { + case .siteCreation: + type = RemoteFeatureFlag.plansInSiteCreation.enabled() ? .freeAndPaid : .wordPressDotComAndDotBlogSubdomains + default: + if coordinator?.site?.hasBloggerPlan == true { + type = .allowlistedTopLevelDomains(["blog"]) + } else { + type = .noWordpressDotCom + } + } + + service.addresses(for: searchTerm, type: type) { [weak self] results in DispatchQueue.main.async { self?.handleResult(results) } @@ -326,8 +360,52 @@ final class WebAddressWizardContent: CollapsableHeaderViewController { return } + trackDomainsSelection(selectedDomain) - selection(selectedDomain) + + let onFailure: () -> () = { [weak self] in + self?.setPrimaryButtonLoading(false, afterDelay: 0.25) + self?.displayActionableNotice(title: Strings.errorTitle, actionTitle: Strings.errorDismiss) + } + + switch domainSelectionType { + case .registerWithPaidPlan: + pushRegisterDomainDetailsViewController() + case .purchaseSeparately: + setPrimaryButtonLoading(true) + coordinator?.handlePurchaseDomainOnly( + on: self, + onSuccess: { [weak self] in + self?.setPrimaryButtonLoading(false, afterDelay: 0.25) + }, + onFailure: onFailure) + case .purchaseWithPaidPlan: + setPrimaryButtonLoading(true) + coordinator?.addDomainToCartLinkedToCurrentSite( + on: self, + onSuccess: { [weak self] in + self?.setPrimaryButtonLoading(false, afterDelay: 0.25) + }, + onFailure: onFailure + ) + case .purchaseFromDomainManagement: + pushPurchaseDomainChoiceScreen() + case .siteCreation: + selection?(selectedDomain) + } + } + + private func setPrimaryButtonLoading(_ isLoading: Bool, afterDelay delay: Double = 0.0) { + // We're dispatching here so that we can wait until after the webview has been + // fully presented before we switch the button back to its default state. + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { + self.primaryActionButton.isEnabled = !isLoading + if isLoading { + SVProgressHUD.show() + } else { + SVProgressHUD.dismiss() + } + } } private func setupCells() { @@ -338,11 +416,7 @@ final class WebAddressWizardContent: CollapsableHeaderViewController { } private func restoreSearchIfNeeded() { - if domainPurchasingEnabled { - search(searchBar.text) - } else { - search(query(from: searchTextField)) - } + search(searchBar.text) } private func prepareViewIfNeeded() { @@ -380,9 +454,6 @@ final class WebAddressWizardContent: CollapsableHeaderViewController { } private func setupTable() { - if !domainPurchasingEnabled { - table.separatorStyle = .none - } table.dataSource = self table.estimatedRowHeight = AddressTableViewCell.estimatedSize.height setupTableBackground() @@ -390,7 +461,7 @@ final class WebAddressWizardContent: CollapsableHeaderViewController { setupCells() setupHeaderAndNoResultsMessage() table.showsVerticalScrollIndicator = false - table.isAccessibilityElement = false + table.accessibilityIdentifier = "DomainSelectionTable" } private func setupTableBackground() { @@ -402,15 +473,6 @@ final class WebAddressWizardContent: CollapsableHeaderViewController { table.separatorInset.left = AddressTableViewCell.Appearance.contentMargins.leading } - private func query(from textField: UITextField?) -> String? { - guard let text = textField?.text, - !text.isEmpty else { - return siteCreator.information?.title - } - - return text - } - @objc private func textChanged(sender: UITextField) { search(sender.text) @@ -422,19 +484,6 @@ final class WebAddressWizardContent: CollapsableHeaderViewController { itemSelectionChanged(false) } - private func trackDomainsSelection(_ domainSuggestion: DomainSuggestion) { - var domainSuggestionProperties: [String: Any] = [ - "chosen_domain": domainSuggestion.domainName as AnyObject, - "search_term": lastSearchQuery as AnyObject - ] - - if domainPurchasingEnabled { - domainSuggestionProperties["domain_cost"] = domainSuggestion.costString - } - - WPAnalytics.track(.enhancedSiteCreationDomainsSelected, withProperties: domainSuggestionProperties) - } - // MARK: - Search logic func updateIcon(isLoading: Bool) { @@ -444,54 +493,40 @@ final class WebAddressWizardContent: CollapsableHeaderViewController { private func search(_ string: String?) { guard let query = string, query.isEmpty == false else { clearContent() + showTransferFooterView() return } + hideTransferFooterView() performSearchIfNeeded(query: query) + trackSearchStarted() } // MARK: - Search logic private func setAddressHintVisibility(isHidden: Bool) { - if domainPurchasingEnabled { - siteTemplateHostingController.view?.isHidden = isHidden - } else { - sitePromptView.isHidden = isHidden - } + siteTemplateHostingController.view?.isHidden = isHidden } private func addAddressHintView() { - if domainPurchasingEnabled { - guard let siteCreationView = siteTemplateHostingController.view else { - return - } - siteCreationView.isUserInteractionEnabled = false - siteCreationView.translatesAutoresizingMaskIntoConstraints = false - containerView.addSubview(siteCreationView) - NSLayoutConstraint.activate([ - siteCreationView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16), - containerView.trailingAnchor.constraint(equalTo: siteCreationView.trailingAnchor, constant: 16), - siteCreationView.topAnchor.constraint(equalTo: searchHeader.bottomAnchor, constant: Metrics.sitePromptTopMargin), - containerView.bottomAnchor.constraint(equalTo: siteCreationView.bottomAnchor, constant: 0) - ]) - } else { - sitePromptView = SitePromptView(frame: .zero) - sitePromptView.isUserInteractionEnabled = false - sitePromptView.translatesAutoresizingMaskIntoConstraints = false - containerView.addSubview(sitePromptView) - NSLayoutConstraint.activate([ - sitePromptView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: Metrics.sitePromptEdgeMargin), - sitePromptView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -Metrics.sitePromptEdgeMargin), - sitePromptView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: Metrics.sitePromptBottomMargin), - sitePromptView.topAnchor.constraint(equalTo: searchHeader.bottomAnchor, constant: Metrics.sitePromptTopMargin) - ]) + guard let siteCreationView = siteTemplateHostingController.view else { + return } + siteCreationView.isUserInteractionEnabled = false + siteCreationView.translatesAutoresizingMaskIntoConstraints = false + containerView.addSubview(siteCreationView) + NSLayoutConstraint.activate([ + siteCreationView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16), + containerView.trailingAnchor.constraint(equalTo: siteCreationView.trailingAnchor, constant: 16), + siteCreationView.topAnchor.constraint(equalTo: searchHeader.bottomAnchor, constant: Metrics.sitePromptTopMargin), + containerView.bottomAnchor.constraint(equalTo: siteCreationView.bottomAnchor, constant: 0) + ]) setAddressHintVisibility(isHidden: true) } // MARK: - Others - private enum Strings { + enum Strings { static let suggestionsUpdated = NSLocalizedString("Suggestions updated", comment: "Announced by VoiceOver when new domains suggestions are shown in Site Creation.") static let noResults = NSLocalizedString("No available addresses matching your search", @@ -504,8 +539,16 @@ final class WebAddressWizardContent: CollapsableHeaderViewController { comment: "Displayed during Site Creation, when searching for Verticals and the server returns an error.") static let mainTitle: String = NSLocalizedString("Choose a domain", comment: "Select domain name. Title") + static let alternativeTitle: String = NSLocalizedString("domainSelection.search.title", + value: "Search domains", + comment: "Search domain - Title for the Suggested domains screen") static let prompt: String = NSLocalizedString("Search for a short and memorable keyword to help people find and visit your website.", comment: "Select domain name. Subtitle") + + static let directPurchasePrompt: String = NSLocalizedString("domainSelection.redirectPrompt.title", + value: "Domains purchased on this site will redirect to %1$@", + comment: "Description for the first domain purchased with a free plan.") + static let createSite: String = NSLocalizedString("Create Site", comment: "Button to progress to the next step") static let selectDomain: String = NSLocalizedString("siteCreation.domains.buttons.selectDomain", @@ -523,12 +566,32 @@ final class WebAddressWizardContent: CollapsableHeaderViewController { value: "Domains", comment: "Back button title shown in Site Creation flow to come back from Plan selection to Domain selection" ) + static let supportButtonTitle = NSLocalizedString("domainSelection.helpButton.title", + value: "Help", + comment: "Help button") + static let domainChoiceTitle = NSLocalizedString("domains.purchase.choice.title", + value: "Purchase Domain", + comment: "Title for the screen where the user can choose how to use the domain they're end up purchasing.") + static let errorTitle = NSLocalizedString("domains.failure.title", + value: "Sorry, the domain you are trying to add cannot be bought on the Jetpack app at this time.", + comment: "Content show when the domain selection action fails.") + static let errorDismiss = NSLocalizedString("domains.failure.dismiss", + value: "Dismiss", + comment: "Action shown in a bottom notice to dismiss it.") + + static func prompt(_ type: DomainSelectionType, _ blog: Blog?) -> String { + if type == .purchaseSeparately, let primaryDomainAddress = blog?.primaryDomainAddress { + return String(format: Strings.directPurchasePrompt, primaryDomainAddress) + } else { + return Strings.prompt + } + } } } // MARK: - Sorting -private extension WebAddressWizardContent { +private extension DomainSelectionViewController { // Mimics the sorting on the web - two top domains, one free domain, and other domains private func sortFreeAndPaidSuggestions(_ suggestions: [DomainSuggestion]) -> [DomainSuggestion] { var topDomains: [DomainSuggestion] = [] @@ -556,7 +619,7 @@ private extension WebAddressWizardContent { // MARK: - NetworkStatusDelegate -extension WebAddressWizardContent: NetworkStatusDelegate { +extension DomainSelectionViewController: NetworkStatusDelegate { func networkStatusDidChange(active: Bool) { isNetworkActive = active } @@ -564,7 +627,7 @@ extension WebAddressWizardContent: NetworkStatusDelegate { // MARK: - UITextFieldDelegate -extension WebAddressWizardContent: UITextFieldDelegate { +extension DomainSelectionViewController: UITextFieldDelegate { func textFieldShouldClear(_ textField: UITextField) -> Bool { return true } @@ -577,7 +640,7 @@ extension WebAddressWizardContent: UITextFieldDelegate { // MARK: - UISearchBarDelegate -extension WebAddressWizardContent: UISearchBarDelegate { +extension DomainSelectionViewController: UISearchBarDelegate { func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) { clearSelectionAndCreateSiteButton() } @@ -589,7 +652,7 @@ extension WebAddressWizardContent: UISearchBarDelegate { // MARK: - VoiceOver -private extension WebAddressWizardContent { +private extension DomainSelectionViewController { func postScreenChangedForVoiceOver() { UIAccessibility.post(notification: .screenChanged, argument: table.tableHeaderView) } @@ -606,43 +669,41 @@ private extension WebAddressWizardContent { } // MARK: UITableViewDataSource -extension WebAddressWizardContent: UITableViewDataSource { +extension DomainSelectionViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { guard !isShowingError else { return 1 } - return (!domainPurchasingEnabled && !hasExactMatch && section == 0) ? 1 : data.count + return data.count } func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { guard data.count > 0 else { return nil } - return (!domainPurchasingEnabled && !hasExactMatch && section == 0) ? nil : Strings.suggestions + return Strings.suggestions } func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - return (!domainPurchasingEnabled && !hasExactMatch && indexPath.section == 0) ? 60 : UITableView.automaticDimension + return UITableView.automaticDimension } func numberOfSections(in tableView: UITableView) -> Int { - return (domainPurchasingEnabled || hasExactMatch) ? 1 : 2 + return 1 } func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - return (!domainPurchasingEnabled && !hasExactMatch && section == 0) ? UIView(frame: CGRect(x: 0, y: 0, width: tableView.frame.width, height: 3)) : nil + return nil } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { if isShowingError { return configureErrorCell(tableView, cellForRowAt: indexPath) - } else if !domainPurchasingEnabled && !hasExactMatch && indexPath.section == 0 { - return configureNoMatchCell(table, cellForRowAt: indexPath) } else { return configureAddressCell(tableView, cellForRowAt: indexPath) } } func configureNoMatchCell(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: WebAddressWizardContent.noMatchCellReuseIdentifier) ?? { + let cell = tableView.dequeueReusableCell(withIdentifier: DomainSelectionViewController.noMatchCellReuseIdentifier) ?? { // Create and configure a new TableView cell if one hasn't been queued yet - let newCell = UITableViewCell(style: .subtitle, reuseIdentifier: WebAddressWizardContent.noMatchCellReuseIdentifier) + let newCell = UITableViewCell(style: .subtitle, reuseIdentifier: DomainSelectionViewController.noMatchCellReuseIdentifier) newCell.detailTextLabel?.text = Strings.noMatch newCell.detailTextLabel?.font = WPStyleGuide.fontForTextStyle(.body, fontWeight: .regular) newCell.detailTextLabel?.textColor = .textSubtle @@ -661,15 +722,9 @@ extension WebAddressWizardContent: UITableViewDataSource { } let domainSuggestion = data[indexPath.row] - if domainPurchasingEnabled { - let tags = AddressTableViewCell.ViewModel.tagsFromPosition(indexPath.row) - let viewModel = AddressTableViewCell.ViewModel(model: domainSuggestion, tags: tags) - cell.update(with: viewModel) - } else { - cell.update(with: domainSuggestion) - cell.addBorder(isFirstCell: (indexPath.row == 0), isLastCell: (indexPath.row == data.count - 1)) - cell.isSelected = domainSuggestion.domainName == selectedDomain?.domainName - } + let tags = AddressTableViewCell.ViewModel.tagsFromPosition(indexPath.row) + let viewModel = AddressTableViewCell.ViewModel(model: domainSuggestion, type: domainSelectionType, tags: tags) + cell.update(with: viewModel) return cell } @@ -686,11 +741,11 @@ extension WebAddressWizardContent: UITableViewDataSource { } // MARK: UITableViewDelegate -extension WebAddressWizardContent: UITableViewDelegate { +extension DomainSelectionViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { // Prevent selection if it's the no matches cell - return (!domainPurchasingEnabled && !hasExactMatch && indexPath.section == 0) ? nil : indexPath + return indexPath } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { @@ -702,11 +757,7 @@ extension WebAddressWizardContent: UITableViewDelegate { let domainSuggestion = data[indexPath.row] self.selectedDomain = domainSuggestion - if domainPurchasingEnabled { - searchBar.resignFirstResponder() - } else { - searchTextField.resignFirstResponder() - } + searchBar.resignFirstResponder() } func retry() { @@ -714,3 +765,200 @@ extension WebAddressWizardContent: UITableViewDelegate { performSearchIfNeeded(query: retryQuery) } } + +// MARK: - Transfer Footer Setup + +private extension DomainSelectionViewController { + func setupTransferFooterView() { + guard domainSelectionType == .purchaseFromDomainManagement else { + return + } + self.view.addSubview(transferFooterView) + self.transferFooterView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate(transferFooterViewConstraints.visible) + } + + /// Updates transfer footer view constraints to either hide or show the view. + private func updateTransferFooterViewConstraints(hidden: Bool, animated: Bool = true) { + guard transferFooterView.superview != nil else { + return + } + + let constraints = transferFooterViewConstraints + let duration = animated ? WPAnimationDurationDefault : 0 + + NSLayoutConstraint.deactivate(hidden ? constraints.visible : constraints.hidden) + NSLayoutConstraint.activate(hidden ? constraints.hidden : constraints.visible) + + if !hidden { + self.transferFooterView.isHidden = false + } + UIView.animate(withDuration: duration) { + self.view.layoutIfNeeded() + } completion: { _ in + self.transferFooterView.isHidden = hidden + self.view.setNeedsLayout() + } + } + + private func showTransferFooterView(animated: Bool = true) { + self.updateTransferFooterViewConstraints(hidden: false, animated: animated) + } + + private func hideTransferFooterView(animated: Bool = true) { + self.updateTransferFooterViewConstraints(hidden: true, animated: animated) + } +} + +// MARK: - Support + +private extension DomainSelectionViewController { + func includeSupportButtonIfNeeded() { + guard includeSupportButton else { return } + + let supportButton = UIBarButtonItem(title: Strings.supportButtonTitle, + style: .plain, + target: self, + action: #selector(handleSupportButtonTapped)) + navigationItem.rightBarButtonItem = supportButton + } + + @objc func handleSupportButtonTapped(sender: UIBarButtonItem) { + let supportVC = SupportTableViewController() + let navigationController = UINavigationController(rootViewController: supportVC) + topmostPresentedViewController.show(navigationController, sender: nil) + } +} + +// MARK: - Routing + +private extension DomainSelectionViewController { + private func pushRegisterDomainDetailsViewController() { + guard let siteID = coordinator?.site?.dotComID?.intValue else { + DDLogError("Cannot register domains for sites without a dotComID") + return + } + + guard let domain = coordinator?.domain else { + return + } + + let controller = RegisterDomainDetailsViewController() + controller.viewModel = RegisterDomainDetailsViewModel(siteID: siteID, domain: domain) { [weak self] name in + guard let self = self, let coordinator else { + return + } + coordinator.domainPurchasedCallback?(self, name) + coordinator.trackDomainPurchasingCompleted() + } + self.navigationController?.pushViewController(controller, animated: true) + } + + private func pushPurchaseDomainChoiceScreen() { + @ObservedObject var choicesViewModel = DomainPurchaseChoicesViewModel() + let view = DomainPurchaseChoicesView( + viewModel: choicesViewModel, + analyticsSource: coordinator?.analyticsSource, + buyDomainAction: { [weak self] in + guard let self else { return } + choicesViewModel.isGetDomainLoading = true + self.coordinator?.handleNoSiteChoice(on: self, choicesViewModel: choicesViewModel) + WPAnalytics.track(.purchaseDomainGetDomainTapped) + }, chooseSiteAction: { [weak self] in + guard let self else { return } + self.coordinator?.handleExistingSiteChoice(on: self) + WPAnalytics.track(.purchaseDomainChooseSiteTapped) + } + ) + let hostingController = UIHostingController(rootView: view) + hostingController.title = Strings.domainChoiceTitle + self.navigationController?.pushViewController(hostingController, animated: true) + } +} + +// MARK: - Back Button + +private extension DomainSelectionViewController { + func setupBackButton() { + if navigationController?.children.count == 1 { + navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, + target: self, + action: #selector(handleCancelButtonTapped)) + } else { + navigationItem.backButtonTitle = Strings.backButtonTitle + } + } + + @objc func handleCancelButtonTapped(sender: UIBarButtonItem) { + dismiss(animated: true) + } +} + +// MARK: - Tracks + +private extension DomainSelectionViewController { + func trackViewDidLoad() { + switch domainSelectionType { + case .siteCreation: + WPAnalytics.track(.enhancedSiteCreationDomainsAccessed) + default: + track(.domainsDashboardDomainsSearchShown) + } + } + + func trackDomainSelected() { + switch domainSelectionType { + case .siteCreation: + break + default: + WPAnalytics.track(.automatedTransferCustomDomainSuggestionSelected) + } + } + + func trackSearchStarted() { + switch domainSelectionType { + case .siteCreation: + break + default: + WPAnalytics.track(.automatedTransferCustomDomainSuggestionQueried) + } + } + + private func trackDomainsSelection(_ domainSuggestion: DomainSuggestion) { + switch domainSelectionType { + case .siteCreation: + var domainSuggestionProperties: [String: Any] = [ + "chosen_domain": domainSuggestion.domainName as AnyObject, + "search_term": lastSearchQuery as AnyObject, + "is_free": domainSuggestion.isFree.stringLiteral + ] + + domainSuggestionProperties["domain_cost"] = domainSuggestion.costString + + WPAnalytics.track(.enhancedSiteCreationDomainsSelected, withProperties: domainSuggestionProperties) + default: + let properties: [AnyHashable: Any] = ["domain_name": domainSuggestion.domainName] + self.track(.domainsSearchSelectDomainTapped, properties: properties, blog: coordinator?.site) + } + } + + func track(_ event: WPAnalyticsEvent, properties: [AnyHashable: Any] = [:], blog: Blog? = nil) { + let defaultProperties = { () -> [AnyHashable: Any] in + if let blog { + return WPAnalytics.domainsProperties(for: blog, origin: self.coordinator?.analyticsSource) + } else { + return WPAnalytics.domainsProperties(origin: self.coordinator?.analyticsSource) + } + }() + + let properties = properties.merging(defaultProperties) { current, _ in + return current + } + + if let blog { + WPAnalytics.track(event, properties: properties, blog: blog) + } else { + WPAnalytics.track(event, properties: properties) + } + } +} diff --git a/WordPress/Classes/ViewRelated/Domains/Transfer Domains/TransferDomainsWebViewController.swift b/WordPress/Classes/ViewRelated/Domains/Transfer Domains/TransferDomainsWebViewController.swift new file mode 100644 index 000000000000..05bc72e988b6 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Domains/Transfer Domains/TransferDomainsWebViewController.swift @@ -0,0 +1,19 @@ +import Foundation + +final class TransferDomainsWebViewController: WebKitViewController { + + private enum Constants { + static let url = URL(string: "https://wordpress.com/setup/domain-transfer/intro")! + } + + init(source: String? = nil) { + let configuration = WebViewControllerConfiguration(url: Constants.url) + configuration.analyticsSource = source + configuration.authenticateWithDefaultAccount() + super.init(configuration: configuration) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/WordPress/Classes/ViewRelated/Domains/Utility/Blog+DomainsDashboardView.swift b/WordPress/Classes/ViewRelated/Domains/Utility/Blog+DomainsDashboardView.swift index bcf1f9d29d65..2ba9b6958f15 100644 --- a/WordPress/Classes/ViewRelated/Domains/Utility/Blog+DomainsDashboardView.swift +++ b/WordPress/Classes/ViewRelated/Domains/Utility/Blog+DomainsDashboardView.swift @@ -44,4 +44,16 @@ extension Blog { var freeDomainIsPrimary: Bool { freeDomain?.isPrimaryDomain ?? false } + + var primaryDomain: Domain? { + guard let domainsSet = domains as? Set, + let freeDomain = (domainsSet.first { $0.isPrimary == true }) else { + return nil + } + return Domain(managedDomain: freeDomain) + } + + var primaryDomainAddress: String { + primaryDomain?.domainName ?? "" + } } diff --git a/WordPress/Classes/ViewRelated/Domains/Utility/DomainExpiryDateFormatter.swift b/WordPress/Classes/ViewRelated/Domains/Utility/DomainExpiryDateFormatter.swift deleted file mode 100644 index 26f3967d0a26..000000000000 --- a/WordPress/Classes/ViewRelated/Domains/Utility/DomainExpiryDateFormatter.swift +++ /dev/null @@ -1,25 +0,0 @@ -import Foundation - -struct DomainExpiryDateFormatter { - static func expiryDate(for domain: Domain) -> String { - if domain.expiryDate.isEmpty { - return Localized.neverExpires - } else if domain.expired { - return Localized.expired - } else if domain.autoRenewing && domain.autoRenewalDate.isEmpty { - return Localized.autoRenews - } else if domain.autoRenewing { - return String(format: Localized.renewsOn, domain.autoRenewalDate) - } else { - return String(format: Localized.expiresOn, domain.expiryDate) - } - } - - enum Localized { - static let neverExpires = NSLocalizedString("Never expires", comment: "Label indicating that a domain name registration has no expiry date.") - static let autoRenews = NSLocalizedString("Auto-renew enabled", comment: "Label indicating that a domain name registration will automatically renew") - static let renewsOn = NSLocalizedString("Renews on %@", comment: "Label indicating the date on which a domain name registration will be renewed. The %@ placeholder will be replaced with a date at runtime.") - static let expiresOn = NSLocalizedString("Expires on %@", comment: "Label indicating the date on which a domain name registration will expire. The %@ placeholder will be replaced with a date at runtime.") - static let expired = NSLocalizedString("Expired", comment: "Label indicating that a domain name registration has expired.") - } -} diff --git a/WordPress/Classes/ViewRelated/Domains/View Models/DomainsStateViewModel.swift b/WordPress/Classes/ViewRelated/Domains/View Models/DomainsStateViewModel.swift new file mode 100644 index 000000000000..25f6120f36d0 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Domains/View Models/DomainsStateViewModel.swift @@ -0,0 +1,64 @@ +import Foundation + +struct DomainsStateViewModel { + let title: String + let description: String + let button: Button? + + struct Button { + let title: String + let action: () -> Void + } +} + +extension DomainsStateViewModel { + static func errorMessageViewModel(from error: Error, action: @escaping () -> Void) -> DomainsStateViewModel { + let title: String + let description: String + let button: DomainsStateViewModel.Button = .init(title: Strings.errorStateButtonTitle) { + action() + } + + let nsError = error as NSError + if nsError.domain == NSURLErrorDomain, nsError.code == NSURLErrorNotConnectedToInternet { + title = Strings.offlineEmptyStateTitle + description = Strings.offlineEmptyStateDescription + } else { + title = Strings.errorEmptyStateTitle + description = Strings.errorEmptyStateDescription + } + + return .init(title: title, description: description, button: button) + } +} + +extension DomainsStateViewModel { + enum Strings { + static let offlineEmptyStateTitle = NSLocalizedString( + "domain.management.offline.empty.state.title", + value: "No Internet Connection", + comment: "The empty state title in All Domains screen when the user is offline" + ) + static let offlineEmptyStateDescription = NSLocalizedString( + "domain.management.offline.empty.state.description", + value: "Please check your network connection and try again.", + comment: "The empty state description in All Domains screen when the user is offline" + ) + static let errorEmptyStateTitle = NSLocalizedString( + "domain.management.error.empty.state.title", + value: "Something went wrong", + comment: "The empty state title in All Domains screen when an error occurs" + ) + + static let errorEmptyStateDescription = NSLocalizedString( + "domain.management.error.empty.state.description", + value: "We encountered an error while loading your domains. Please contact support if the issue persists.", + comment: "The empty state description in All Domains screen when an error occurs" + ) + static let errorStateButtonTitle = NSLocalizedString( + "domain.management.error.state.button.title", + value: "Try again", + comment: "The empty state button title in All Domains screen when an error occurs" + ) + } +} diff --git a/WordPress/Classes/ViewRelated/Domains/View Models/SiteDomainsViewModel.swift b/WordPress/Classes/ViewRelated/Domains/View Models/SiteDomainsViewModel.swift new file mode 100644 index 000000000000..ae3a3511d122 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Domains/View Models/SiteDomainsViewModel.swift @@ -0,0 +1,184 @@ +import Foundation +import WordPressKit +import Combine + +final class SiteDomainsViewModel: ObservableObject { + private let blog: Blog + private let domainsService: DomainsServiceAllDomainsFetching? + + @Published + private(set) var state: State = .loading + private(set) var loadedDomains: [DomainsService.AllDomainsListItem] = [] + + init(blog: Blog, domainsService: DomainsServiceAllDomainsFetching?) { + self.blog = blog + self.domainsService = domainsService + } + + func refresh() { + domainsService?.fetchAllDomains(resolveStatus: true, noWPCOM: false, completion: { [weak self] result in + guard let self else { + return + } + switch result { + case .success(let domains): + self.loadedDomains = domains + let sections = Self.buildSections(from: blog, domains: domains) + self.state = .normal(sections) + case .failure(let error): + self.state = .message(self.errorMessageViewModel(from: error)) + } + }) + } + + private func errorMessageViewModel(from error: Error) -> DomainsStateViewModel { + return DomainsStateViewModel.errorMessageViewModel(from: error) { [weak self] in + self?.state = .loading + self?.refresh() + } + } + + // MARK: - Sections + + private static func buildSections(from blog: Blog, domains: [DomainsService.AllDomainsListItem]) -> [Section] { + let wpcomDomains = domains.filter { $0.wpcomDomain } + let otherDomains = domains.filter { !$0.wpcomDomain } + + return Self.buildFreeDomainSections(from: blog, wpComDomains: wpcomDomains) + Self.buildDomainsSections(from: blog, domains: otherDomains) + } + + private static func buildFreeDomainSections(from blog: Blog, wpComDomains: [DomainsService.AllDomainsListItem]) -> [Section] { + let blogWpComDomains = wpComDomains.filter { $0.blogId == blog.dotComID?.intValue } + guard let freeDomain = blogWpComDomains.count > 1 ? blogWpComDomains.first(where: { $0.isWpcomStagingDomain }) : blogWpComDomains.first else { + return [] + } + + return [ + Section( + title: Strings.freeDomainSectionTitle, + footer: blog.freeDomainIsPrimary ? Strings.primaryDomainDescription : nil, + content: .rows([.init( + viewModel: .init( + name: freeDomain.domain, + description: nil, + status: nil, + expiryDate: AllDomainsListItemViewModel.expiryDate(from: freeDomain), + isPrimary: blog.freeDomainIsPrimary + ), + navigation: nil)]) + ) + ] + } + + private static func buildDomainsSections(from blog: Blog, domains: [DomainsService.AllDomainsListItem]) -> [Section] { + var sections: [Section] = [] + + let primaryDomainName = blog.domainsList.first(where: { $0.domain.isPrimaryDomain })?.domain.domainName + let blogDomains = domains.filter({ $0.blogId == blog.dotComID?.intValue }) + let primaryDomain = blogDomains.first(where: { primaryDomainName == $0.domain }) + let otherDomains = blogDomains.filter({ primaryDomainName != $0.domain }) + + if let primaryDomain { + let section = Section( + title: String(format: Strings.domainsListSectionTitle, blog.title ?? blog.freeSiteAddress), + footer: Strings.primaryDomainDescription, + content: .rows([.init( + viewModel: .init( + name: primaryDomain.domain, + description: nil, + status: primaryDomain.status, + expiryDate: AllDomainsListItemViewModel.expiryDate(from: primaryDomain), + isPrimary: true + ), + navigation: navigation(from: primaryDomain) + )]) + ) + sections.append(section) + } + + if otherDomains.count > 0 { + let domainRows = otherDomains.map { + SiteDomainsViewModel.Section.Row( + viewModel: .init( + name: $0.domain, + description: nil, + status: $0.status, + expiryDate: AllDomainsListItemViewModel.expiryDate(from: $0), + isPrimary: false + ), + navigation: navigation(from: $0) + ) + } + + let section = Section( + title: primaryDomain == nil ? String(format: Strings.domainsListSectionTitle, blog.title ?? blog.freeSiteAddress) : nil, + footer: nil, + content: .rows(domainRows) + ) + + sections.append(section) + } + + if sections.count == 0 || blog.canRegisterDomainWithPaidPlan { + sections.append(Section(title: nil, footer: nil, content: .upgradePlan)) + } else { + sections.append(Section(title: nil, footer: nil, content: .addDomain)) + } + + return sections + } + + private static func navigation(from domain: DomainsService.AllDomainsListItem) -> SiteDomainsViewModel.Section.Row.Navigation { + return .init(domain: domain.domain, siteSlug: domain.siteSlug, type: domain.type) + } +} + +extension SiteDomainsViewModel { + enum Strings { + static let freeDomainSectionTitle = NSLocalizedString("site.domains.freeDomainSection.title", + value: "Your Free WordPress.com domain", + comment: "A section title which displays a row with a free WP.com domain") + static let primaryDomainDescription = NSLocalizedString("site.domains.primaryDomain", + value: "Your primary site address is what visitors will see in their address bar when visiting your website.", + comment: "Footer of the primary site section in the Domains Dashboard.") + static let domainsListSectionTitle: String = NSLocalizedString("site.domains.domainSection.title", + value: "Other domains for %1$@", + comment: "Header of the secondary domains list section in the Domains Dashboard. %1$@ is the name of the site.") + } +} + +// MARK: - Types + +extension SiteDomainsViewModel { + enum State { + case normal([Section]) + case loading + case message(DomainsStateViewModel) + } + + struct Section: Identifiable { + enum SectionKind { + case rows([Row]) + case addDomain + case upgradePlan + } + + struct Row: Identifiable { + struct Navigation: Hashable { + let domain: String + let siteSlug: String + let type: DomainType + let analyticsSource: String = "site_domains" + } + + let id = UUID() + let viewModel: AllDomainsListCardView.ViewModel + let navigation: Navigation? + } + + let id = UUID() + let title: String? + let footer: String? + let content: SectionKind + } +} diff --git a/WordPress/Classes/ViewRelated/Domains/Views/DomainDetailsWebViewControllerWrapper.swift b/WordPress/Classes/ViewRelated/Domains/Views/DomainDetailsWebViewControllerWrapper.swift new file mode 100644 index 000000000000..e65154a7cbd2 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Domains/Views/DomainDetailsWebViewControllerWrapper.swift @@ -0,0 +1,29 @@ +import SwiftUI +import UIKit +import WordPressKit + +/// Makes DomainDetailsWebViewController available to SwiftUI +struct DomainDetailsWebViewControllerWrapper: UIViewControllerRepresentable { + private let domain: String + private let siteSlug: String + private let type: DomainType + private let analyticsSource: String? + + init(domain: String, siteSlug: String, type: DomainType, analyticsSource: String? = nil) { + self.domain = domain + self.siteSlug = siteSlug + self.type = type + self.analyticsSource = analyticsSource + } + + func makeUIViewController(context: Context) -> DomainDetailsWebViewController { + DomainDetailsWebViewController( + domain: domain, + siteSlug: siteSlug, + type: type, + analyticsSource: analyticsSource + ) + } + + func updateUIViewController(_ uiViewController: DomainDetailsWebViewController, context: Context) { } +} diff --git a/WordPress/Classes/ViewRelated/Domains/Views/DomainResultView.swift b/WordPress/Classes/ViewRelated/Domains/Views/DomainResultView.swift index 5b41bf082f70..c7687ce5eabd 100644 --- a/WordPress/Classes/ViewRelated/Domains/Views/DomainResultView.swift +++ b/WordPress/Classes/ViewRelated/Domains/Views/DomainResultView.swift @@ -55,22 +55,7 @@ struct DomainResultView: View { Spacer().frame(height: Metrics.subtitleToNoticeBoxSpacing) - HStack() { - Image(uiImage: .gridicon(.infoOutline)) - .frame(height: Metrics.iconHeight) - .foregroundColor(Color(UIColor.muriel(color: .gray))) - .accessibility(hidden: true) - - Spacer().frame(width: Metrics.iconToNoticeSpacing) - - Text(TextContent.notice) - .font(.footnote) - .foregroundColor(Color(UIColor.muriel(color: .textSubtle))) - } - .padding(Metrics.noticeBoxPadding) - .frame(maxWidth: .infinity) - .background(Color(UIColor.tertiaryFill)) - .cornerRadius(Metrics.noticeBoxCornerRadius) + DomainSetupNoticeView(noticeText: TextContent.notice) } .frame(width: geometry.size.width) .frame(minHeight: geometry.size.height) @@ -119,12 +104,7 @@ private extension DomainResultView { static let logoHeight = 65.0 static let logoToTitleSpacing = 33.0 static let titleToSubtitleSpacing = 16.0 - static let noticeBoxCornerRadius = 8.0 - static let noticeBoxPadding = EdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16) - static let iconHeight = 24.0 - static let iconToNoticeSpacing = 16.0 static let subtitleToNoticeBoxSpacing = 32.0 - static let primaryButtonCornerRadius = 7.0 } private enum Fonts { diff --git a/WordPress/Classes/ViewRelated/Domains/Views/DomainSetupNoticeView.swift b/WordPress/Classes/ViewRelated/Domains/Views/DomainSetupNoticeView.swift new file mode 100644 index 000000000000..b4de32dff312 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Domains/Views/DomainSetupNoticeView.swift @@ -0,0 +1,40 @@ +import SwiftUI +import Foundation + +struct DomainSetupNoticeView: View { + + let noticeText: String + + var body: some View { + + HStack() { + Image(uiImage: .gridicon(.infoOutline)) + .frame(height: Metrics.iconHeight) + .foregroundColor(Color(UIColor.muriel(color: .gray))) + .accessibility(hidden: true) + + Spacer().frame(width: Metrics.iconToNoticeSpacing) + + Text(noticeText) + .font(.footnote) + .dynamicTypeSize(.large) + .foregroundColor(Color(UIColor.muriel(color: .textSubtle))) + .fixedSize(horizontal: false, vertical: true) + } + .padding(Metrics.noticeBoxPadding) + .frame(maxWidth: .infinity) + .background(Color(UIColor.tertiaryFill)) + .cornerRadius(Metrics.noticeBoxCornerRadius) + } +} + +// MARK: - Constants +private extension DomainSetupNoticeView { + + private enum Metrics { + static let noticeBoxCornerRadius = 8.0 + static let noticeBoxPadding = EdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16) + static let iconHeight = 24.0 + static let iconToNoticeSpacing = 16.0 + } +} diff --git a/WordPress/Classes/ViewRelated/Domains/Views/DomainsDashboardCoordinator.swift b/WordPress/Classes/ViewRelated/Domains/Views/DomainsDashboardCoordinator.swift index d31e97338055..bd9dcfd0aad9 100644 --- a/WordPress/Classes/ViewRelated/Domains/Views/DomainsDashboardCoordinator.swift +++ b/WordPress/Classes/ViewRelated/Domains/Views/DomainsDashboardCoordinator.swift @@ -19,6 +19,7 @@ import UIKit dashboardViewController.navigationController?.popViewController(animated: true) } controller.navigationItem.largeTitleDisplayMode = .never - dashboardViewController.show(controller, sender: nil) + let navigationController = UINavigationController(rootViewController: controller) + dashboardViewController.present(navigationController, animated: true) } } diff --git a/WordPress/Classes/ViewRelated/Domains/Views/DomainsDashboardFactory.swift b/WordPress/Classes/ViewRelated/Domains/Views/DomainsDashboardFactory.swift index b720d367e9c6..0e7d2b10d0ef 100644 --- a/WordPress/Classes/ViewRelated/Domains/Views/DomainsDashboardFactory.swift +++ b/WordPress/Classes/ViewRelated/Domains/Views/DomainsDashboardFactory.swift @@ -3,18 +3,21 @@ import SwiftUI struct DomainsDashboardFactory { static func makeDomainsDashboardViewController(blog: Blog) -> UIViewController { - let viewController = UIHostingController(rootView: DomainsDashboardView(blog: blog)) + let viewController = SiteDomainsViewController(blog: blog) viewController.extendedLayoutIncludesOpaqueBars = true return viewController } - static func makeDomainsSuggestionViewController(blog: Blog, domainSelectionType: DomainSelectionType, onDismiss: @escaping () -> Void) -> RegisterDomainSuggestionsViewController { - let viewController = RegisterDomainSuggestionsViewController.instance( - site: blog, + static func makeDomainsSuggestionViewController(blog: Blog, domainSelectionType: DomainSelectionType, onDismiss: @escaping () -> Void) -> DomainSelectionViewController { + let coordinator = RegisterDomainCoordinator(site: blog) + let viewController = DomainSelectionViewController( + service: DomainsServiceAdapter(coreDataStack: ContextManager.shared), domainSelectionType: domainSelectionType, - includeSupportButton: false) + includeSupportButton: false, + coordinator: coordinator + ) - viewController.domainPurchasedCallback = { viewController, domain in + coordinator.domainPurchasedCallback = { viewController, domain in let blogService = BlogService(coreDataStack: ContextManager.shared) blogService.syncBlogAndAllMetadata(blog) { } WPAnalytics.track(.domainCreditRedemptionSuccess) @@ -26,6 +29,16 @@ struct DomainsDashboardFactory { viewController.present(controller, animated: true) } + let domainAddedToCart = FreeToPaidPlansCoordinator.plansFlowAfterDomainAddedToCartBlock( + customTitle: nil, + analyticsSource: "site_domains" + ) { [weak coordinator] controller, domain in + coordinator?.domainPurchasedCallback?(controller, domain) + coordinator?.trackDomainPurchasingCompleted() + } + + coordinator.domainAddedToCartAndLinkedToSiteCallback = domainAddedToCart + return viewController } } diff --git a/WordPress/Classes/ViewRelated/Domains/Views/DomainsDashboardView.swift b/WordPress/Classes/ViewRelated/Domains/Views/DomainsDashboardView.swift deleted file mode 100644 index ca2e83a37d50..000000000000 --- a/WordPress/Classes/ViewRelated/Domains/Views/DomainsDashboardView.swift +++ /dev/null @@ -1,199 +0,0 @@ -import SwiftUI -import WordPressKit - -/// The Domains dashboard screen, accessible from My Site -struct DomainsDashboardView: View { - @ObservedObject var blog: Blog - @State var isShowingDomainRegistrationFlow = false - @State var blogService = BlogService(coreDataStack: ContextManager.shared) - @State var domainsList: [Blog.DomainRepresentation] = [] - - // Property observer - private func showingDomainRegistrationFlow(to value: Bool) { - if value { - WPAnalytics.track(.domainsDashboardAddDomainTapped, properties: WPAnalytics.domainsProperties(for: blog), blog: blog) - } - } - - var body: some View { - List { - if blog.supports(.domains) { - makeSiteAddressSection(blog: blog) - } - makeDomainsSection(blog: blog) - } - .listStyle(GroupedListStyle()) - .padding(.top, blog.supports(.domains) ? Metrics.topPadding : 0) - .buttonStyle(PlainButtonStyle()) - .onTapGesture(perform: { }) - .onAppear { - updateDomainsList() - - blogService.refreshDomains(for: blog, success: { - updateDomainsList() - }, failure: nil) - } - .navigationBarTitle(TextContent.navigationTitle) - .sheet(isPresented: $isShowingDomainRegistrationFlow, content: { - makeDomainSearch(for: blog, onDismiss: { - isShowingDomainRegistrationFlow = false - blogService.refreshDomains(for: blog, success: { - updateDomainsList() - }, failure: nil) - }) - }) - } - - @ViewBuilder - private func makeDomainsSection(blog: Blog) -> some View { - if blog.hasDomains { - makeDomainsListSection(blog: blog) - } else { - makeGetFirstDomainSection(blog: blog) - } - } - - /// Builds the site address section for the given blog - private func makeSiteAddressSection(blog: Blog) -> some View { - Section(footer: Text(TextContent.primarySiteSectionFooter(blog.hasPaidPlan))) { - VStack(alignment: .leading) { - Text(TextContent.siteAddressTitle) - Text(blog.freeSiteAddress) - .bold() - if blog.freeDomainIsPrimary { - ShapeWithTextView(title: TextContent.primaryAddressLabel) - .smallRoundedRectangle() - } - } - } - } - - @ViewBuilder - private func makeDomainCell(domain: Blog.DomainRepresentation) -> some View { - VStack(alignment: .leading) { - Text(domain.domain.domainName) - if domain.domain.isPrimaryDomain { - ShapeWithTextView(title: TextContent.primaryAddressLabel) - .smallRoundedRectangle() - } - makeExpiryRenewalLabel(domain: domain) - } - } - - /// Builds the domains list section with the` add a domain` button at the bottom, for the given blog - private func makeDomainsListSection(blog: Blog) -> some View { - Section(header: Text(TextContent.domainsListSectionHeader)) { - ForEach(domainsList) { - makeDomainCell(domain: $0) - } - if blog.supports(.domains) { - PresentationButton( - isShowingDestination: $isShowingDomainRegistrationFlow.onChange(showingDomainRegistrationFlow), - appearance: { - HStack { - Text(TextContent.additionalDomainTitle(blog.canRegisterDomainWithPaidPlan)) - .foregroundColor(Color(UIColor.primary)) - .bold() - Spacer() - } - } - ) - } - } - } - - /// Builds the Get New Domain section when no othert domains are present for the given blog - private func makeGetFirstDomainSection(blog: Blog) -> some View { - Section { - PresentationCard( - title: TextContent.firstDomainTitle(blog.canRegisterDomainWithPaidPlan), - description: TextContent.firstDomainDescription(blog.canRegisterDomainWithPaidPlan), - highlight: siteAddressForGetFirstDomainSection, - isShowingDestination: $isShowingDomainRegistrationFlow.onChange(showingDomainRegistrationFlow)) { - ShapeWithTextView(title: TextContent.firstSearchDomainButtonTitle) - .largeRoundedRectangle() - } - } - } - - private var siteAddressForGetFirstDomainSection: String { - blog.canRegisterDomainWithPaidPlan ? "" : blog.freeSiteAddress - } - - private func makeExpiryRenewalLabel(domain: Blog.DomainRepresentation) -> some View { - let stringForDomain = DomainExpiryDateFormatter.expiryDate(for: domain.domain) - - return Text(stringForDomain) - .font(.subheadline) - .foregroundColor(domain.domain.expirySoon || domain.domain.expired ? Color(UIColor.error) : Color(UIColor.textSubtle)) - } - - /// Instantiates the proper search depending if it's for claiming a free domain with a paid plan or purchasing a new one - private func makeDomainSearch(for blog: Blog, onDismiss: @escaping () -> Void) -> some View { - return DomainSuggestionViewControllerWrapper( - blog: blog, - domainSelectionType: blog.canRegisterDomainWithPaidPlan ? .registerWithPaidPlan : .purchaseSeparately, - onDismiss: onDismiss - ) - } - - private func updateDomainsList() { - domainsList = blog.domainsList - } -} - -// MARK: - Constants -private extension DomainsDashboardView { - - enum TextContent { - // Navigation bar - static let navigationTitle = NSLocalizedString("Site Domains", comment: "Title of the Domains Dashboard.") - // Site address section - static func primarySiteSectionFooter(_ paidPlan: Bool) -> String { - paidPlan ? "" : NSLocalizedString("Your primary site address is what visitors will see in their address bar when visiting your website.", - comment: "Footer of the primary site section in the Domains Dashboard.") - } - - static let siteAddressTitle = NSLocalizedString("Your free WordPress.com address is", - comment: "Title of the site address section in the Domains Dashboard.") - static let primaryAddressLabel = NSLocalizedString("Primary site address", - comment: "Primary site address label, used in the site address section of the Domains Dashboard.") - - // Domains section - static let domainsListSectionHeader: String = NSLocalizedString("Your Site Domains", - comment: "Header of the domains list section in the Domains Dashboard.") - static let paidPlanDomainSectionFooter: String = NSLocalizedString("All WordPress.com plans include a custom domain name. Register your free premium domain now.", - comment: "Footer of the free domain registration section for a paid plan.") - - static let additionalRedirectedDomainTitle: String = NSLocalizedString("Add a domain", - comment: "Label of the button that starts the purchase of an additional redirected domain in the Domains Dashboard.") - - static let firstRedirectedDomainTitle: String = NSLocalizedString("Get your domain", - comment: "Title of the card that starts the purchase of the first redirected domain in the Domains Dashboard.") - static let firstRedirectedDomainDescription = NSLocalizedString("Domains purchased on this site will redirect users to ", - comment: "Description for the first domain purchased with a free plan.") - static let firstPaidPlanRegistrationTitle: String = NSLocalizedString("Claim your free domain", - comment: "Title of the card that starts the registration of a free domain with a paid plan, in the Domains Dashboard.") - static let firstPaidPlanRegistrationDescription = NSLocalizedString("You have a free one-year domain registration with your plan", - comment: "Description for the first domain purchased with a paid plan.") - static let firstSearchDomainButtonTitle = NSLocalizedString("Search for a domain", - comment: "title of the button that searches the first domain.") - - static func firstDomainTitle(_ canRegisterDomainWithPaidPlan: Bool) -> String { - canRegisterDomainWithPaidPlan ? firstPaidPlanRegistrationTitle : firstRedirectedDomainTitle - } - - static func firstDomainDescription(_ canRegisterDomainWithPaidPlan: Bool) -> String { - canRegisterDomainWithPaidPlan ? firstPaidPlanRegistrationDescription : firstRedirectedDomainDescription - } - - static func additionalDomainTitle(_ canRegisterDomainWithPaidPlan: Bool) -> String { - canRegisterDomainWithPaidPlan ? firstPaidPlanRegistrationTitle : additionalRedirectedDomainTitle - } - } - - enum Metrics { - static let sectionPaddingDefaultHeight: CGFloat = 16.0 - static let topPadding: CGFloat = -34.0 - } -} diff --git a/WordPress/Classes/ViewRelated/Domains/Views/DomainsStateView.swift b/WordPress/Classes/ViewRelated/Domains/Views/DomainsStateView.swift new file mode 100644 index 000000000000..853a2ebc1ae7 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Domains/Views/DomainsStateView.swift @@ -0,0 +1,31 @@ +import Foundation +import SwiftUI +import DesignSystem + +struct DomainsStateView: View { + private let viewModel: DomainsStateViewModel + + init(viewModel: DomainsStateViewModel) { + self.viewModel = viewModel + } + + var body: some View { + VStack(spacing: Length.Padding.single) { + Text(viewModel.title) + .font(Font.DS.heading3) + .multilineTextAlignment(.center) + .foregroundStyle(Color.DS.Foreground.secondary) + Text(viewModel.description) + .font(Font.DS.Body.medium) + .foregroundStyle(Color.DS.Foreground.secondary) + .multilineTextAlignment(.center) + if let button = viewModel.button { + Spacer() + .frame(height: Length.Padding.single) + DSButton(title: button.title, style: .init(emphasis: .primary, size: .medium)) { + button.action() + } + } + } + } +} diff --git a/WordPress/Classes/ViewRelated/Domains/Views/PresentationButton.swift b/WordPress/Classes/ViewRelated/Domains/Views/PresentationButton.swift deleted file mode 100644 index f8fe99db7ca6..000000000000 --- a/WordPress/Classes/ViewRelated/Domains/Views/PresentationButton.swift +++ /dev/null @@ -1,14 +0,0 @@ -import SwiftUI - -struct PresentationButton: View { - @Binding var isShowingDestination: Bool - var appearance: () -> Appearance - - var body: some View { - Button(action: { - isShowingDestination = true - }) { - self.appearance() - } - } -} diff --git a/WordPress/Classes/ViewRelated/Domains/Views/PresentationCard.swift b/WordPress/Classes/ViewRelated/Domains/Views/PresentationCard.swift deleted file mode 100644 index 1b9aaf060825..000000000000 --- a/WordPress/Classes/ViewRelated/Domains/Views/PresentationCard.swift +++ /dev/null @@ -1,31 +0,0 @@ -import SwiftUI - -/// A card with a title, a description and a button that can present a view -struct PresentationCard: View { - var title: String - var description: String - var highlight: String - @Binding var isShowingDestination: Bool - - private let titleFontSize: CGFloat = 28 - - var appearance: () -> Appearance - - var body: some View { - VStack { - Text(title) - .font(Font.system(size: titleFontSize, - weight: .regular, - design: .serif)) - .padding() - (Text(description) + - Text(highlight).bold()) - .multilineTextAlignment(.center) - .fixedSize(horizontal: false, vertical: true) - PresentationButton( - isShowingDestination: $isShowingDestination, - appearance: appearance) - .padding() - } - } -} diff --git a/WordPress/Classes/ViewRelated/Domains/Views/PrimaryDomainView.swift b/WordPress/Classes/ViewRelated/Domains/Views/PrimaryDomainView.swift new file mode 100644 index 000000000000..3517e00cab36 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Domains/Views/PrimaryDomainView.swift @@ -0,0 +1,31 @@ +import SwiftUI +import DesignSystem + +struct PrimaryDomainView: View { + var body: some View { + Group { + HStack(spacing: Length.Padding.half) { + Image(systemName: "globe") + .font(.callout) + .foregroundStyle(Color.DS.Foreground.primary) + Text(Strings.primaryDomain) + .font(.callout) + .foregroundStyle(Color.DS.Foreground.primary) + } + .padding(.vertical, Length.Padding.half) + .padding(.horizontal, Length.Padding.single) + } + .background(Color.DS.Background.secondary) + .clipShape(RoundedRectangle(cornerRadius: Length.Radius.small)) + .accessibilityElement(children: .ignore) + .accessibilityLabel(Strings.primaryDomain) + } +} + +private extension PrimaryDomainView { + enum Strings { + static let primaryDomain = NSLocalizedString("site.domains.primaryDomain.title", + value: "Primary domain", + comment: "Primary domain label, used in the site address section of the Domains Dashboard.") + } +} diff --git a/WordPress/Classes/ViewRelated/Domains/Views/SiteDomainsPresentationCard.swift b/WordPress/Classes/ViewRelated/Domains/Views/SiteDomainsPresentationCard.swift new file mode 100644 index 000000000000..6e0fc2b36909 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Domains/Views/SiteDomainsPresentationCard.swift @@ -0,0 +1,52 @@ +import SwiftUI +import DesignSystem + +/// A card with a title, a description and buttons that return an action +struct SiteDomainsPresentationCard: View { + let title: String + let description: String + let destinations: [Destination] + + var body: some View { + VStack(spacing: Length.Padding.medium) { + VStack(spacing: Length.Padding.single) { + VStack(spacing: -Length.Padding.large) { + DashboardDomainsCardSearchView() + Text(title) + .style(.heading4) + .foregroundColor(.DS.Foreground.primary) + } + + Text(description) + .style(.bodyLarge(.regular)) + .foregroundColor(.DS.Foreground.secondary) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + .padding(.horizontal, Length.Padding.single) + } + + VStack(spacing: Length.Padding.single) { + ForEach(destinations) { destination in + DSButton( + title: destination.title, + style: .init( + emphasis: destination.style, + size: .large, + isJetpack: AppConfiguration.isJetpack + )) { + destination.action() + } + } + } + } + } +} + +extension SiteDomainsPresentationCard { + struct Destination: Identifiable { + let id: UUID = UUID() + let title: String + let style: DSButtonStyle.Emphasis + let action: (() -> Void) + } +} diff --git a/WordPress/Classes/ViewRelated/Domains/Views/SiteDomainsView.swift b/WordPress/Classes/ViewRelated/Domains/Views/SiteDomainsView.swift new file mode 100644 index 000000000000..0a71b3b840ef --- /dev/null +++ b/WordPress/Classes/ViewRelated/Domains/Views/SiteDomainsView.swift @@ -0,0 +1,274 @@ +import SwiftUI +import WordPressKit +import DesignSystem + +/// The Site Domains screen, accessible from My Site +struct SiteDomainsView: View { + + @ObservedObject var blog: Blog + @State var isShowingDomainSelectionWithType: DomainSelectionType? + @StateObject var viewModel: SiteDomainsViewModel + + // Property observer + private func showingDomainSelectionWithType(to value: DomainSelectionType?) { + switch value { + case .registerWithPaidPlan: + WPAnalytics.track(.domainsDashboardAddDomainTapped, properties: WPAnalytics.domainsProperties(for: blog), blog: blog) + case .purchaseSeparately: + WPAnalytics.track(.domainsDashboardGetDomainTapped, properties: WPAnalytics.domainsProperties(for: blog), blog: blog) + case .purchaseWithPaidPlan: + WPAnalytics.track(.domainsDashboardGetPlanTapped, properties: WPAnalytics.domainsProperties(for: blog), blog: blog) + case .none: + break + default: + break + } + } + + var body: some View { + ZStack { + Color.DS.Background.secondary.edgesIgnoringSafeArea(.all) + + switch viewModel.state { + case .normal(let sections): + List { + makeDomainsSections(blog: blog, sections: sections) + } + .listRowSeparator(.hidden) + //.listRowSpacing(Length.Padding.double) Re-enable when we update to Xcode 15 + case .message(let messageViewModel): + VStack { + HStack(alignment: .center) { + DomainsStateView(viewModel: messageViewModel) + .padding(.horizontal, Length.Padding.double) + } + } + case .loading: + ProgressView() + .progressViewStyle(.circular) + } + } + .onAppear { + viewModel.refresh() + } + .sheet(item: $isShowingDomainSelectionWithType, content: { domainSelectionType in + makeDomainSearch(for: blog, domainSelectionType: domainSelectionType, onDismiss: { + isShowingDomainSelectionWithType = nil + viewModel.refresh() + }) + .ignoresSafeArea() + }) + } + + // MARK: - Domains Section + + /// Builds the domains list section with the` add a domain` button at the bottom, for the given blog + @ViewBuilder + private func makeDomainsSections(blog: Blog, sections: [SiteDomainsViewModel.Section]) -> some View { + ForEach(sections, id: \.id) { section in + switch section.content { + case .rows(let rows): + makeDomainsListSection(blog: blog, section: section, rows: rows) + case .addDomain: + makeAddDomainSection(blog: blog) + case .upgradePlan: + makeGetFirstDomainSection(blog: blog) + } + } + } + + private func makeDomainsListSection(blog: Blog, section: SiteDomainsViewModel.Section, rows: [SiteDomainsViewModel.Section.Row]) -> some View { + Section { + ForEach(rows) { row in + if let navigation = row.navigation { + NavigationLink(destination: { + DomainDetailsWebViewControllerWrapper( + domain: navigation.domain, + siteSlug: navigation.siteSlug, + type: navigation.type, + analyticsSource: navigation.analyticsSource + ) + .navigationTitle(navigation.domain) + }, label: { + AllDomainsListCardView(viewModel: row.viewModel, padding: 0) + }) + } else { + AllDomainsListCardView(viewModel: row.viewModel, padding: 0) + } + } + } header: { + if let title = section.title { + Text(title) + } + } footer: { + if let footer = section.footer { + Text(footer) + } + } + } + + private func makeAddDomainSection(blog: Blog) -> some View { + let destination: DomainSelectionType = blog.canRegisterDomainWithPaidPlan ? .registerWithPaidPlan : .purchaseSeparately + + return Section { + Button { + $isShowingDomainSelectionWithType.onChange(showingDomainSelectionWithType).wrappedValue = destination + } label: { + Text(TextContent.additionalDomainTitle(blog.canRegisterDomainWithPaidPlan)) + .style(TextStyle.bodyMedium(.regular)) + .foregroundColor(Color.DS.Foreground.brand(isJetpack: AppConfiguration.isJetpack)) + } + } + } + + // MARK: - First Domain Section + + /// Builds the Get New Domain section when no othert domains are present for the given blog + private func makeGetFirstDomainSection(blog: Blog) -> some View { + Section { + SiteDomainsPresentationCard( + title: TextContent.firstDomainTitle(blog.canRegisterDomainWithPaidPlan), + description: TextContent.firstDomainDescription(blog.canRegisterDomainWithPaidPlan), + destinations: makeGetFirstDomainSectionDestinations(blog: blog) + ) + } + } + + private func makeGetFirstDomainSectionDestinations(blog: Blog) -> [SiteDomainsPresentationCard.Destination] { + let primaryDestination: DomainSelectionType = blog.canRegisterDomainWithPaidPlan ? .registerWithPaidPlan : .purchaseWithPaidPlan + var destinations: [SiteDomainsPresentationCard.Destination] = [ + .init( + title: TextContent.primaryButtonTitle(blog.canRegisterDomainWithPaidPlan), + style: .primary, + action: { + $isShowingDomainSelectionWithType.onChange(showingDomainSelectionWithType).wrappedValue = primaryDestination + } + ) + ] + + if !blog.canRegisterDomainWithPaidPlan { + destinations.append( + .init( + title: TextContent.firstDomainDirectPurchaseButtonTitle, + style: .tertiary, + action: { + $isShowingDomainSelectionWithType.onChange(showingDomainSelectionWithType).wrappedValue = .purchaseSeparately + } + ) + ) + } + + return destinations + } + + /// Instantiates the proper search depending if it's for claiming a free domain with a paid plan or purchasing a new one + private func makeDomainSearch(for blog: Blog, domainSelectionType: DomainSelectionType, onDismiss: @escaping () -> Void) -> some View { + return DomainSuggestionViewControllerWrapper( + blog: blog, + domainSelectionType: domainSelectionType, + onDismiss: onDismiss + ) + } +} + +// MARK: - Constants + +private extension SiteDomainsView { + + enum TextContent { + // Navigation bar + static let navigationTitle = NSLocalizedString("Site Domains", comment: "Title of the Domains Dashboard.") + + // Domains section + static let additionalRedirectedDomainTitle: String = NSLocalizedString("Add a domain", + comment: "Label of the button that starts the purchase of an additional redirected domain in the Domains Dashboard.") + static let firstFreeDomainWithPaidPlanDomainTitle: String = NSLocalizedString("site.domains.freeDomainWithPaidPlan.title", + value: "Get your domain", + comment: "Title of the card that starts the purchase of the first domain with a paid plan.") + static let firstFreeDomainWithPaidPlanDomainDescription = NSLocalizedString("site.domains.freeDomainWithPaidPlan.description", + value: "Get a free one-year domain registration or transfer with any annual paid plan.", + comment: "Description for the first domain purchased with a paid plan.") + static let firstDomainRegistrationTitle: String = NSLocalizedString("Claim your free domain", + comment: "Title of the card that starts the registration of a free domain with a paid plan, in the Domains Dashboard.") + static let firstDomainRegistrationDescription = NSLocalizedString("You have a free one-year domain registration with your plan.", + comment: "Description for the first domain purchased with a paid plan.") + static let firstDomainRegistrationButtonTitle = NSLocalizedString("Search for a domain", + comment: "title of the button that searches the first domain.") + + static let firstDomainDirectPurchaseButtonTitle: String = NSLocalizedString("site.domains.purchaseDirectly.buttons.title", + value: "Just search for a domain", + comment: "Title for a button that opens domain purchasing flow.") + static let firstDomainWithPaidPlanButtonTitle: String = NSLocalizedString("site.domains.purchaseWithPlan.buttons.title", + value: "Upgrade to a plan", + comment: "Title for a button that opens plan and domain purchasing flow.") + + static func firstDomainTitle(_ canRegisterDomainWithPaidPlan: Bool) -> String { + canRegisterDomainWithPaidPlan ? firstDomainRegistrationTitle : firstFreeDomainWithPaidPlanDomainTitle + } + + static func firstDomainDescription(_ canRegisterDomainWithPaidPlan: Bool) -> String { + canRegisterDomainWithPaidPlan ? firstDomainRegistrationDescription : firstFreeDomainWithPaidPlanDomainDescription + } + + static func additionalDomainTitle(_ canRegisterDomainWithPaidPlan: Bool) -> String { + canRegisterDomainWithPaidPlan ? firstDomainRegistrationTitle : additionalRedirectedDomainTitle + } + + static func primaryButtonTitle(_ canRegisterDomainWithPaidPlan: Bool) -> String { + canRegisterDomainWithPaidPlan ? firstDomainRegistrationButtonTitle : firstDomainWithPaidPlanButtonTitle + } + } + + struct Metrics { + static let insets = EdgeInsets(.init(top: Length.Padding.double, leading: Length.Padding.double, bottom: Length.Padding.double, trailing: Length.Padding.double)) + } +} + +final class SiteDomainsViewController: UIHostingController { + + // MARK: - Properties + + private let domainManagementFeatureFlag = RemoteFeatureFlag.domainManagement + private let viewModel: SiteDomainsViewModel + + // MARK: - Init + + init(blog: Blog) { + let account = try? WPAccount.lookupDefaultWordPressComAccount(in: ContextManager.shared.mainContext) + let domainsService = DomainsService(coreDataStack: ContextManager.shared, wordPressComRestApi: account?.wordPressComRestApi) + let viewModel = SiteDomainsViewModel(blog: blog, domainsService: domainsService) + self.viewModel = viewModel + super.init(rootView: .init(blog: blog, viewModel: viewModel)) + } + + @MainActor required dynamic init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - View Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + self.title = SiteDomainsView.TextContent.navigationTitle + self.setupAllDomainsBarButtonItem() + } + + // MARK: - Setup + + private func setupAllDomainsBarButtonItem() { +#if JETPACK + guard domainManagementFeatureFlag.enabled() else { + return + } + let title = AllDomainsListViewController.Strings.title + let action = UIAction { [weak self] _ in + guard let self else { return } + let domains = self.viewModel.loadedDomains.filter { !$0.wpcomDomain } + let allDomainsViewController = AllDomainsListViewController(viewModel: .init(domains: domains)) + self.navigationController?.pushViewController(allDomainsViewController, animated: true) + WPAnalytics.track(.domainsDashboardAllDomainsTapped) + } + self.navigationItem.rightBarButtonItem = .init(title: title, primaryAction: action) +#endif + } +} diff --git a/WordPress/Classes/ViewRelated/EEUUSCompliance/CompliancePopover.swift b/WordPress/Classes/ViewRelated/EEUUSCompliance/CompliancePopover.swift index 4513a4cb0469..29a320a086b7 100644 --- a/WordPress/Classes/ViewRelated/EEUUSCompliance/CompliancePopover.swift +++ b/WordPress/Classes/ViewRelated/EEUUSCompliance/CompliancePopover.swift @@ -1,10 +1,8 @@ import SwiftUI +import JetpackStatsWidgetsCore +import DesignSystem struct CompliancePopover: View { - private enum Constants { - static let verticalScrollBuffer = Length.Padding.large - } - @StateObject var viewModel: CompliancePopoverViewModel @@ -16,7 +14,7 @@ struct CompliancePopover: View { footnote buttonsHStack } - .padding(Length.Padding.small) + .padding(Length.Padding.medium) .fixedSize(horizontal: false, vertical: true) } @@ -34,7 +32,7 @@ struct CompliancePopover: View { private var analyticsToggle: some View { Toggle(Strings.toggleTitle, isOn: $viewModel.isAnalyticsEnabled) .foregroundColor(Color.DS.Foreground.primary) - .toggleStyle(SwitchToggleStyle(tint: Color.DS.Background.brand)) + .toggleStyle(SwitchToggleStyle(tint: Color.DS.Background.brand(isJetpack: AppConfiguration.isJetpack))) .padding(.vertical, Length.Padding.single) } @@ -48,7 +46,7 @@ struct CompliancePopover: View { HStack(spacing: Length.Padding.single) { settingsButton saveButton - }.padding(.top, Length.Padding.small) + }.padding(.top, Length.Padding.medium) } private var settingsButton: some View { @@ -57,13 +55,13 @@ struct CompliancePopover: View { }) { ZStack { RoundedRectangle(cornerRadius: Length.Padding.single) - .stroke(Color.DS.Border.divider, lineWidth: Length.Border.thin) + .stroke(Color.DS.divider, lineWidth: Length.Border.thin) Text(Strings.settingsButtonTitle) .font(.body) } } - .foregroundColor(Color.DS.Background.brand) - .frame(height: Length.Hitbox.minTapDimension) + .foregroundColor(Color.DS.Background.brand(isJetpack: AppConfiguration.isJetpack)) + .frame(height: Length.Hitbox.minTappableLength) } private var saveButton: some View { @@ -71,14 +69,14 @@ struct CompliancePopover: View { self.viewModel.didTapSave() }) { ZStack { - RoundedRectangle(cornerRadius: Length.Radius.minHeightButton) - .fill(Color.DS.Background.brand) + RoundedRectangle(cornerRadius: 8) + .fill(Color.DS.Background.brand(isJetpack: AppConfiguration.isJetpack)) Text(Strings.saveButtonTitle) .font(.body) } } .foregroundColor(.white) - .frame(height: Length.Hitbox.minTapDimension) + .frame(height: Length.Hitbox.minTappableLength) } } diff --git a/WordPress/Classes/ViewRelated/EEUUSCompliance/CompliancePopoverCoordinator.swift b/WordPress/Classes/ViewRelated/EEUUSCompliance/CompliancePopoverCoordinator.swift index 7738b29c9283..6d9fd7a47256 100644 --- a/WordPress/Classes/ViewRelated/EEUUSCompliance/CompliancePopoverCoordinator.swift +++ b/WordPress/Classes/ViewRelated/EEUUSCompliance/CompliancePopoverCoordinator.swift @@ -1,52 +1,87 @@ import UIKit protocol CompliancePopoverCoordinatorProtocol: AnyObject { - func presentIfNeeded(on viewController: UIViewController) + func presentIfNeeded() func navigateToSettings() func dismiss() } final class CompliancePopoverCoordinator: CompliancePopoverCoordinatorProtocol { - private weak var presentingViewController: UIViewController? + + // MARK: - Dependencies + private let complianceService = ComplianceLocationService() private let defaults: UserDefaults + // MARK: - Views + + private static var window: UIWindow? + + private let presentingViewController = UIViewController() + + // MARK: - Init + init(defaults: UserDefaults = UserDefaults.standard) { self.defaults = defaults } - func presentIfNeeded(on viewController: UIViewController) { + func presentIfNeeded() { guard FeatureFlag.compliancePopover.enabled, !defaults.didShowCompliancePopup else { return } complianceService.getIPCountryCode { [weak self] result in - if case .success(let countryCode) = result { - guard let self, self.shouldShowPrivacyBanner(countryCode: countryCode) else { - return - } - DispatchQueue.main.async { - self.presentPopover(on: viewController) - } + guard let self, case .success(let countryCode) = result, self.shouldShowPrivacyBanner(countryCode: countryCode) else { + return + } + DispatchQueue.main.async { + self.presentPopover() } } } func navigateToSettings() { - presentingViewController?.dismiss(animated: true) { + self.dismiss { RootViewCoordinator.sharedPresenter.navigateToPrivacySettings() } } func dismiss() { - presentingViewController?.dismiss(animated: true) + self.dismiss(completion: nil) } + // MARK: - Helpers + private func shouldShowPrivacyBanner(countryCode: String) -> Bool { let isCountryInEU = Self.gdprCountryCodes.contains(countryCode) return isCountryInEU && !defaults.didShowCompliancePopup } - private func presentPopover(on viewController: UIViewController) { + private func dismiss(completion: (() -> Void)? = nil) { + self.presentingViewController.dismiss(animated: true) { + self.removeWindow() + completion?() + } + } + + private func removeWindow() { + guard let window = Self.window else { + return + } + window.isHidden = true + window.resignKey() + Self.window = nil + } + + private func presentPopover() { + self.removeWindow() + + let window = UIWindow() + window.windowLevel = .alert + window.backgroundColor = .clear + window.rootViewController = presentingViewController + window.makeKeyAndVisible() + Self.window = window + let complianceViewModel = CompliancePopoverViewModel( defaults: defaults, contextManager: ContextManager.shared @@ -55,9 +90,7 @@ final class CompliancePopoverCoordinator: CompliancePopoverCoordinatorProtocol { let complianceViewController = CompliancePopoverViewController(viewModel: complianceViewModel) let bottomSheetViewController = BottomSheetViewController(childViewController: complianceViewController, customHeaderSpacing: 0) - bottomSheetViewController.show(from: viewController) - - self.presentingViewController = viewController + bottomSheetViewController.show(from: presentingViewController) } } diff --git a/WordPress/Classes/ViewRelated/Gutenberg/EditorMediaUtility.swift b/WordPress/Classes/ViewRelated/Gutenberg/EditorMediaUtility.swift index 965a0f2c2fc9..88a9c6c2efb3 100644 --- a/WordPress/Classes/ViewRelated/Gutenberg/EditorMediaUtility.swift +++ b/WordPress/Classes/ViewRelated/Gutenberg/EditorMediaUtility.swift @@ -240,7 +240,7 @@ class EditorMediaUtility { return } - let remote = MediaServiceRemoteFactory().remote(for: post.blog) + let remote = try? MediaServiceRemoteFactory().remote(for: post.blog) remote?.getMetadataFromVideoPressID(videoPressID, isSitePrivate: post.blog.isPrivate(), success: { metadata in completion(.success(metadata!)) }, failure: { error in diff --git a/WordPress/Classes/ViewRelated/Gutenberg/GutenbergMediaInserterHelper.swift b/WordPress/Classes/ViewRelated/Gutenberg/GutenbergMediaInserterHelper.swift index bb5d97751c42..3786c0011a14 100644 --- a/WordPress/Classes/ViewRelated/Gutenberg/GutenbergMediaInserterHelper.swift +++ b/WordPress/Classes/ViewRelated/Gutenberg/GutenbergMediaInserterHelper.swift @@ -1,6 +1,5 @@ import Foundation import CoreServices -import WPMediaPicker import Gutenberg import MediaEditor @@ -14,8 +13,6 @@ class GutenbergMediaInserterHelper: NSObject { /// fileprivate var mediaSelectionMethod: MediaSelectionMethod = .none - var didPickMediaCallback: GutenbergMediaPickerHelperCallback? - init(post: AbstractPost, gutenberg: Gutenberg) { self.post = post self.gutenberg = gutenberg @@ -39,70 +36,13 @@ class GutenbergMediaInserterHelper: NSObject { } func insertFromDevice(_ selection: [Any], callback: @escaping MediaPickerDidPickMediaCallback) { - if let assets = selection as? [PHAsset] { - insertFromDevice(assets: assets, callback: callback) - } else if let providers = selection as? [NSItemProvider] { + if let providers = selection as? [NSItemProvider] { insertItemProviders(providers, callback: callback) } else { callback(nil) } } - func insertFromDevice(assets: [PHAsset], callback: @escaping MediaPickerDidPickMediaCallback) { - guard (assets as [AsyncImage]).filter({ $0.isEdited }).isEmpty else { - insertFromMediaEditor(assets: assets, callback: callback) - return - } - - var mediaCollection: [MediaInfo] = [] - let group = DispatchGroup() - assets.forEach { asset in - group.enter() - insertFromDevice(asset: asset, callback: { media in - guard let media = media, - let selectedMedia = media.first else { - group.leave() - return - } - mediaCollection.append(selectedMedia) - group.leave() - }) - } - - group.notify(queue: .main) { - callback(mediaCollection) - } - } - - func insertFromDevice(asset: PHAsset, callback: @escaping MediaPickerDidPickMediaCallback) { - guard let media = insert(exportableAsset: asset, source: .deviceLibrary) else { - callback([]) - return - } - let options = PHImageRequestOptions() - options.deliveryMode = .fastFormat - options.version = .current - options.resizeMode = .fast - options.isNetworkAccessAllowed = true - let mediaUploadID = media.gutenbergUploadID - // Getting a quick thumbnail of the asset to display while the image is being exported and uploaded. - PHImageManager.default().requestImage(for: asset, targetSize: asset.pixelSize(), contentMode: .default, options: options) { (image, info) in - guard let thumbImage = image, let resizedImage = thumbImage.resizedImage(asset.pixelSize(), interpolationQuality: CGInterpolationQuality.low) else { - callback([MediaInfo(id: mediaUploadID, url: nil, type: media.mediaTypeString)]) - return - } - let filePath = NSTemporaryDirectory() + "\(mediaUploadID).jpg" - let url = URL(fileURLWithPath: filePath) - do { - try resizedImage.writeJPEGToURL(url) - callback([MediaInfo(id: mediaUploadID, url: url.absoluteString, type: media.mediaTypeString)]) - } catch { - callback([MediaInfo(id: mediaUploadID, url: nil, type: media.mediaTypeString)]) - return - } - } - } - private func insertItemProviders(_ providers: [NSItemProvider], callback: @escaping MediaPickerDidPickMediaCallback) { let media: [MediaInfo] = providers.compactMap { // WARNING: Media is a CoreData entity and has to be thread-confined @@ -146,39 +86,6 @@ class GutenbergMediaInserterHelper: NSObject { } } - func insertFromMediaEditor(assets: [AsyncImage], callback: @escaping MediaPickerDidPickMediaCallback) { - var mediaCollection: [MediaInfo] = [] - let group = DispatchGroup() - assets.forEach { asset in - group.enter() - if let image = asset.editedImage { - insertFromImage(image: image, callback: { media in - guard let media = media, - let selectedMedia = media.first else { - group.leave() - return - } - mediaCollection.append(selectedMedia) - group.leave() - }) - } else if let asset = asset as? PHAsset { - insertFromDevice(asset: asset, callback: { media in - guard let media = media, - let selectedMedia = media.first else { - group.leave() - return - } - mediaCollection.append(selectedMedia) - group.leave() - }) - } - } - - group.notify(queue: .main) { - callback(mediaCollection) - } - } - func syncUploads() { for media in post.media { if media.remoteStatus == .failed { @@ -214,6 +121,10 @@ class GutenbergMediaInserterHelper: NSObject { mediaCoordinator.retryMedia(media) } + func retryFailedMediaUploads() { + _ = mediaCoordinator.uploadMedia(for: post, automatedRetry: true) + } + func hasFailedMedia() -> Bool { return mediaCoordinator.hasFailedMedia(for: post) } diff --git a/WordPress/Classes/ViewRelated/Gutenberg/GutenbergMediaPickerHelper.swift b/WordPress/Classes/ViewRelated/Gutenberg/GutenbergMediaPickerHelper.swift index dce9c93a9325..dea27f2a5e9b 100644 --- a/WordPress/Classes/ViewRelated/Gutenberg/GutenbergMediaPickerHelper.swift +++ b/WordPress/Classes/ViewRelated/Gutenberg/GutenbergMediaPickerHelper.swift @@ -4,33 +4,16 @@ import UIKit import Photos import PhotosUI import WordPressShared -import WPMediaPicker import Gutenberg import UniformTypeIdentifiers public typealias GutenbergMediaPickerHelperCallback = ([Any]?) -> Void -class GutenbergMediaPickerHelper: NSObject { - - fileprivate struct Constants { - static let mediaPickerInsertText = NSLocalizedString( - "Insert %@", - comment: "Button title used in media picker to insert media (photos / videos) into a post. Placeholder will be the number of items that will be inserted." - ) - } - - fileprivate let post: AbstractPost - fileprivate unowned let context: UIViewController - fileprivate weak var navigationPicker: WPNavigationMediaPickerViewController? - fileprivate let noResultsView = NoResultsViewController.controller() +final class GutenbergMediaPickerHelper: NSObject { + private let post: AbstractPost + private unowned let context: UIViewController /// Media Library Data Source - /// - fileprivate lazy var mediaLibraryDataSource: MediaLibraryPickerDataSource = { - let dataSource = MediaLibraryPickerDataSource(post: self.post) - dataSource.ignoreSyncErrors = true - return dataSource - }() var didPickMediaCallback: GutenbergMediaPickerHelperCallback? @@ -39,22 +22,7 @@ class GutenbergMediaPickerHelper: NSObject { self.post = post } - func presentMediaPickerFullScreen(animated: Bool, - filter: WPMediaType, - dataSourceType: MediaPickerDataSourceType = .device, - allowMultipleSelection: Bool, - callback: @escaping GutenbergMediaPickerHelperCallback) { - switch dataSourceType { - case .device: - presentNativePicker(filter: filter, allowMultipleSelection: allowMultipleSelection, completion: callback) - case .mediaLibrary: - presentLegacyPicker(filter: filter, allowMultipleSelection: allowMultipleSelection, callback: callback) - @unknown default: - break - } - } - - private func presentNativePicker(filter: WPMediaType, allowMultipleSelection: Bool, completion: @escaping GutenbergMediaPickerHelperCallback) { + func presetDevicePhotosPicker(filter: WPMediaType, allowMultipleSelection: Bool, completion: @escaping GutenbergMediaPickerHelperCallback) { didPickMediaCallback = completion var configuration = PHPickerConfiguration() @@ -70,26 +38,10 @@ class GutenbergMediaPickerHelper: NSObject { context.present(picker, animated: true) } - private func presentLegacyPicker(filter: WPMediaType, - allowMultipleSelection: Bool, - callback: @escaping GutenbergMediaPickerHelperCallback) { - didPickMediaCallback = callback - - let mediaPickerOptions = WPMediaPickerOptions.withDefaults(filter: filter, allowMultipleSelection: allowMultipleSelection) - let picker = WPNavigationMediaPickerViewController(options: mediaPickerOptions) - navigationPicker = picker - - picker.startOnGroupSelector = false - picker.showGroupSelector = false - picker.dataSource = mediaLibraryDataSource - picker.selectionActionTitle = Constants.mediaPickerInsertText - picker.mediaPicker.options = mediaPickerOptions - picker.delegate = self - picker.mediaPicker.registerClass(forCustomHeaderView: DeviceMediaPermissionsHeader.self) - - picker.previewActionTitle = NSLocalizedString("Edit %@", comment: "Button that displays the media editor to the user") - picker.modalPresentationStyle = .currentContext - context.present(picker, animated: true) + func presentSiteMediaPicker(filter: WPMediaType, allowMultipleSelection: Bool, completion: @escaping GutenbergMediaPickerHelperCallback) { + didPickMediaCallback = completion + MediaPickerMenu(viewController: context, filter: .init(filter), isMultipleSelectionEnabled: allowMultipleSelection) + .showSiteMediaPicker(blog: post.blog, delegate: self) } func presentCameraCaptureFullScreen(animated: Bool, @@ -110,8 +62,10 @@ extension GutenbergMediaPickerHelper: ImagePickerControllerDelegate { switch mediaType { case UTType.image.identifier: if let image = info[.originalImage] as? UIImage { - self.didPickMediaCallback?([image]) - self.didPickMediaCallback = nil + MediaHelper.advertiseImageOptimization() { [self] in + self.didPickMediaCallback?([image]) + self.didPickMediaCallback = nil + } } case UTType.movie.identifier: @@ -131,90 +85,34 @@ extension GutenbergMediaPickerHelper: ImagePickerControllerDelegate { } } -// MARK: - User messages for video limits allowances -// extension GutenbergMediaPickerHelper: VideoLimitsAlertPresenter {} -// MARK: - Picker Delegate -// -extension GutenbergMediaPickerHelper: WPMediaPickerViewControllerDelegate { - - func mediaPickerController(_ picker: WPMediaPickerViewController, didFinishPicking assets: [WPMediaAsset]) { - invokeMediaPickerCallback(asset: assets) - picker.dismiss(animated: true, completion: nil) - } - - open func mediaPickerController(_ picker: WPMediaPickerViewController, handleError error: Error) -> Bool { - let presenter = context.topmostPresentedViewController - let alert = WPMediaPickerAlertHelper.buildAlertControllerWithError(error) - presenter.present(alert, animated: true) - return true - } - - func mediaPickerControllerDidCancel(_ picker: WPMediaPickerViewController) { - mediaLibraryDataSource.searchCancelled() - context.dismiss(animated: true, completion: { self.invokeMediaPickerCallback(asset: nil) }) - } - - fileprivate func invokeMediaPickerCallback(asset: [WPMediaAsset]?) { - didPickMediaCallback?(asset) +extension GutenbergMediaPickerHelper: SiteMediaPickerViewControllerDelegate { + func siteMediaPickerViewController(_ viewController: SiteMediaPickerViewController, didFinishWithSelection selection: [Media]) { + context.dismiss(animated: true) + didPickMediaCallback?(selection) didPickMediaCallback = nil } - - func emptyViewController(forMediaPickerController picker: WPMediaPickerViewController) -> UIViewController? { - guard picker == navigationPicker?.mediaPicker else { - return nil - } - return noResultsView - } - - func mediaPickerController(_ picker: WPMediaPickerViewController, didUpdateSearchWithAssetCount assetCount: Int) { - if (mediaLibraryDataSource.searchQuery?.count ?? 0) > 0 { - noResultsView.configureForNoSearchResult() - } else { - noResultsView.removeFromView() - } - } - - func mediaPickerControllerWillBeginLoadingData(_ picker: WPMediaPickerViewController) { - noResultsView.configureForFetching() - } - - func mediaPickerControllerDidEndLoadingData(_ picker: WPMediaPickerViewController) { - noResultsView.removeFromView() - noResultsView.configureForNoAssets(userCanUploadMedia: false) - } - } extension GutenbergMediaPickerHelper: PHPickerViewControllerDelegate { func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { context.dismiss(animated: true) - didPickMediaCallback?(results.map(\.itemProvider)) - didPickMediaCallback = nil - } -} + guard results.count > 0 else { + return + } -fileprivate extension WPMediaPickerOptions { - static func withDefaults( - showMostRecentFirst: Bool = true, - filter: WPMediaType = [.image], - allowCaptureOfMedia: Bool = false, - showSearchBar: Bool = true, - badgedUTTypes: Set = [UTType.gif.identifier], - allowMultipleSelection: Bool = false, - preferredStatusBarStyle: UIStatusBarStyle = WPStyleGuide.preferredStatusBarStyle - ) -> WPMediaPickerOptions { - let options = WPMediaPickerOptions() - options.showMostRecentFirst = showMostRecentFirst - options.filter = filter - options.allowCaptureOfMedia = allowCaptureOfMedia - options.showSearchBar = showSearchBar - options.badgedUTTypes = badgedUTTypes - options.allowMultipleSelection = allowMultipleSelection - options.preferredStatusBarStyle = preferredStatusBarStyle - - return options + let mediaFilter = picker.configuration.filter + if mediaFilter == PHPickerFilter(.all) || mediaFilter == PHPickerFilter(.image) { + MediaHelper.advertiseImageOptimization() { [self] in + didPickMediaCallback?(results.map(\.itemProvider)) + didPickMediaCallback = nil + } + } + else { + didPickMediaCallback?(results.map(\.itemProvider)) + didPickMediaCallback = nil + } } } diff --git a/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController+InformativeDialog.swift b/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController+InformativeDialog.swift deleted file mode 100644 index 804aac56f3bd..000000000000 --- a/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController+InformativeDialog.swift +++ /dev/null @@ -1,71 +0,0 @@ -import Foundation - -/// This extension handles Alert operations. -extension GutenbergViewController { - - enum InfoDialog { - static let postMessage = NSLocalizedString( - "You’re now using the block editor for new posts — great! If you’d like to change to the classic editor, go to ‘My Site’ > ‘Site Settings’.", - comment: "Popup content about why this post is being opened in block editor" - ) - static let pageMessage = NSLocalizedString( - "You’re now using the block editor for new pages — great! If you’d like to change to the classic editor, go to ‘My Site’ > ‘Site Settings’.", - comment: "Popup content about why this post is being opened in block editor" - ) - static let phase2Message = NSLocalizedString( - "We made big improvements to the block editor and think it's worth a try!\n\nWe enabled it for new posts and pages but if you'd like to change to the classic editor, go to 'My Site' > 'Site Settings'.", - comment: "Popup content about why this post is being opened in block editor" - ) - static let title = NSLocalizedString( - "Block editor enabled", - comment: "Popup title about why this post is being opened in block editor" - ) - static let okButtonTitle = NSLocalizedString("OK", comment: "OK button to close the informative dialog on Gutenberg editor") - } - - func showInformativeDialogIfNecessary() { - if shouldPresentInformativeDialog { - showMigrationInformativeDialog() - } else if shouldPresentPhase2informativeDialog { - showPhase2InformativeDialog() - } - } - - func showMigrationInformativeDialog() { - let message = post is Page ? InfoDialog.pageMessage : InfoDialog.postMessage - GutenbergViewController.showInformativeDialog(on: self, message: message) - } - - func showPhase2InformativeDialog() { - GutenbergViewController.showInformativeDialog(on: self, message: InfoDialog.phase2Message) - GutenbergSettings().setShowPhase2Dialog(false, for: post.blog) - } - - static func showInformativeDialog( - on viewController: UIViewControllerTransitioningDelegate & UIViewController, - message: String, - animated: Bool = true - ) { - let okButton: (title: String, handler: FancyAlertViewController.FancyAlertButtonHandler?) = - ( - title: InfoDialog.okButtonTitle, - handler: { (alert, button) in - alert.dismiss(animated: animated, completion: nil) - } - ) - - let config = FancyAlertViewController.Config( - titleText: InfoDialog.title, - bodyText: message, - headerImage: nil, - dividerPosition: .top, - defaultButton: okButton, - cancelButton: nil - ) - - let alert = FancyAlertViewController.controllerWithConfiguration(configuration: config) - alert.modalPresentationStyle = .custom - alert.transitioningDelegate = viewController - viewController.present(alert, animated: animated) - } -} diff --git a/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController+MoreActions.swift b/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController+MoreActions.swift index 232b734f5717..03098eca377a 100644 --- a/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController+MoreActions.swift +++ b/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController+MoreActions.swift @@ -6,8 +6,9 @@ import WordPressFlux /// navigation bar button of Gutenberg editor. extension GutenbergViewController { - private enum ErrorCode: Int { + enum ErrorCode: Int { case expectedSecondaryAction = 1 + case managedObjectContextMissing = 2 } func displayMoreSheet() { diff --git a/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController.swift b/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController.swift index 09c4eaa9adcd..ace88febc455 100644 --- a/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController.swift @@ -1,5 +1,4 @@ import UIKit -import WPMediaPicker import Gutenberg import Aztec import WordPressFlux @@ -17,15 +16,8 @@ class GutenbergViewController: UIViewController, PostEditor, FeaturedImageDelega case continueFromHomepageEditing } - private lazy var stockPhotos: GutenbergStockPhotos = { - return GutenbergStockPhotos(gutenberg: gutenberg, mediaInserter: mediaInserterHelper) - }() - private lazy var filesAppMediaPicker: GutenbergFilesAppMediaSource = { - return GutenbergFilesAppMediaSource(gutenberg: gutenberg, mediaInserter: mediaInserterHelper) - }() - private lazy var tenorMediaPicker: GutenbergTenorMediaPicker = { - return GutenbergTenorMediaPicker(gutenberg: gutenberg, mediaInserter: mediaInserterHelper) - }() + private lazy var filesAppMediaPicker = GutenbergFilesAppMediaSource(gutenberg: gutenberg, mediaInserter: mediaInserterHelper) + private lazy var externalMediaPicker = GutenbergExternalMediaPicker(gutenberg: gutenberg, mediaInserter: mediaInserterHelper) lazy var gutenbergSettings: GutenbergSettings = { return GutenbergSettings() @@ -207,9 +199,8 @@ class GutenbergViewController: UIViewController, PostEditor, FeaturedImageDelega mediaPickerHelper = GutenbergMediaPickerHelper(context: self, post: post) mediaInserterHelper = GutenbergMediaInserterHelper(post: post, gutenberg: gutenberg) featuredImageHelper = GutenbergFeaturedImageHelper(post: post, gutenberg: gutenberg) - stockPhotos = GutenbergStockPhotos(gutenberg: gutenberg, mediaInserter: mediaInserterHelper) filesAppMediaPicker = GutenbergFilesAppMediaSource(gutenberg: gutenberg, mediaInserter: mediaInserterHelper) - tenorMediaPicker = GutenbergTenorMediaPicker(gutenberg: gutenberg, mediaInserter: mediaInserterHelper) + externalMediaPicker = GutenbergExternalMediaPicker(gutenberg: gutenberg, mediaInserter: mediaInserterHelper) gutenbergImageLoader.post = post refreshInterface() } @@ -255,12 +246,6 @@ class GutenbergViewController: UIViewController, PostEditor, FeaturedImageDelega return UInt(currentMetrics.wordCount) } - /// Media Library Data Source - /// - lazy var mediaLibraryDataSource: MediaLibraryPickerDataSource = { - return MediaLibraryPickerDataSource(post: self.post) - }() - // MARK: - Private variables private lazy var gutenbergImageLoader: GutenbergImageLoader = { @@ -293,8 +278,24 @@ class GutenbergViewController: UIViewController, PostEditor, FeaturedImageDelega }() // MARK: - Initializers - required convenience init(post: AbstractPost, loadAutosaveRevision: Bool, replaceEditor: @escaping ReplaceEditorCallback, editorSession: PostEditorAnalyticsSession?) { - self.init(post: post, loadAutosaveRevision: loadAutosaveRevision, replaceEditor: replaceEditor, editorSession: editorSession) + required convenience init( + post: AbstractPost, loadAutosaveRevision: Bool, + replaceEditor: @escaping ReplaceEditorCallback, + editorSession: PostEditorAnalyticsSession? + ) { + self.init( + post: post, + loadAutosaveRevision: loadAutosaveRevision, + replaceEditor: replaceEditor, + editorSession: editorSession, + // Notice this parameter. + // The value is the default set in the required init but we need to set it explicitly, + // otherwise we'd trigger and infinite loop on this init. + // + // The reason we need this init at all even though the other one does the same job is + // to conform to the PostEditor protocol. + navigationBarManager: nil + ) } required init( @@ -343,6 +344,7 @@ class GutenbergViewController: UIViewController, PostEditor, FeaturedImageDelega setupGutenbergView() configureNavigationBar() refreshInterface() + observeNetworkStatus() gutenberg.delegate = self fetchBlockSettings() @@ -585,17 +587,33 @@ extension GutenbergViewController { extension GutenbergViewController: GutenbergBridgeDelegate { func gutenbergDidGetRequestFetch(path: String, completion: @escaping (Result) -> Void) { - post.managedObjectContext!.perform { + guard let context = post.managedObjectContext else { + didEncounterMissingContextError() + completion(.failure(URLError(.unknown) as NSError)) + return + } + context.perform { GutenbergNetworkRequest(path: path, blog: self.post.blog, method: .get).request(completion: completion) } } func gutenbergDidPostRequestFetch(path: String, data: [String: AnyObject]?, completion: @escaping (Result) -> Void) { - post.managedObjectContext!.perform { + guard let context = post.managedObjectContext else { + didEncounterMissingContextError() + completion(.failure(URLError(.unknown) as NSError)) + return + } + context.perform { GutenbergNetworkRequest(path: path, blog: self.post.blog, method: .post, data: data).request(completion: completion) } } + private func didEncounterMissingContextError() { + DispatchQueue.main.async { + WordPressAppDelegate.crashLogging?.logError(NSError(domain: self.errorDomain, code: ErrorCode.managedObjectContextMissing.rawValue, userInfo: [NSDebugDescriptionErrorKey: "The post is missing an associated managed object context"])) + } + } + func editorDidAutosave() { autosaver.contentDidChange() } @@ -609,14 +627,10 @@ extension GutenbergViewController: GutenbergBridgeDelegate { gutenbergDidRequestMediaFromDevicePicker(filter: flags, allowMultipleSelection: allowMultipleSelection, with: callback) case .deviceCamera: gutenbergDidRequestMediaFromCameraPicker(filter: flags, with: callback) - case .stockPhotos: - stockPhotos.presentPicker(origin: self, post: post, multipleSelection: allowMultipleSelection, callback: callback) + externalMediaPicker.presentStockPhotoPicker(origin: self, post: post, multipleSelection: allowMultipleSelection, callback: callback) case .tenor: - tenorMediaPicker.presentPicker(origin: self, - post: post, - multipleSelection: allowMultipleSelection, - callback: callback) + externalMediaPicker.presentTenorPicker(origin: self, post: post, multipleSelection: allowMultipleSelection, callback: callback) case .otherApps, .allFiles: filesAppMediaPicker.presentPicker(origin: self, filters: filter, allowedTypesOnBlog: post.blog.allowedTypeIdentifiers, multipleSelection: allowMultipleSelection, callback: callback) default: break @@ -646,31 +660,22 @@ extension GutenbergViewController: GutenbergBridgeDelegate { } func gutenbergDidRequestMediaFromSiteMediaLibrary(filter: WPMediaType, allowMultipleSelection: Bool, with callback: @escaping MediaPickerDidPickMediaCallback) { - mediaPickerHelper.presentMediaPickerFullScreen(animated: true, - filter: filter, - dataSourceType: .mediaLibrary, - allowMultipleSelection: allowMultipleSelection, - callback: { [weak self] assets in + mediaPickerHelper.presentSiteMediaPicker(filter: filter, allowMultipleSelection: allowMultipleSelection) { [weak self] assets in guard let self, let media = assets as? [Media] else { callback(nil) return } self.mediaInserterHelper.insertFromSiteMediaLibrary(media: media, callback: callback) - }) + } } func gutenbergDidRequestMediaFromDevicePicker(filter: WPMediaType, allowMultipleSelection: Bool, with callback: @escaping MediaPickerDidPickMediaCallback) { - - mediaPickerHelper.presentMediaPickerFullScreen(animated: true, - filter: filter, - dataSourceType: .device, - allowMultipleSelection: allowMultipleSelection, - callback: { [weak self] assets in + mediaPickerHelper.presetDevicePhotosPicker(filter: filter, allowMultipleSelection: allowMultipleSelection) { [weak self] assets in guard let self, let assets, !assets.isEmpty else { return callback(nil) } self.mediaInserterHelper.insertFromDevice(assets, callback: callback) - }) + } } func gutenbergDidRequestMediaFromCameraPicker(filter: WPMediaType, with callback: @escaping MediaPickerDidPickMediaCallback) { @@ -678,9 +683,7 @@ extension GutenbergViewController: GutenbergBridgeDelegate { guard let self, let asset = assets?.first else { return callback(nil) } - if let asset = asset as? PHAsset { - self.mediaInserterHelper.insertFromDevice(asset: asset, callback: callback) - } else if let image = asset as? UIImage { + if let image = asset as? UIImage { self.mediaInserterHelper.insertFromImage(image: image, callback: callback, source: .camera) } else if let url = asset as? URL { self.mediaInserterHelper.insertFromDevice(url: url, callback: callback, source: .camera) @@ -768,19 +771,6 @@ extension GutenbergViewController: GutenbergBridgeDelegate { present(alertController, animated: true, completion: nil) } - struct AnyEncodable: Encodable { - - let value: Encodable - init(value: Encodable) { - self.value = value - } - - func encode(to encoder: Encoder) throws { - try value.encode(to: encoder) - } - - } - func gutenbergDidRequestMediaFilesEditorLoad(_ mediaFiles: [[String: Any]], blockId: String) { if mediaFiles.isEmpty { @@ -863,7 +853,11 @@ extension GutenbergViewController: GutenbergBridgeDelegate { message = error.localizedDescription if media.canRetry { let retryUploadAction = UIAlertAction(title: MediaAttachmentActionSheet.retryUploadActionTitle, style: .default) { (action) in - self.mediaInserterHelper.retryUploadOf(media: media) + #if DEBUG || INTERNAL_BUILD + self.mediaInserterHelper.retryFailedMediaUploads() + #else + self.mediaInserterHelper.retryUploadOf(media: media) + #endif } alertController.addAction(retryUploadAction) } @@ -1090,6 +1084,10 @@ extension GutenbergViewController: GutenbergBridgeDelegate { self.topmostPresentedViewController.present(navController, animated: true) } + func gutenbergDidRequestConnectionStatus() -> Bool { + return ReachabilityUtils.isInternetReachable() + } + func gutenbergDidRequestSendEventToHost(_ eventName: String, properties: [AnyHashable: Any]) -> Void { post.managedObjectContext?.perform { WPAnalytics.trackBlockEditorEvent(eventName, properties: properties, blog: self.post.blog) @@ -1097,6 +1095,19 @@ extension GutenbergViewController: GutenbergBridgeDelegate { } } +// MARK: - NetworkAwareUI NetworkStatusDelegate + +extension GutenbergViewController: NetworkStatusDelegate { + func networkStatusDidChange(active: Bool) { + gutenberg.connectionStatusChange(isConnected: active) + #if DEBUG || INTERNAL_BUILD + if active { + mediaInserterHelper.retryFailedMediaUploads() + } + #endif + } +} + // MARK: - Suggestions implementation extension GutenbergViewController { @@ -1420,7 +1431,6 @@ private extension GutenbergViewController { ) static let stopUploadActionTitle = NSLocalizedString("Stop upload", comment: "User action to stop upload.") static let retryUploadActionTitle = NSLocalizedString("Retry", comment: "User action to retry media upload.") - static let retryAllFailedUploadsActionTitle = NSLocalizedString("Retry all", comment: "User action to retry all failed media uploads.") } } diff --git a/WordPress/Classes/ViewRelated/Gutenberg/Utils/GutenbergTenorMediaPicker.swift b/WordPress/Classes/ViewRelated/Gutenberg/Utils/GutenbergExternalMeidaPicker.swift similarity index 53% rename from WordPress/Classes/ViewRelated/Gutenberg/Utils/GutenbergTenorMediaPicker.swift rename to WordPress/Classes/ViewRelated/Gutenberg/Utils/GutenbergExternalMeidaPicker.swift index d22b1c8ccd0f..c1e75b4bb6a0 100644 --- a/WordPress/Classes/ViewRelated/Gutenberg/Utils/GutenbergTenorMediaPicker.swift +++ b/WordPress/Classes/ViewRelated/Gutenberg/Utils/GutenbergExternalMeidaPicker.swift @@ -1,7 +1,6 @@ import Gutenberg -class GutenbergTenorMediaPicker { - private var tenor: TenorPicker? +class GutenbergExternalMediaPicker { private var mediaPickerCallback: MediaPickerDidPickMediaCallback? private let mediaInserter: GutenbergMediaInserterHelper private unowned var gutenberg: Gutenberg @@ -12,23 +11,31 @@ class GutenbergTenorMediaPicker { self.gutenberg = gutenberg } - func presentPicker(origin: UIViewController, post: AbstractPost, multipleSelection: Bool, callback: @escaping MediaPickerDidPickMediaCallback) { - let picker = TenorPicker() - tenor = picker - picker.allowMultipleSelection = true - picker.delegate = self + func presentTenorPicker(origin: UIViewController, post: AbstractPost, multipleSelection: Bool, callback: @escaping MediaPickerDidPickMediaCallback) { mediaPickerCallback = callback - picker.presentPicker(origin: origin, blog: post.blog) self.multipleSelection = multipleSelection + + MediaPickerMenu(viewController: origin, isMultipleSelectionEnabled: multipleSelection) + .showFreeGIFPicker(blog: post.blog, delegate: self) + } + + func presentStockPhotoPicker(origin: UIViewController, post: AbstractPost, multipleSelection: Bool, callback: @escaping MediaPickerDidPickMediaCallback) { + mediaPickerCallback = callback + self.multipleSelection = multipleSelection + + MediaPickerMenu(viewController: origin, isMultipleSelectionEnabled: multipleSelection) + .showStockPhotosPicker(blog: post.blog, delegate: self) } } -extension GutenbergTenorMediaPicker: TenorPickerDelegate { - func tenorPicker(_ picker: TenorPicker, didFinishPicking assets: [TenorMedia]) { +extension GutenbergExternalMediaPicker: ExternalMediaPickerViewDelegate { + func externalMediaPickerViewController(_ viewController: ExternalMediaPickerViewController, didFinishWithSelection assets: [ExternalMediaAsset]) { defer { mediaPickerCallback = nil - tenor = nil } + + viewController.presentingViewController?.dismiss(animated: true) + guard assets.isEmpty == false else { mediaPickerCallback?(nil) return @@ -36,34 +43,38 @@ extension GutenbergTenorMediaPicker: TenorPickerDelegate { // For blocks that support multiple uploads this will upload all images. // If multiple uploads are not supported then it will seperate them out to Image Blocks. - multipleSelection ? insertOnBlock(with: assets) : insertSingleImages(assets) + if multipleSelection { + insertOnBlock(with: assets, source: viewController.source) + } else { + insertSingleImages(assets, source: viewController.source) + } } /// Adds the given image object to the requesting block and seperates multiple images to seperate image blocks /// - Parameter asset: Tenor Media object to add. - func insertSingleImages(_ assets: [TenorMedia]) { + func insertSingleImages(_ assets: [ExternalMediaAsset], source: MediaSource) { // Append the first item via callback given by Gutenberg. if let firstItem = assets.first { - insertOnBlock(with: [firstItem]) + insertOnBlock(with: [firstItem], source: source) } // Append the rest of images via `.appendMedia` event. // Ideally we would send all picked images via the given callback, but that seems to not be possible yet. - appendOnNewBlocks(assets: assets.dropFirst()) + appendOnNewBlocks(assets: assets.dropFirst(), source: source) } /// Adds the given images to the requesting block /// - Parameter assets: Tenor Media objects to add. - func insertOnBlock(with assets: [TenorMedia]) { + func insertOnBlock(with assets: [ExternalMediaAsset], source: MediaSource) { guard let callback = mediaPickerCallback else { return assertionFailure("Image picked without callback") } let mediaInfo = assets.compactMap { (asset) -> MediaInfo? in - guard let media = self.mediaInserter.insert(exportableAsset: asset, source: .tenor) else { + guard let media = self.mediaInserter.insert(exportableAsset: asset, source: source) else { return nil } let mediaUploadID = media.gutenbergUploadID - return MediaInfo(id: mediaUploadID, url: asset.URL.absoluteString, type: media.mediaTypeString) + return MediaInfo(id: mediaUploadID, url: asset.largeURL.absoluteString, type: media.mediaTypeString) } callback(mediaInfo) @@ -71,10 +82,10 @@ extension GutenbergTenorMediaPicker: TenorPickerDelegate { /// Create a new image block for each of the image objects in the slice. /// - Parameter assets: Tenor Media objects to append. - func appendOnNewBlocks(assets: ArraySlice) { + func appendOnNewBlocks(assets: ArraySlice, source: MediaSource) { assets.forEach { - if let media = self.mediaInserter.insert(exportableAsset: $0, source: .tenor) { - self.gutenberg.appendMedia(id: media.gutenbergUploadID, url: $0.URL, type: .image) + if let media = self.mediaInserter.insert(exportableAsset: $0, source: source) { + self.gutenberg.appendMedia(id: media.gutenbergUploadID, url: $0.largeURL, type: .image) } } } diff --git a/WordPress/Classes/ViewRelated/Gutenberg/Utils/GutenbergStockPhotos.swift b/WordPress/Classes/ViewRelated/Gutenberg/Utils/GutenbergStockPhotos.swift deleted file mode 100644 index 881486d66013..000000000000 --- a/WordPress/Classes/ViewRelated/Gutenberg/Utils/GutenbergStockPhotos.swift +++ /dev/null @@ -1,82 +0,0 @@ -import Gutenberg - -class GutenbergStockPhotos { - private var stockPhotos: StockPhotosPicker? - private var mediaPickerCallback: MediaPickerDidPickMediaCallback? - private let mediaInserter: GutenbergMediaInserterHelper - private unowned var gutenberg: Gutenberg - private var multipleSelection = false - - init(gutenberg: Gutenberg, mediaInserter: GutenbergMediaInserterHelper) { - self.mediaInserter = mediaInserter - self.gutenberg = gutenberg - } - - func presentPicker(origin: UIViewController, post: AbstractPost, multipleSelection: Bool, callback: @escaping MediaPickerDidPickMediaCallback) { - let picker = StockPhotosPicker() - stockPhotos = picker - picker.allowMultipleSelection = multipleSelection - picker.delegate = self - mediaPickerCallback = callback - picker.presentPicker(origin: origin, blog: post.blog) - self.multipleSelection = multipleSelection - } -} - -extension GutenbergStockPhotos: StockPhotosPickerDelegate { - - func stockPhotosPicker(_ picker: StockPhotosPicker, didFinishPicking assets: [StockPhotosMedia]) { - defer { - mediaPickerCallback = nil - stockPhotos = nil - } - guard assets.isEmpty == false else { - mediaPickerCallback?(nil) - return - } - - // For blocks that support multiple uploads this will upload all images. - // If multiple uploads are not supported then it will seperate them out to Image Blocks. - multipleSelection ? insertOnBlock(with: assets) : insertSingleImages(assets) - } - - /// Adds the given image object to the requesting block and seperates multiple images to seperate image blocks - /// - Parameter asset: Stock Media object to add. - func insertSingleImages(_ assets: [StockPhotosMedia]) { - // Append the first item via callback given by Gutenberg. - if let firstItem = assets.first { - insertOnBlock(with: [firstItem]) - } - // Append the rest of images via `.appendMedia` event. - // Ideally we would send all picked images via the given callback, but that seems to not be possible yet. - appendOnNewBlocks(assets: assets.dropFirst()) - } - - /// Adds the given images to the requesting block - /// - Parameter assets: Stock Media objects to add. - func insertOnBlock(with assets: [StockPhotosMedia]) { - guard let callback = mediaPickerCallback else { - return assertionFailure("Image picked without callback") - } - - let mediaInfo = assets.compactMap({ (asset) -> MediaInfo? in - guard let media = self.mediaInserter.insert(exportableAsset: asset, source: .stockPhotos) else { - return nil - } - let mediaUploadID = media.gutenbergUploadID - return MediaInfo(id: mediaUploadID, url: asset.URL.absoluteString, type: media.mediaTypeString) - }) - - callback(mediaInfo) - } - - /// Create a new image block for each of the image objects in the slice. - /// - Parameter assets: Stock Media objects to append. - func appendOnNewBlocks(assets: ArraySlice) { - assets.forEach { - if let media = self.mediaInserter.insert(exportableAsset: $0, source: .stockPhotos) { - self.gutenberg.appendMedia(id: media.gutenbergUploadID, url: $0.URL, type: .image) - } - } - } -} diff --git a/WordPress/Classes/ViewRelated/Jetpack/Branding/Button/JetpackButton.swift b/WordPress/Classes/ViewRelated/Jetpack/Branding/Button/JetpackButton.swift index 92171c112b6c..94edf069ca8c 100644 --- a/WordPress/Classes/ViewRelated/Jetpack/Branding/Button/JetpackButton.swift +++ b/WordPress/Classes/ViewRelated/Jetpack/Branding/Button/JetpackButton.swift @@ -91,7 +91,6 @@ class JetpackButton: CircularImageButton { static let iconInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 10) static let contentInsets = UIEdgeInsets(top: 6, left: 6, bottom: 6, right: 10) static let maximumFontPointSize: CGFloat = 22 - static let imageBackgroundViewMultiplier: CGFloat = 0.75 static var titleFont: UIFont { let fontDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .callout) let font = UIFont(descriptor: fontDescriptor, size: min(fontDescriptor.pointSize, maximumFontPointSize)) diff --git a/WordPress/Classes/ViewRelated/Jetpack/Branding/Coordinator/JetpackRedirector.swift b/WordPress/Classes/ViewRelated/Jetpack/Branding/Coordinator/JetpackRedirector.swift index a328d31b3ae5..bbef8643cc65 100644 --- a/WordPress/Classes/ViewRelated/Jetpack/Branding/Coordinator/JetpackRedirector.swift +++ b/WordPress/Classes/ViewRelated/Jetpack/Branding/Coordinator/JetpackRedirector.swift @@ -1,4 +1,5 @@ import Foundation +import StoreKit class JetpackRedirector { @@ -30,8 +31,63 @@ class JetpackRedirector { // First, check if the WordPress app can open Jetpack by testing its URL scheme. // if we can potentially open Jetpack app, let's open it through universal link to avoid scheme conflicts (e.g., a certain game :-). - // finally, if the user might not have Jetpack installed, direct them to App Store page. - let urlToOpen = UIApplication.shared.canOpenURL(jetpackDeepLinkURL) ? jetpackUniversalLinkURL : jetpackAppStoreURL - UIApplication.shared.open(urlToOpen) + // finally, if the user might not have Jetpack installed, open App Store view controller through StoreKit. + if UIApplication.shared.canOpenURL(jetpackDeepLinkURL) { + UIApplication.shared.open(jetpackUniversalLinkURL) + } else { + showJetpackAppInstallation(fallbackURL: jetpackAppStoreURL) + } + } + + private static func showJetpackAppInstallation(fallbackURL: URL) { + let viewController = RootViewCoordinator.sharedPresenter.rootViewController.topmostPresentedViewController + let storeProductVC = SKStoreProductViewController() + let appID = [SKStoreProductParameterITunesItemIdentifier: "1565481562"] + + configureNavigationBarAppearance(storeProductVC) + + storeProductVC.loadProduct(withParameters: appID) { (result, error) in + if result { + viewController.present(storeProductVC, animated: true) + } else if let error = error { + DDLogError("Failed loading Jetpack App product: \(error.localizedDescription)") + UIApplication.shared.open(fallbackURL) + } + } } + + // MARK: - SKStoreProductViewController navigation bar appearance + + /// Sets SKStoreProductViewController navigation bar translucent + /// + /// Application's global navigation appearance settings interferes with SKStoreProductViewController + /// which requires for this temporary workaround + private static func configureNavigationBarAppearance(_ controller: SKStoreProductViewController) { + let previousisTranslucentValue = UINavigationBar.appearance().isTranslucent + UINavigationBar.appearance().isTranslucent = true + + /// Reset to default translucent value + storeProductViewControllerObserver = StoreProductViewControllerObserver(onDismiss: { + UINavigationBar.appearance().isTranslucent = previousisTranslucentValue + storeProductViewControllerObserver = nil + }) + + controller.delegate = storeProductViewControllerObserver + } + + /// Observe product view controller dismissal + class StoreProductViewControllerObserver: NSObject, SKStoreProductViewControllerDelegate { + private let onDismiss: () -> () + + init(onDismiss: @escaping () -> ()) { + self.onDismiss = onDismiss + super.init() + } + + func productViewControllerDidFinish(_ viewController: SKStoreProductViewController) { + onDismiss() + } + } + + private static var storeProductViewControllerObserver: StoreProductViewControllerObserver? } diff --git a/WordPress/Classes/ViewRelated/Jetpack/Branding/Fullscreen Overlay/JetpackFullscreenOverlayViewController.swift b/WordPress/Classes/ViewRelated/Jetpack/Branding/Fullscreen Overlay/JetpackFullscreenOverlayViewController.swift index 29589db8777b..9075f8879504 100644 --- a/WordPress/Classes/ViewRelated/Jetpack/Branding/Fullscreen Overlay/JetpackFullscreenOverlayViewController.swift +++ b/WordPress/Classes/ViewRelated/Jetpack/Branding/Fullscreen Overlay/JetpackFullscreenOverlayViewController.swift @@ -278,9 +278,7 @@ private extension JetpackFullscreenOverlayViewController { static let compactStackViewSpacing: CGFloat = 10 static let closeButtonRadius: CGFloat = 30 static let mainButtonsContentInsets = NSDirectionalEdgeInsets(top: 4, leading: 12, bottom: 4, trailing: 12) - static let mainButtonsContentEdgeInsets = UIEdgeInsets(top: 4, left: 12, bottom: 4, right: 12) static let learnMoreButtonContentInsets = NSDirectionalEdgeInsets(top: 4, leading: 0, bottom: 4, trailing: 24) - static let learnMoreButtonContentEdgeInsets = UIEdgeInsets(top: 4, left: 0, bottom: 4, right: 24) static let externalIconSize = CGSize(width: 16, height: 16) static let externalIconBounds = CGRect(x: 0, y: -2, width: 16, height: 16) static let switchButtonCornerRadius: CGFloat = 6 @@ -288,7 +286,6 @@ private extension JetpackFullscreenOverlayViewController { static let titleKern: CGFloat = 0.37 static let buttonsNormalBottomSpacing: CGFloat = 30 static let singleButtonBottomSpacing: CGFloat = 60 - static let actionInfoButtonBottomSpacing: CGFloat = 24 } enum Constants { diff --git a/WordPress/Classes/ViewRelated/Jetpack/Branding/JetpackBrandingVisibility.swift b/WordPress/Classes/ViewRelated/Jetpack/Branding/JetpackBrandingVisibility.swift index 11c193be947e..676ecc8e9310 100644 --- a/WordPress/Classes/ViewRelated/Jetpack/Branding/JetpackBrandingVisibility.swift +++ b/WordPress/Classes/ViewRelated/Jetpack/Branding/JetpackBrandingVisibility.swift @@ -1,30 +1,28 @@ - -import Foundation - /// An enum that unifies the checks to limit the visibility of the Jetpack branding elements (banners and badges) +/// +/// This thing was born as an enum but at the time of writing had only one case in use. +/// There is no reason to have that case linger around, it's been left merely to avoid changing its 15 usages. enum JetpackBrandingVisibility { case all - case wordPressApp - case dotcomAccounts - case dotcomAccountsOnWpApp // useful if we want to release in phases and exclude the feature flag in some cases - case featureFlagBased - var enabled: Bool { + func isEnabled( + isWordPress: Bool, + isDotComAvailable: Bool, + shouldShowJetpackFeatures: Bool + ) -> Bool { switch self { case .all: - return AppConfiguration.isWordPress && - AccountHelper.isDotcomAvailable() && - JetpackFeaturesRemovalCoordinator.shouldShowJetpackFeatures() - case .wordPressApp: - return AppConfiguration.isWordPress - case .dotcomAccounts: - return AccountHelper.isDotcomAvailable() - case .dotcomAccountsOnWpApp: - return AppConfiguration.isWordPress && - AccountHelper.isDotcomAvailable() - case .featureFlagBased: - return true + return isWordPress && isDotComAvailable && shouldShowJetpackFeatures } } + + @available(*, deprecated, message: "Use the isEnabled function to allow injecting the configuration.") + var enabled: Bool { + return isEnabled( + isWordPress: AppConfiguration.isWordPress, + isDotComAvailable: AccountHelper.isDotcomAvailable(), + shouldShowJetpackFeatures: JetpackFeaturesRemovalCoordinator.shouldShowJetpackFeatures() + ) + } } diff --git a/WordPress/Classes/ViewRelated/Jetpack/Branding/Menu Card/JetpackBrandingMenuCardCell.swift b/WordPress/Classes/ViewRelated/Jetpack/Branding/Menu Card/JetpackBrandingMenuCardCell.swift index fbbf031067bd..692ce1f0d4b9 100644 --- a/WordPress/Classes/ViewRelated/Jetpack/Branding/Menu Card/JetpackBrandingMenuCardCell.swift +++ b/WordPress/Classes/ViewRelated/Jetpack/Branding/Menu Card/JetpackBrandingMenuCardCell.swift @@ -349,7 +349,6 @@ private extension JetpackBrandingMenuCardCell { // Learn more button static let learnMoreButtonContentInsets = NSDirectionalEdgeInsets(top: 4, leading: 0, bottom: 4, trailing: 24) - static let learnMoreButtonContentEdgeInsets = UIEdgeInsets(top: 4, left: 0, bottom: 4, right: 24) static let learnMoreButtonTextColor: UIColor = UIColor.muriel(color: .jetpackGreen, .shade40) } diff --git a/WordPress/Classes/ViewRelated/Jetpack/Login/JetpackLoginViewController.swift b/WordPress/Classes/ViewRelated/Jetpack/Login/JetpackLoginViewController.swift index 4d7d11fd9e9a..3f73849204e2 100644 --- a/WordPress/Classes/ViewRelated/Jetpack/Login/JetpackLoginViewController.swift +++ b/WordPress/Classes/ViewRelated/Jetpack/Login/JetpackLoginViewController.swift @@ -10,7 +10,6 @@ class JetpackLoginViewController: UIViewController { // MARK: - Constants - fileprivate let jetpackInstallRelativePath = "plugin-install.php?tab=plugin-information&plugin=jetpack" var blog: Blog // MARK: - Properties @@ -158,13 +157,6 @@ class JetpackLoginViewController: UIViewController { faqButton.isHidden = tacButton.isHidden } - // MARK: - Private Helpers - - fileprivate func managedObjectContext() -> NSManagedObjectContext { - return ContextManager.sharedInstance().mainContext - } - - // MARK: - Browser fileprivate func openInstallJetpackURL() { diff --git a/WordPress/Classes/ViewRelated/Me/All Domains/Coordinators/AllDomainsAddDomainCoordinator.swift b/WordPress/Classes/ViewRelated/Me/All Domains/Coordinators/AllDomainsAddDomainCoordinator.swift new file mode 100644 index 000000000000..a83566cba9ad --- /dev/null +++ b/WordPress/Classes/ViewRelated/Me/All Domains/Coordinators/AllDomainsAddDomainCoordinator.swift @@ -0,0 +1,43 @@ +import Foundation + +@objc final class AllDomainsAddDomainCoordinator: NSObject { + static func presentAddDomainFlow(in allDomainsViewController: AllDomainsListViewController) { + let analyticsSource = AllDomainsListViewController.Constants.analyticsSource + let coordinator = RegisterDomainCoordinator(site: nil, analyticsSource: analyticsSource) + let domainSuggestionsViewController = DomainSelectionViewController( + service: DomainsServiceAdapter(coreDataStack: ContextManager.shared), + domainSelectionType: .purchaseFromDomainManagement, + includeSupportButton: false, + coordinator: coordinator + ) + + let domainPurchasedCallback = { (domainViewController: UIViewController, domainName: String) in + domainViewController.dismiss(animated: true) { + allDomainsViewController.reloadDomains() + } + } + + let domainAddedToCart = FreeToPaidPlansCoordinator.plansFlowAfterDomainAddedToCartBlock( + customTitle: nil, + analyticsSource: analyticsSource + ) { [weak coordinator] controller, domain in + domainPurchasedCallback(controller, domain) + coordinator?.trackDomainPurchasingCompleted() + } + + coordinator.domainPurchasedCallback = domainPurchasedCallback // For no site flow (domain only) + coordinator.domainAddedToCartAndLinkedToSiteCallback = domainAddedToCart // For existing site flow (plans) + + let navigationController = UINavigationController(rootViewController: domainSuggestionsViewController) + navigationController.isModalInPresentation = true + allDomainsViewController.present(navigationController, animated: true) + } +} + +extension AllDomainsAddDomainCoordinator { + private enum Strings { + static let searchTitle = NSLocalizedString("domain.management.addDomain.search.title", + value: "Search for a domain", + comment: "Search domain - Title for the Suggested domains screen") + } +} diff --git a/WordPress/Classes/ViewRelated/Me/All Domains/View Models/AllDomainsListItemViewModel.swift b/WordPress/Classes/ViewRelated/Me/All Domains/View Models/AllDomainsListItemViewModel.swift new file mode 100644 index 000000000000..aba2f3a84040 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Me/All Domains/View Models/AllDomainsListItemViewModel.swift @@ -0,0 +1,73 @@ +import Foundation + +struct AllDomainsListItemViewModel { + + // MARK: - Types + + private enum Strings { + static let expired = NSLocalizedString( + "domain.management.card.expired.label", + value: "Expired", + comment: "The expired label of the domain card in All Domains screen." + ) + static let renews = NSLocalizedString( + "domain.management.card.renews.label", + value: "Renews", + comment: "The renews label of the domain card in All Domains screen." + ) + + static let neverExpires = NSLocalizedString( + "domain.management.card.neverExpires.label", + value: "Never expires", + comment: "Label indicating that a domain name registration has no expiry date." + ) + } + + typealias Row = AllDomainsListCardView.ViewModel + typealias Domain = DomainsService.AllDomainsListItem + typealias Status = Domain.Status + typealias StatusType = DomainsService.AllDomainsListItem.StatusType + + // MARK: - Properties + + private static let dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .medium + formatter.timeStyle = .none + return formatter + }() + + let domain: Domain + let row: Row + + // MARK: - Init + + init(domain: Domain) { + self.domain = domain + self.row = .init( + name: domain.domain, + description: Self.description(from: domain), + status: domain.status, + expiryDate: Self.expiryDate(from: domain) + ) + } + + // MARK: - Helpers + + static func description(from domain: Domain) -> String? { + guard !domain.isDomainOnlySite else { + return nil + } + return !domain.blogName.isEmpty ? domain.blogName : domain.siteSlug + } + + static func expiryDate(from domain: Domain) -> String { + guard let date = domain.expiryDate, domain.hasRegistration else { + return Strings.neverExpires + } + let expired = date < Date() + let notice = expired ? Strings.expired : Strings.renews + let formatted = Self.dateFormatter.string(from: date) + return "\(notice) \(formatted)" + } +} diff --git a/WordPress/Classes/ViewRelated/Me/All Domains/View Models/AllDomainsListViewModel+Strings.swift b/WordPress/Classes/ViewRelated/Me/All Domains/View Models/AllDomainsListViewModel+Strings.swift new file mode 100644 index 000000000000..16ecd8f591be --- /dev/null +++ b/WordPress/Classes/ViewRelated/Me/All Domains/View Models/AllDomainsListViewModel+Strings.swift @@ -0,0 +1,35 @@ +import Foundation + +extension AllDomainsListViewModel { + + enum Strings { + static let emptyStateButtonTitle = NSLocalizedString( + "domain.management.default.empty.state.button.title", + value: "Find a domain", + comment: "The empty state button title in All Domains screen when the user doesn't have any domains" + ) + static let emptyStateTitle = NSLocalizedString( + "domain.management.default.empty.state.title", + value: "You don't have any domains", + comment: "The empty state title in All Domains screen when the user doesn't have any domains" + ) + static let emptyStateDescription = NSLocalizedString( + "domain.management.default.empty.state.description", + value: "Tap below to find your perfect domain.", + comment: "The empty state description in All Domains screen when the user doesn't have any domains" + ) + static let searchEmptyStateTitle = NSLocalizedString( + "domain.management.search.empty.state.title", + value: "No Matching Domains Found", + comment: "The empty state title in All Domains screen when the are no domains matching the search criteria" + ) + static func searchEmptyStateDescription(_ searchQuery: String) -> String { + let format = NSLocalizedString( + "domain.management.search.empty.state.description", + value: "We couldn't find any domains that match your search for '%@'", + comment: "The empty state description in All Domains screen when the are no domains matching the search criteria" + ) + return String(format: format, searchQuery) + } + } +} diff --git a/WordPress/Classes/ViewRelated/Me/All Domains/View Models/AllDomainsListViewModel.swift b/WordPress/Classes/ViewRelated/Me/All Domains/View Models/AllDomainsListViewModel.swift new file mode 100644 index 000000000000..ddacf06ab67c --- /dev/null +++ b/WordPress/Classes/ViewRelated/Me/All Domains/View Models/AllDomainsListViewModel.swift @@ -0,0 +1,189 @@ +import Foundation +import Combine + +class AllDomainsListViewModel { + + // MARK: - Types + + enum State { + /// This state is set when domains data is loaded. + case normal([AllDomainsListItemViewModel]) + + /// This state is set when domains data is being fetched. + case loading + + /// This state is set when the list is empty or an error occurs. + case message(DomainsStateViewModel) + } + + private enum ViewModelError: Error { + case internalError(reason: String) + } + + private typealias Domain = DomainsService.AllDomainsListItem + + // MARK: - Configuration + + var addDomainAction: (() -> Void)? + + // MARK: - Dependencies + + private var domainsService: DomainsService? + + // MARK: - Properties + + @Published + private(set) var state: State = .normal([]) + + private var domains = [Domain]() + + private var lastSearchQuery: String? + + private let searchQueue: OperationQueue = { + let queue = OperationQueue() + queue.maxConcurrentOperationCount = 1 + return queue + }() + + // MARK: - Init + + init(coreDataStack: CoreDataStackSwift = ContextManager.shared, domains: [DomainsService.AllDomainsListItem] = []) { + if let account = defaultAccount(with: coreDataStack) { + self.domainsService = .init(coreDataStack: coreDataStack, wordPressComRestApi: account.wordPressComRestApi) + } + self.domains = domains + if domains.count > 0 { + self.state = self.state(from: domains, searchQuery: lastSearchQuery) + } + } + + private func defaultAccount(with contextManager: CoreDataStackSwift) -> WPAccount? { + try? WPAccount.lookupDefaultWordPressComAccount(in: contextManager.mainContext) + } + + // MARK: - Resolving State + + /// Determines the state of the view based on the domains and an optional search query. + /// + /// - Parameters: + /// - domains: An array of domain objects to be filtered. + /// - searchQuery: An optional search query to filter the domains. Pass `nil` or an empty string to skip filtering. + /// - Returns: The `.normal` or `.empty` state. + private func state(from domains: [Domain], searchQuery: String?) -> State { + if domains.isEmpty { + return .message(noDomainsMessageViewModel()) + } + + var domains = domains + + if let searchQuery, !searchQuery.trimmingCharacters(in: .whitespaces).isEmpty { + domains = domains.filter { $0.matches(searchQuery: searchQuery) } + } + + let viewModels = domains.map { AllDomainsListItemViewModel(domain: $0) } + + if let searchQuery, viewModels.isEmpty { + return .message(noSearchResultsMessageViewModel(searchQuery: searchQuery)) + } else if viewModels.isEmpty { + return .message(noDomainsMessageViewModel()) + } else { + return .normal(viewModels) + } + } + + /// Determines the state of the view based on an error. + /// + /// - Parameter error: An error that occurred during a data operation. + /// - Returns: Always returns the `.empty` state. + private func state(from error: Error) -> State { + return .message(self.errorMessageViewModel(from: error)) + } + + // MARK: - Load Domains + + func loadData() { + if domains.isEmpty { + self.state = .loading + } + self.fetchAllDomains { [weak self] result in + guard let self else { + return + } + switch result { + case .success(let domains): + self.domains = domains + self.state = self.state(from: domains, searchQuery: lastSearchQuery) + case .failure(let error): + self.state = self.state(from: error) + } + } + } + + private func fetchAllDomains(completion: @escaping (DomainsService.AllDomainsEndpointResult) -> Void) { + guard let service = domainsService else { + completion(.failure(ViewModelError.internalError(reason: "The `domainsService` property is nil"))) + return + } + service.fetchAllDomains(resolveStatus: true, noWPCOM: true, completion: completion) + } + + // MARK: - Search + + func search(_ query: String?) { + // Keep track of the previous search query. + self.lastSearchQuery = query + + // Search shouldn't be performed if the user doesn't have any domains. + guard !domains.isEmpty else { + return + } + + // Perform search asynchrounously. + switch state { + case .normal, .message: + self.searchQueue.cancelAllOperations() + self.searchQueue.addOperation { [weak self] in + guard let self else { + return + } + let state: State = self.state(from: domains, searchQuery: query) + DispatchQueue.main.async { + self.state = state + } + } + default: + break + } + } + + // MARK: - Creating Message State View Models + + /// The message to display when the user doesn't have any domains. + private func noDomainsMessageViewModel() -> DomainsStateViewModel { + let action: () -> Void = { [weak self] in + self?.addDomainAction?() + WPAnalytics.track(.allDomainsFindDomainTapped) + } + return .init( + title: Strings.emptyStateTitle, + description: Strings.emptyStateDescription, + button: .init(title: Strings.emptyStateButtonTitle, action: action) + ) + } + + /// The message to display when an error occurs. + private func errorMessageViewModel(from error: Error) -> DomainsStateViewModel { + return DomainsStateViewModel.errorMessageViewModel(from: error) { [weak self] in + self?.loadData() + } + } + + /// The message to display when there are no domains matching the search query. + private func noSearchResultsMessageViewModel(searchQuery: String) -> DomainsStateViewModel { + return .init( + title: Strings.searchEmptyStateTitle, + description: Strings.searchEmptyStateDescription(searchQuery), + button: nil + ) + } +} diff --git a/WordPress/Classes/ViewRelated/Me/All Domains/Views/AllDomainsListActivityIndicatorTableViewCell.swift b/WordPress/Classes/ViewRelated/Me/All Domains/Views/AllDomainsListActivityIndicatorTableViewCell.swift new file mode 100644 index 000000000000..8cbeacc226b9 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Me/All Domains/Views/AllDomainsListActivityIndicatorTableViewCell.swift @@ -0,0 +1,36 @@ +import UIKit + +final class AllDomainsListActivityIndicatorTableViewCell: UITableViewCell { + + private let activityIndicator: UIActivityIndicatorView = UIActivityIndicatorView(style: .medium) + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + self.setupSubviews() + } + + private func setupSubviews() { + self.backgroundColor = .clear + self.contentView.backgroundColor = .clear + self.contentView.addSubview(activityIndicator) + self.activityIndicator.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + self.activityIndicator.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), + self.activityIndicator.centerYAnchor.constraint(equalTo: contentView.centerYAnchor) + ]) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func prepareForReuse() { + super.prepareForReuse() + self.activityIndicator.startAnimating() + } + + override func didMoveToSuperview() { + super.didMoveToSuperview() + self.activityIndicator.startAnimating() + } +} diff --git a/WordPress/Classes/ViewRelated/Me/All Domains/Views/AllDomainsListCardView.swift b/WordPress/Classes/ViewRelated/Me/All Domains/Views/AllDomainsListCardView.swift new file mode 100644 index 000000000000..9e6ece725437 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Me/All Domains/Views/AllDomainsListCardView.swift @@ -0,0 +1,184 @@ +import SwiftUI +import DesignSystem + +struct AllDomainsListCardView: View { + + // MARK: - Types + + struct ViewModel: Identifiable { + let id = UUID() + let name: String + let description: String? + let status: Status? + let expiryDate: String? + let isPrimary: Bool + + typealias Status = DomainsService.AllDomainsListItem.Status + typealias StatusType = DomainsService.AllDomainsListItem.StatusType + + init(name: String, description: String?, status: Status?, expiryDate: String?, isPrimary: Bool = false) { + self.name = name + self.description = description + self.status = status + self.expiryDate = expiryDate + self.isPrimary = isPrimary + } + } + + // MARK: - Properties + + private let viewModel: ViewModel + private let padding: CGFloat + + // MARK: - Init + + init(viewModel: ViewModel, padding: CGFloat = Length.Padding.double) { + self.viewModel = viewModel + self.padding = padding + } + + // MARK: - Views + + var body: some View { + textContainerVStack + .padding(padding) + } + + private var textContainerVStack: some View { + VStack(alignment: .leading, spacing: Length.Padding.single) { + domainText + domainHeadline + primaryDomainLabel + statusHStack + } + } + + private var domainText: some View { + Text(viewModel.name) + .font(.callout) + .foregroundColor(.primary) + } + + private var domainHeadline: some View { + Group { + if let value = viewModel.description { + Text(value) + .font(.subheadline) + .foregroundColor(.secondary) + } else { + EmptyView() + } + } + } + + private var primaryDomainLabel: some View { + Group { + if viewModel.isPrimary { + PrimaryDomainView() + } else { + EmptyView() + } + } + } + + private var statusHStack: some View { + HStack(spacing: Length.Padding.double) { + if let status = viewModel.status { + statusText(status: status) + Spacer() + } + expirationText + } + } + + private func statusText(status: DomainsService.AllDomainsListItem.Status) -> some View { + HStack(spacing: Length.Padding.single) { + Circle() + .fill(status.type.indicatorColor) + .frame( + width: Length.Padding.single, + height: Length.Padding.single + ) + Text(status.value) + .foregroundColor(status.type.textColor) + .font(.subheadline.weight(status.type.fontWeight)) + } + } + + private var expirationText: some View { + Group { + if let date = viewModel.expiryDate { + Text(date) + .font(.subheadline) + .foregroundColor(viewModel.status?.type.expireTextColor ?? Color.DS.Foreground.secondary) + } else { + EmptyView() + } + } + } +} + +private extension AllDomainsListCardView.ViewModel.StatusType { + + var fontWeight: Font.Weight { + switch self { + case .error, .alert: + return .bold + default: + return .regular + } + } + + var indicatorColor: Color { + switch self { + case .success, .premium: + return Color.DS.Foreground.success + case .warning: + return Color.DS.Foreground.warning + case .alert, .error: + return Color.DS.Foreground.error + case .neutral: + return Color.DS.Foreground.secondary + } + } + + var textColor: Color { + switch self { + case .warning: + return Color.DS.Foreground.warning + case .alert, .error: + return Color.DS.Foreground.error + default: + return Color.DS.Foreground.primary + } + } + + var expireTextColor: Color { + switch self { + case .warning: + return Color.DS.Foreground.warning + default: + return Color.DS.Foreground.secondary + } + } +} + +// MARK: - Previews + +struct AllDomainsListCardView_Previews: PreviewProvider { + static var previews: some View { + ZStack { + Color(.systemBackground) + AllDomainsListCardView( + viewModel: .init( + name: "domain.cool.cool", + description: "A Cool Website", + status: .init(value: "Active", type: .success), + expiryDate: "Expires Aug 15 2004" + ) + ) + } + .ignoresSafeArea() + .environment(\.colorScheme, .light) + } +} diff --git a/WordPress/Classes/ViewRelated/Me/All Domains/Views/AllDomainsListEmptyView.swift b/WordPress/Classes/ViewRelated/Me/All Domains/Views/AllDomainsListEmptyView.swift new file mode 100644 index 000000000000..89a63e98ec45 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Me/All Domains/Views/AllDomainsListEmptyView.swift @@ -0,0 +1,47 @@ +import UIKit +import WordPressUI +import DesignSystem +import SwiftUI + +final class AllDomainsListEmptyView: UIView { + + typealias ViewModel = DomainsStateViewModel + + // MARK: - Views + + private let stackView: UIStackView = { + let stackView = UIStackView() + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.backgroundColor = .clear + return stackView + }() + + // MARK: - Init + + init(viewModel: ViewModel? = nil) { + super.init(frame: .zero) + self.render(with: viewModel) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Rendering + + private func render(with viewModel: ViewModel?) { + self.addSubview(stackView) + self.pinSubviewToAllEdges(stackView) + self.update(with: viewModel) + } + + func update(with viewModel: ViewModel?) { + stackView.removeAllSubviews() + + if let viewModel = viewModel, + let stateView = UIHostingController(rootView: DomainsStateView(viewModel: viewModel)).view { + stateView.backgroundColor = .clear + stackView.addArrangedSubview(stateView) + } + } +} diff --git a/WordPress/Classes/ViewRelated/Me/All Domains/Views/AllDomainsListTableViewCell.swift b/WordPress/Classes/ViewRelated/Me/All Domains/Views/AllDomainsListTableViewCell.swift new file mode 100644 index 000000000000..e186f3feab99 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Me/All Domains/Views/AllDomainsListTableViewCell.swift @@ -0,0 +1,28 @@ +import UIKit +import SwiftUI + +final class AllDomainsListTableViewCell: UITableViewCell { + + private var hostingController: UIHostingController? + + func update(with viewModel: ViewModel, parent: UIViewController) { + let content = AllDomainsListCardView(viewModel: viewModel) + + if let hostingController { + hostingController.rootView = content + hostingController.view.invalidateIntrinsicContentSize() + } else { + let hostingController = UIHostingController(rootView: content) + hostingController.view.backgroundColor = .clear + hostingController.view.translatesAutoresizingMaskIntoConstraints = false + hostingController.willMove(toParent: parent) + self.contentView.addSubview(hostingController.view) + self.contentView.pinSubviewToAllEdges(hostingController.view) + parent.addChild(hostingController) + hostingController.didMove(toParent: parent) + self.hostingController = hostingController + } + } + + typealias ViewModel = AllDomainsListCardView.ViewModel +} diff --git a/WordPress/Classes/ViewRelated/Me/All Domains/Views/AllDomainsListViewController+Strings.swift b/WordPress/Classes/ViewRelated/Me/All Domains/Views/AllDomainsListViewController+Strings.swift new file mode 100644 index 000000000000..4c231cd0f867 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Me/All Domains/Views/AllDomainsListViewController+Strings.swift @@ -0,0 +1,17 @@ +import Foundation + +extension AllDomainsListViewController { + + enum Strings { + static let title = NSLocalizedString( + "domain.management.title", + value: "All Domains", + comment: "Domain Management Screen Title" + ) + static let searchBar = NSLocalizedString( + "domain.management.search-bar.title", + value: "Search domains", + comment: "The search bar title in All Domains screen." + ) + } +} diff --git a/WordPress/Classes/ViewRelated/Me/All Domains/Views/AllDomainsListViewController.swift b/WordPress/Classes/ViewRelated/Me/All Domains/Views/AllDomainsListViewController.swift new file mode 100644 index 000000000000..8ee630b2726f --- /dev/null +++ b/WordPress/Classes/ViewRelated/Me/All Domains/Views/AllDomainsListViewController.swift @@ -0,0 +1,268 @@ +import UIKit +import Combine +import AutomatticTracks +import DesignSystem + +final class AllDomainsListViewController: UIViewController { + + // MARK: - Types + + enum Constants { + static let analyticsSource = "all_domains" + } + + private enum Layout { + static let interRowSpacing = Length.Padding.double + } + + private enum CellIdentifiers { + static let myDomain = String(describing: AllDomainsListTableViewCell.self) + static let activityIndicator = String(describing: AllDomainsListActivityIndicatorTableViewCell.self) + } + + typealias ViewModel = AllDomainsListViewModel + typealias Domain = AllDomainsListItemViewModel + + // MARK: - Dependencies + + private let crashLogger: CrashLogging + private let viewModel: ViewModel + + // MARK: - Views + + private let tableView = UITableView(frame: .zero, style: .insetGrouped) + private let refreshControl = UIRefreshControl() + private let emptyView = AllDomainsListEmptyView() + + // MARK: - Properties + + private lazy var state: ViewModel.State = viewModel.state + + // MARK: - Observation + + private var cancellable = Set() + + // MARK: - Init + + init(viewModel: ViewModel = .init(), crashLogger: CrashLogging = CrashLogging.main) { + self.viewModel = viewModel + self.crashLogger = crashLogger + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Public Functions + + func reloadDomains() { + viewModel.loadData() + } + + // MARK: - View Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + self.viewModel.addDomainAction = { [weak self] in + self?.navigateToAddDomain() + WPAnalytics.track(.addDomainTapped) + } + self.title = Strings.title + WPStyleGuide.configureColors(view: view, tableView: nil) + self.setupSubviews() + self.observeState() + self.viewModel.loadData() + WPAnalytics.track(.domainsListShown) + } + + // MARK: - Setup Views + + private func setupSubviews() { + self.setupBarButtonItems() + self.setupSearchBar() + self.setupTableView() + self.setupRefreshControl() + self.setupEmptyView() + self.setupNavigationBarAppearance() + } + + private func setupBarButtonItems() { + let addAction = UIAction { [weak self] _ in + self?.viewModel.addDomainAction?() + } + let addBarButtonItem = UIBarButtonItem(systemItem: .add, primaryAction: addAction) + self.navigationItem.rightBarButtonItem = addBarButtonItem + } + + private func setupSearchBar() { + let searchController = UISearchController(searchResultsController: nil) + searchController.delegate = self + searchController.searchBar.delegate = self + searchController.searchBar.placeholder = Strings.searchBar + self.navigationItem.searchController = searchController + self.navigationItem.hidesSearchBarWhenScrolling = false + self.extendedLayoutIncludesOpaqueBars = true + self.edgesForExtendedLayout = .top + } + + private func setupTableView() { + self.tableView.backgroundColor = UIColor.systemGroupedBackground + self.tableView.translatesAutoresizingMaskIntoConstraints = false + self.tableView.dataSource = self + self.tableView.delegate = self + self.tableView.sectionHeaderHeight = .leastNormalMagnitude + self.tableView.sectionFooterHeight = Layout.interRowSpacing + self.tableView.contentInset.top = Layout.interRowSpacing + self.tableView.register(AllDomainsListTableViewCell.self, forCellReuseIdentifier: CellIdentifiers.myDomain) + self.tableView.register(AllDomainsListActivityIndicatorTableViewCell.self, forCellReuseIdentifier: CellIdentifiers.activityIndicator) + self.tableView.separatorStyle = .none + self.view.addSubview(tableView) + self.view.pinSubviewToAllEdges(tableView) + self.view.backgroundColor = tableView.backgroundColor + } + + private func setupEmptyView() { + self.emptyView.translatesAutoresizingMaskIntoConstraints = false + self.view.addSubview(emptyView) + NSLayoutConstraint.activate([ + self.emptyView.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor), + self.emptyView.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor), + self.emptyView.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor, constant: Length.Padding.double), + self.emptyView.trailingAnchor.constraint(equalTo: view.readableContentGuide.trailingAnchor, constant: -Length.Padding.double) + ]) + } + + /// Force the navigation bar separator to be always visible. + private func setupNavigationBarAppearance() { + let appearance = self.navigationController?.navigationBar.standardAppearance + self.navigationItem.scrollEdgeAppearance = appearance + self.navigationItem.compactScrollEdgeAppearance = appearance + } + + private func setupRefreshControl() { + let action = UIAction { [weak self] action in + guard let self, let refreshControl = action.sender as? UIRefreshControl else { + return + } + self.tableView.sendSubviewToBack(refreshControl) + self.viewModel.loadData() + } + self.refreshControl.addAction(action, for: .valueChanged) + self.tableView.addSubview(refreshControl) + } + + // MARK: - Reacting to State Changes + + private func observeState() { + self.viewModel.$state.sink { [weak self] state in + guard let self else { + return + } + self.state = state + switch state { + case .normal, .loading: + self.refreshControl.endRefreshing() + self.tableView.isHidden = false + self.tableView.reloadData() + case .message(let viewModel): + self.tableView.isHidden = true + self.emptyView.update(with: viewModel) + } + self.emptyView.isHidden = !tableView.isHidden + }.store(in: &cancellable) + } + + // MARK: - Navigation + + private func navigateToAddDomain() { + AllDomainsAddDomainCoordinator.presentAddDomainFlow(in: self) + } + + private func navigateToDomainDetails(with viewModel: Domain) { + guard let navigationController = navigationController else { + self.crashLogger.logMessage("Failed to navigate to Domain Details screen from All Domains screen", level: .error) + return + } + let domain = viewModel.domain + let destination = DomainDetailsWebViewController( + domain: domain.domain, + siteSlug: domain.siteSlug, + type: domain.type, + analyticsSource: Constants.analyticsSource + ) + destination.configureSandboxStore { + navigationController.pushViewController(destination, animated: true) + } + } +} + +// MARK: - UITableViewDataSource + +extension AllDomainsListViewController: UITableViewDataSource, UITableViewDelegate { + + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + // Workaround to change the section height using `tableView.sectionHeaderHeight`. + return UIView() + } + + func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { + // Workaround to change the footer height using `tableView.sectionFooterHeight`. + return UIView() + } + + func numberOfSections(in tableView: UITableView) -> Int { + switch state { + case .normal(let domains): return domains.count + case .loading: return 1 + default: return 0 + } + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return 1 + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + switch state { + case .loading: + return tableView.dequeueReusableCell(withIdentifier: CellIdentifiers.activityIndicator, for: indexPath) + case .normal(let domains): + let domain = domains[indexPath.section] + let cell = tableView.dequeueReusableCell(withIdentifier: CellIdentifiers.myDomain, for: indexPath) as! AllDomainsListTableViewCell + cell.accessoryType = .disclosureIndicator + cell.update(with: domain.row, parent: self) + return cell + default: + return UITableViewCell() + } + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + switch state { + case .normal(let domains): + let domain = domains[indexPath.section] + self.navigateToDomainDetails(with: domain) + default: + break + } + } +} + +// MARK: - UISearchControllerDelegate & UISearchBarDelegate + +extension AllDomainsListViewController: UISearchControllerDelegate, UISearchBarDelegate { + + func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { + self.viewModel.search(searchText) + } + + func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { + self.viewModel.search(nil) + } + + func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) { + WPAnalytics.track(.myDomainsSearchDomainTapped) + } +} diff --git a/WordPress/Classes/ViewRelated/Me/All Domains/Views/DomainPurchaseChoicesView.swift b/WordPress/Classes/ViewRelated/Me/All Domains/Views/DomainPurchaseChoicesView.swift new file mode 100644 index 000000000000..ab8b8268acce --- /dev/null +++ b/WordPress/Classes/ViewRelated/Me/All Domains/Views/DomainPurchaseChoicesView.swift @@ -0,0 +1,218 @@ +import SwiftUI +import DesignSystem + +class DomainPurchaseChoicesViewModel: ObservableObject { + @Published var isGetDomainLoading: Bool = false +} + +struct DomainPurchaseChoicesView: View { + private enum Constants { + static let imageLength: CGFloat = 36 + } + + + @StateObject var viewModel = DomainPurchaseChoicesViewModel() + + let analyticsSource: String? + + let buyDomainAction: (() -> Void) + let chooseSiteAction: (() -> Void) + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: Length.Padding.single) { + Text(Strings.header) + .font(.largeTitle.bold()) + Text(Strings.subheader) + .foregroundStyle(Color.DS.Foreground.secondary) + .padding(.bottom, Length.Padding.medium) + getDomainCard + .padding(.bottom, Length.Padding.medium) + chooseSiteCard + .padding(.bottom, Length.Padding.single) + Text(Strings.footnote) + .foregroundStyle(Color.DS.Foreground.secondary) + .font(.subheadline) + Spacer() + } + .padding(.top, Length.Padding.double) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, Length.Padding.double) + .background(Color.DS.Background.primary) + .onAppear { + self.track(.purchaseDomainScreenShown) + } + } + + private var getDomainCard: some View { + card( + imageName: "site-menu-domains", + title: Strings.buyDomainTitle, + subtitle: Strings.buyDomainSubtitle, + buttonTitle: Strings.buyDomainButtonTitle, + isProgressViewActive: true + ) { + self.track(.purchaseDomainGetDomainTapped) + self.buyDomainAction() + } + } + + private var chooseSiteCard: some View { + card( + imageName: "block-layout", + title: Strings.chooseSiteTitle, + subtitle: Strings.chooseSiteSubtitle, + buttonTitle: Strings.chooseSiteButtonTitle, + footer: Strings.chooseSiteFooter, + isProgressViewActive: false + ) { + self.track(.purchaseDomainChooseSiteTapped) + self.chooseSiteAction() + } + } + + private func card( + imageName: String, + title: String, + subtitle: String, + buttonTitle: String, + footer: String? = nil, + isProgressViewActive: Bool, + action: @escaping () -> Void + ) -> some View { + VStack(alignment: .leading, spacing: Length.Padding.single) { + Group { + Image(imageName) + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.DS.Background.brand(isJetpack: AppConfiguration.isJetpack)) + .frame(width: Constants.imageLength, height: Constants.imageLength) + .padding(.top, Length.Padding.double) + VStack(alignment: .leading, spacing: Length.Padding.single) { + Text(title) + .font(.title2.bold()) + Text(subtitle) + .foregroundStyle(Color.DS.Foreground.secondary) + if let footer { + Text(footer) + .foregroundStyle(Color.DS.Foreground.brand(isJetpack: AppConfiguration.isJetpack)) + .font(.body.bold()) + } + } + .padding(.bottom, Length.Padding.single) + DSButton( + title: buttonTitle, + style: .init(emphasis: .primary, size: .large, isJetpack: AppConfiguration.isJetpack), + isLoading: isProgressViewActive ? $viewModel.isGetDomainLoading : .constant(false), + action: action + ) + .padding(.bottom, Length.Padding.double) + .disabled(viewModel.isGetDomainLoading) + } + .padding(.horizontal, Length.Padding.double) + } + .background(Color.DS.Background.secondary) + .clipShape(RoundedRectangle(cornerRadius: 8)) + } + + private var chooseSiteTexts: some View { + VStack(alignment: .leading, spacing: Length.Padding.single) { + Text(Strings.chooseSiteTitle) + .font(.title2.bold()) + Text(Strings.chooseSiteSubtitle) + .foregroundStyle(Color.DS.Foreground.secondary) + Text(Strings.chooseSiteFooter) + .foregroundStyle(Color.DS.Foreground.brand(isJetpack: AppConfiguration.isJetpack)) + } + } + + private func track(_ event: WPAnalyticsEvent, properties: [AnyHashable: Any]? = nil) { + let defaultProperties: [AnyHashable: Any] = { + guard let source = self.analyticsSource else { + return [:] + } + return [WPAppAnalyticsKeySource: source] + }() + + let properties = defaultProperties.merging(properties ?? [:]) { first, second in + return first + } + + WPAnalytics.track(event, properties: properties) + } +} + +private extension DomainPurchaseChoicesView { + enum Strings { + static let header = NSLocalizedString( + "domain.management.purchase.title", + value: "Choose how to use your domain", + comment: "Domain management purchase domain screen title." + ) + static let subheader = NSLocalizedString( + "domain.management.purchase.subtitle", + value: "Don't worry, you can easily add a site later.", + comment: "Domain management purchase domain screen title" + ) + + static let buyDomainTitle = NSLocalizedString( + "domain.management.buy.card.title", + value: "Just buy a domain", + comment: "Domain management buy domain card title" + ) + + static let buyDomainSubtitle = NSLocalizedString( + "domain.management.buy.card.subtitle", + value: "Add a site later.", + comment: "Domain management buy domain card subtitle" + ) + + static let buyDomainButtonTitle = NSLocalizedString( + "domain.management.buy.card.button.title", + value: "Get Domain", + comment: "Domain management buy domain card button title" + ) + + static let chooseSiteTitle = NSLocalizedString( + "domain.management.site.card.title", + value: "Existing WordPress.com site", + comment: "Domain management choose site card title" + ) + + static let chooseSiteSubtitle = NSLocalizedString( + "domain.management.site.card.subtitle", + value: "Use with a site you already started.", + comment: "Domain management choose site card subtitle" + ) + + static let chooseSiteFooter = NSLocalizedString( + "domain.management.site.card.footer", + value: "Free domain for the first year*", + comment: "Domain management choose site card subtitle" + ) + + static let chooseSiteButtonTitle = NSLocalizedString( + "domain.management.site.card.button.title", + value: "Choose Site", + comment: "Domain management choose site card button title" + ) + + static let footnote = NSLocalizedString( + "domain.management.purchase.footer", + value: "*A free domain for one year is included with all paid annual plans", + comment: "Domain management choose site card button title" + ) + } +} + +struct DomainPurchaseChoicesView_Previews: PreviewProvider { + static var previews: some View { + DomainPurchaseChoicesView(viewModel: DomainPurchaseChoicesViewModel(), analyticsSource: nil) { + print("Buy domain tapped.") + } chooseSiteAction: { + print("Choose site tapped") + } + .environment(\.colorScheme, .dark) + } +} diff --git a/WordPress/Classes/ViewRelated/Me/App Settings/AppSettingsViewController.swift b/WordPress/Classes/ViewRelated/Me/App Settings/AppSettingsViewController.swift index 1fdd54e614aa..38272bd88e38 100644 --- a/WordPress/Classes/ViewRelated/Me/App Settings/AppSettingsViewController.swift +++ b/WordPress/Classes/ViewRelated/Me/App Settings/AppSettingsViewController.swift @@ -5,13 +5,9 @@ import Gridicons import WordPressShared import SVProgressHUD import WordPressFlux +import DesignSystem class AppSettingsViewController: UITableViewController { - enum Sections: Int { - case media - case other - } - fileprivate var handler: ImmuTableViewHandler! // MARK: - Dependencies @@ -163,12 +159,7 @@ class AppSettingsViewController: UITableViewController { @objc func trackImageSizeChanged() { let value = MediaSettings().maxImageSizeSetting - - var properties = [String: AnyObject]() - properties["enabled"] = (value != Int.max) as AnyObject - properties["value"] = value as Int as AnyObject - - WPAnalytics.track(.appSettingsImageOptimizationChanged, withProperties: properties) + WPAnalytics.track(.appSettingsMaxImageSizeChanged, properties: ["size": value]) } func pushVideoResolutionSettings() -> ImmuTableAction { @@ -179,10 +170,7 @@ class AppSettingsViewController: UITableViewController { MediaSettings.VideoResolution.size3840x2160, MediaSettings.VideoResolution.sizeOriginal] - let titles = values.map({ (settings: MediaSettings.VideoResolution) -> String in - settings.description - }) - + let titles = values.map { $0.description } let currentVideoResolution = MediaSettings().maxVideoSizeSetting let settingsSelectionConfiguration = [SettingsSelectionDefaultValueKey: currentVideoResolution, @@ -206,6 +194,36 @@ class AppSettingsViewController: UITableViewController { } } + func pushImageQualitySettings() -> ImmuTableAction { + return { [weak self] row in + let values = [MediaSettings.ImageQuality.low, + MediaSettings.ImageQuality.medium, + MediaSettings.ImageQuality.high, + MediaSettings.ImageQuality.maximum] + + let titles = values.map { $0.description } + let currentImageQuality = MediaSettings().imageQualitySetting + let title = NSLocalizedString("appSettings.media.imageQuality.title", value: "Quality", comment: "The quality of image used when uploading") + + let settingsSelectionConfiguration = [SettingsSelectionDefaultValueKey: currentImageQuality, + SettingsSelectionTitleKey: title, + SettingsSelectionTitlesKey: titles, + SettingsSelectionValuesKey: values] as [String: Any] + + let viewController = SettingsSelectionViewController(dictionary: settingsSelectionConfiguration) + + viewController?.onItemSelected = { quality in + let newQuality = quality as! MediaSettings.ImageQuality + MediaSettings().imageQualitySetting = newQuality + + // Track setting changes + WPAnalytics.track(.appSettingsImageQualityChanged, properties: ["quality": newQuality.description]) + } + + self?.navigationController?.pushViewController(viewController!, animated: true) + } + } + func openMediaCacheSettings() -> ImmuTableAction { return { [weak self] _ in let controller = MediaCacheSettingsViewController(style: .insetGrouped) @@ -220,6 +238,28 @@ class AppSettingsViewController: UITableViewController { } } + func imageOptimizationChanged() -> (Bool) -> Void { + return { [weak self] value in + MediaSettings().imageOptimizationEnabled = value + + // Track setting changes + WPAnalytics.track(.appSettingsOptimizeImagesChanged, properties: ["enabled": value]) + + // Show/hide image optimization settings + guard let self, let tableView else { + return + } + tableView.performBatchUpdates { + let originalAutomaticallyReloadTableView = self.handler.automaticallyReloadTableView + self.handler.automaticallyReloadTableView = false + self.reloadViewModel() + self.handler.automaticallyReloadTableView = originalAutomaticallyReloadTableView + + tableView.reloadSections(IndexSet(integer: 0), with: .automatic) + } + } + } + func pushAppearanceSettings() -> ImmuTableAction { return { [weak self] row in let values = UIUserInterfaceStyle.allStyles @@ -262,14 +302,14 @@ class AppSettingsViewController: UITableViewController { func pushDebugMenu() -> ImmuTableAction { return { [weak self] row in - let controller = DebugMenuViewController(style: .insetGrouped) + let controller = DebugMenuViewController() self?.navigationController?.pushViewController(controller, animated: true) } } func pushDesignSystemGallery() -> ImmuTableAction { return { [weak self] row in - let controller = UIHostingController(rootView: ColorGallery()) + let controller = UIHostingController(rootView: DesignSystemGallery()) self?.navigationController?.pushViewController(controller, animated: true) } } @@ -395,11 +435,24 @@ private extension AppSettingsViewController { func mediaTableSection() -> ImmuTableSection { let mediaHeader = NSLocalizedString("Media", comment: "Title label for the media settings section in the app settings") + let imageOptimizationValue = MediaSettings().imageOptimizationEnabled + let imageOptimization = SwitchRow( + title: NSLocalizedString("appSettings.media.imageOptimizationRow", value: "Optimize Images", comment: "Option to enable the optimization of images when uploading."), + value: imageOptimizationValue, + onChange: imageOptimizationChanged(), + accessibilityIdentifier: "imageOptimizationSwitch" + ) + let imageSizingRow = ImageSizingRow( title: NSLocalizedString("Max Image Upload Size", comment: "Title for the image size settings option."), value: Int(MediaSettings().maxImageSizeSetting), onChange: imageSizeChanged()) + let imageQualityRow = NavigationItemRow( + title: NSLocalizedString("appSettings.media.imageQualityRow", value: "Image Quality", comment: "Title for the image quality settings option."), + detail: MediaSettings().imageQualitySetting.description, + action: pushImageQualitySettings()) + let videoSizingRow = NavigationItemRow( title: NSLocalizedString("Max Video Upload Size", comment: "Title for the video size settings option."), detail: MediaSettings().maxVideoSizeSetting.description, @@ -410,13 +463,21 @@ private extension AppSettingsViewController { detail: mediaCacheRowDescription, action: openMediaCacheSettings()) + let rows: [ImmuTableRow] = imageOptimizationValue ? [ + imageOptimization, + imageSizingRow, + imageQualityRow, + videoSizingRow, + mediaCacheRow + ] : [ + imageOptimization, + videoSizingRow, + mediaCacheRow + ] + return ImmuTableSection( headerText: mediaHeader, - rows: [ - imageSizingRow, - videoSizingRow, - mediaCacheRow - ], + rows: rows, footerText: NSLocalizedString("Free up storage space on this device by deleting temporary media files. This will not affect the media on your site.", comment: "Explanatory text for clearing device media cache.") ) @@ -474,6 +535,7 @@ private extension AppSettingsViewController { let designSystem = NavigationItemRow( title: NSLocalizedString("Design System", comment: "Navigates to design system gallery only available in development builds"), + icon: UIImage(systemName: "paintpalette"), action: pushDesignSystemGallery() ) diff --git a/WordPress/Classes/ViewRelated/Me/App Settings/DebugFeatureFlagsView.swift b/WordPress/Classes/ViewRelated/Me/App Settings/DebugFeatureFlagsView.swift index 627e6fa80c91..261e11c53f7c 100644 --- a/WordPress/Classes/ViewRelated/Me/App Settings/DebugFeatureFlagsView.swift +++ b/WordPress/Classes/ViewRelated/Me/App Settings/DebugFeatureFlagsView.swift @@ -9,7 +9,7 @@ struct DebugFeatureFlagsView: View { } .tint(Color(UIColor.jetpackGreen)) .listStyle(.grouped) - .searchable(text: $viewModel.filterTerm) + .searchable(text: $viewModel.filterTerm, placement: .navigationBarDrawer(displayMode: .always)) .navigationTitle(navigationTitle) .navigationBarTitleDisplayMode(.inline) .apply(addToolbarTitleMenu) diff --git a/WordPress/Classes/ViewRelated/Me/App Settings/DebugMenuViewController.swift b/WordPress/Classes/ViewRelated/Me/App Settings/DebugMenuViewController.swift index 6503c66220d6..a6d8cba7755b 100644 --- a/WordPress/Classes/ViewRelated/Me/App Settings/DebugMenuViewController.swift +++ b/WordPress/Classes/ViewRelated/Me/App Settings/DebugMenuViewController.swift @@ -1,203 +1,221 @@ import UIKit import AutomatticTracks import SwiftUI +import WordPressFlux -class DebugMenuViewController: UITableViewController { - private var handler: ImmuTableViewHandler! +struct DebugMenuView: View { + @StateObject private var viewModel = DebugMenuViewModel() - override init(style: UITableView.Style) { - super.init(style: style) + fileprivate var navigation: NavigationContext - title = NSLocalizedString("Debug Settings", comment: "Debug settings title") - } - - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - required convenience init() { - self.init(style: .grouped) - } - - override func viewDidLoad() { - super.viewDidLoad() - - ImmuTable.registerRows([ - SwitchWithSubtitleRow.self, - ButtonRow.self, - EditableTextRow.self - ], tableView: tableView) - - handler = ImmuTableViewHandler(takeOver: self) - reloadViewModel() - - WPStyleGuide.configureColors(view: view, tableView: tableView) - WPStyleGuide.configureAutomaticHeightRows(for: tableView) + var body: some View { + List { + Section { main } + Section(Strings.sectionSettings) { settings } + if let blog = viewModel.blog { + Section(Strings.sectionQuickStart) { makeQuickStart(with: blog) } + } + Section(Strings.sectionLogging) { logging } + } + .toolbar { + ToolbarItem(placement: .principal) { + (Text(Image(systemName: "bolt.fill")).foregroundColor(.yellow) + Text(" " + Strings.title)).font(.headline) + } + } } - private func reloadViewModel() { - handler.viewModel = ImmuTable(sections: [ - ImmuTableSection(headerText: Strings.general, rows: generalRows), - ImmuTableSection(headerText: Strings.tools, rows: toolsRows), - ImmuTableSection(headerText: Strings.crashLogging, rows: crashLoggingRows), - ImmuTableSection(headerText: Strings.reader, rows: readerRows), - ]) + @ViewBuilder private var main: some View { + NavigationLink { + DebugFeatureFlagsView() + } label: { + DebugMenuRow(systemImage: "flag.fill", color: .pink, title: Strings.featureFlags) + } } - // MARK: Tools - - private var generalRows: [ImmuTableRow] { - [ - NavigationItemRow(title: Strings.featureFlags) { [weak self] _ in - let vc = UIHostingController(rootView: DebugFeatureFlagsView()) - self?.navigationController?.pushViewController(vc, animated: true) - } - ] + @ViewBuilder private var settings: some View { + NavigationLink(Strings.remoteConfigTitle) { + RemoteConfigDebugView() + } + NavigationLink(Strings.sandboxStoreCookieSecretRow) { + StoreSandboxSecretScreen(cookieJar: HTTPCookieStorage.shared) + } + NavigationLink(Strings.weeklyRoundup) { + WeeklyRoundupDebugScreen() + } + NavigationLink(Strings.readerCssTitle) { + readerSettings + } } - private var toolsRows: [ImmuTableRow] { - var toolsRows = [ - ButtonRow(title: Strings.quickStartForNewSiteRow, action: { [weak self] _ in - self?.displayBlogPickerForQuickStart(type: .newSite) - }), - ButtonRow(title: Strings.quickStartForExistingSiteRow, action: { [weak self] _ in - self?.displayBlogPickerForQuickStart(type: .existingSite) - }), - ButtonRow(title: Strings.removeQuickStartRow, action: { [weak self] _ in - if let blog = RootViewCoordinator.sharedPresenter.mySitesCoordinator.currentBlog { - QuickStartTourGuide.shared.remove(from: blog) - } - self?.tableView.deselectSelectedRowWithAnimationAfterDelay(true) - }), - ButtonRow(title: Strings.sandboxStoreCookieSecretRow, action: { [weak self] _ in - self?.displayStoreSandboxSecretInserter() - }), - ButtonRow(title: Strings.remoteConfigTitle, action: { [weak self] _ in - self?.displayRemoteConfigDebugMenu() - }), - ] - - toolsRows.append(ButtonRow(title: "Weekly Roundup", action: { [weak self] _ in - self?.displayWeeklyRoundupDebugTools() - })) - - return toolsRows + @ViewBuilder private func makeQuickStart(with blog: Blog) -> some View { + Button(Strings.quickStartForNewSiteRow) { + QuickStartTourGuide.shared.setup(for: blog, type: .newSite) + viewModel.objectWillChange.send() // Refresh + showSuccessNotice() + } + Button(Strings.quickStartForExistingSiteRow) { + QuickStartTourGuide.shared.setup(for: blog, type: .existingSite) + viewModel.objectWillChange.send() // Refresh + showSuccessNotice() + } + Button(Strings.removeQuickStartRow, role: .destructive) { + QuickStartTourGuide.shared.remove(from: blog) + viewModel.objectWillChange.send() // Refresh + showSuccessNotice() + }.disabled(blog.quickStartType == .undefined) } - // MARK: Crash Logging - - private var crashLoggingRows: [ImmuTableRow] { - - var rows: [ImmuTableRow] = [ - ButtonRow(title: Strings.sendLogMessage, action: { _ in - WordPressAppDelegate.crashLogging?.logMessage("Debug Log Message \(UUID().uuidString)") - self.tableView.deselectSelectedRowWithAnimationAfterDelay(true) - }), - ButtonRow(title: Strings.sendTestCrash, action: { _ in - DDLogInfo("Initiating user-requested crash") - WordPressAppDelegate.crashLogging?.crash() - }) - ] - + @ViewBuilder private var logging: some View { + Button(Strings.sendLogMessage) { + WordPressAppDelegate.crashLogging?.logMessage("Debug Log Message \(UUID().uuidString)") + showSuccessNotice() + } + Button(Strings.sendTestCrash) { + DDLogInfo("Initiating user-requested crash") + WordPressAppDelegate.crashLogging?.crash() + } if let eventLogging = WordPressAppDelegate.eventLogging { - let tableViewController = EncryptedLogTableViewController(eventLogging: eventLogging) - let encryptedLoggingRow = ButtonRow(title: Strings.encryptedLogging) { _ in - self.navigationController?.pushViewController(tableViewController, animated: true) - } - rows.append(encryptedLoggingRow) + let viewController = EncryptedLogTableViewController(eventLogging: eventLogging) + Button { + navigation.push(viewController) + } label: { + HStack { + Text(Strings.encryptedLogging) + Spacer() + Image(systemName: "chevron.right") + .font(.subheadline.weight(.semibold)) + .foregroundStyle(.secondary.opacity(0.5)) + } + .contentShape(Rectangle()) + }.buttonStyle(.plain) } + Toggle(Strings.alwaysSendLogs, isOn: $viewModel.isForcedCrashLoggingEnabled) + } - let alwaysSendLogsRow = SwitchWithSubtitleRow(title: Strings.alwaysSendLogs, value: UserSettings.userHasForcedCrashLoggingEnabled) { isOn in - UserSettings.userHasForcedCrashLoggingEnabled = isOn + private var readerSettings: some View { + let viewController = SettingsTextViewController(text: ReaderCSS().customAddress, placeholder: Strings.readerURLPlaceholder, hint: Strings.readerURLHint) + viewController.title = Strings.readerCssTitle + viewController.onAttributedValueChanged = { + var reader = ReaderCSS() + reader.customAddress = $0.string } + return Wrapped(viewController: viewController) + } +} - rows.append(alwaysSendLogsRow) +private func showSuccessNotice() { + let notice = Notice(title: "✅", feedbackType: .success) + ActionDispatcher.dispatch(NoticeAction.post(notice)) +} - return rows +private final class DebugMenuViewModel: ObservableObject { + var blog: Blog? { + RootViewCoordinator.sharedPresenter.mySitesCoordinator.currentBlog } - private func displayBlogPickerForQuickStart(type: QuickStartType) { - let successHandler: BlogSelectorSuccessHandler = { [weak self] selectedObjectID in - guard let blog = ContextManager.shared.mainContext.object(with: selectedObjectID) as? Blog else { - return - } - - self?.dismiss(animated: true) { [weak self] in - self?.enableQuickStart(for: blog, type: type) - } + var isForcedCrashLoggingEnabled: Bool { + get { UserSettings.userHasForcedCrashLoggingEnabled } + set { + UserSettings.userHasForcedCrashLoggingEnabled = newValue + objectWillChange.send() } + } +} - let selectorViewController = BlogSelectorViewController(selectedBlogObjectID: nil, - successHandler: successHandler, - dismissHandler: nil) - - selectorViewController.displaysNavigationBarWhenSearching = WPDeviceIdentification.isiPad() - selectorViewController.dismissOnCancellation = true - selectorViewController.displaysOnlyDefaultAccountSites = true - - let navigationController = UINavigationController(rootViewController: selectorViewController) - present(navigationController, animated: true) +private struct DebugMenuRow: View { + let systemImage: String + let color: Color + let title: String + + var body: some View { + HStack { + Image(systemName: systemImage) + .font(.system(size: 14)) + .foregroundStyle(.white) + .frame(width: 26, height: 26, alignment: .center) + .background(color) + .clipShape(RoundedRectangle(cornerRadius: 6, style: .continuous)) + Text(title) + Spacer() + } } +} - private func displayStoreSandboxSecretInserter() { - let view = StoreSandboxSecretScreen(cookieJar: HTTPCookieStorage.shared) - let viewController = UIHostingController(rootView: view) +final class DebugMenuViewController: UIHostingController { + init() { + let navigation = NavigationContext() + super.init(rootView: DebugMenuView(navigation: navigation)) + navigation.parentViewController = self + } - self.navigationController?.pushViewController(viewController, animated: true) + @MainActor required dynamic init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") } - private func displayWeeklyRoundupDebugTools() { - let view = WeeklyRoundupDebugScreen() - let viewController = UIHostingController(rootView: view) + static func configure(in window: UIWindow?) { + guard FeatureFlag.debugMenu.enabled else { + return + } + assert(window != nil) - self.navigationController?.pushViewController(viewController, animated: true) + let gesture = UIScreenEdgePanGestureRecognizer(target: DebugMenuViewController.self, action: #selector(showDebugMenu)) + gesture.edges = .right + window?.addGestureRecognizer(gesture) } - private func enableQuickStart(for blog: Blog, type: QuickStartType) { - QuickStartTourGuide.shared.setup(for: blog, type: type) + @objc private static func showDebugMenu() { + guard let window = UIApplication.sharedIfAvailable()?.mainWindow, + let topViewController = window.topmostPresentedViewController, + !((topViewController as? UINavigationController)?.viewControllers.first is DebugMenuViewController) else { + return + } + let viewController = DebugMenuViewController() + viewController.navigationItem.leftBarButtonItem = UIBarButtonItem(systemItem: .close, primaryAction: .init { [weak topViewController] _ in + topViewController?.dismiss(animated: true) + }) + viewController.configureDefaultNavigationBarAppearance() + + let navigation = UINavigationController(rootViewController: viewController) + navigation.navigationBar.isTranslucent = true // Reset to default + topViewController.present(navigation, animated: true) } +} - private func displayRemoteConfigDebugMenu() { - let viewController = RemoteConfigDebugViewController() - self.navigationController?.pushViewController(viewController, animated: true) +private final class NavigationContext { + weak var parentViewController: UIViewController? + + /// The alternative solution of using `UIViewControllerRepresentable` won't + /// work well without a convoluted way to pass navigaiton items. + func push(_ viewController: UIViewController) { + parentViewController?.navigationController?.pushViewController(viewController, animated: true) } +} - // MARK: Reader - - private var readerRows: [ImmuTableRow] { - return [ - EditableTextRow(title: Strings.readerCssTitle, value: ReaderCSS().customAddress ?? "") { row in - let textViewController = SettingsTextViewController(text: ReaderCSS().customAddress, placeholder: Strings.readerURLPlaceholder, hint: Strings.readerURLHint) - textViewController.title = Strings.readerCssTitle - textViewController.onAttributedValueChanged = { [weak self] url in - var readerCSS = ReaderCSS() - readerCSS.customAddress = url.string - self?.reloadViewModel() - } +private struct Wrapped: UIViewControllerRepresentable { + let viewController: T - self.navigationController?.pushViewController(textViewController, animated: true) - } - ] - } + func makeUIViewController(context: Context) -> T { viewController } + func updateUIViewController(_ viewController: T, context: Context) {} +} - enum Strings { - static let tools = NSLocalizedString("Tools", comment: "Title of the Tools section of the debug screen used in debug builds of the app") - static let sandboxStoreCookieSecretRow = NSLocalizedString("Use Sandbox Store", comment: "Title of a row displayed on the debug screen used to configure the sandbox store use in the App.") - static let quickStartForNewSiteRow = NSLocalizedString("Enable Quick Start for New Site", comment: "Title of a row displayed on the debug screen used in debug builds of the app") - static let quickStartForExistingSiteRow = NSLocalizedString("Enable Quick Start for Existing Site", comment: "Title of a row displayed on the debug screen used in debug builds of the app") - static let sendTestCrash = NSLocalizedString("Send Test Crash", comment: "Title of a row displayed on the debug screen used to crash the app and send a crash report to the crash logging provider to ensure everything is working correctly") - static let sendLogMessage = NSLocalizedString("Send Log Message", comment: "Title of a row displayed on the debug screen used to send a pretend error message to the crash logging provider to ensure everything is working correctly") - static let alwaysSendLogs = NSLocalizedString("Always Send Crash Logs", comment: "Title of a row displayed on the debug screen used to indicate whether crash logs should be forced to send, even if they otherwise wouldn't") - static let crashLogging = NSLocalizedString("Crash Logging", comment: "Title of a section on the debug screen that shows a list of actions related to crash logging") - static let encryptedLogging = NSLocalizedString("Encrypted Logs", comment: "Title of a row displayed on the debug screen used to display a screen that shows a list of encrypted logs") - static let reader = NSLocalizedString("Reader", comment: "Title of the Reader section of the debug screen used in debug builds of the app") - static let readerCssTitle = NSLocalizedString("Reader CSS URL", comment: "Title of the screen that allows the user to change the Reader CSS URL for debug builds") - static let readerURLPlaceholder = NSLocalizedString("Default URL", comment: "Placeholder for the reader CSS URL") - static let readerURLHint = NSLocalizedString("Add a custom CSS URL here to be loaded in Reader. If you're running Calypso locally this can be something like: http://192.168.15.23:3000/calypso/reader-mobile.css", comment: "Hint for the reader CSS URL field") - static let remoteConfigTitle = NSLocalizedString("debugMenu.remoteConfig.title", value: "Remote Config", comment: "Remote Config debug menu title") - static let general = NSLocalizedString("debugMenu.generalSectionTitle", value: "General", comment: "General section title") - static let featureFlags = NSLocalizedString("debugMenu.featureFlags", value: "Feature Flags", comment: "Feature flags menu item") - static let removeQuickStartRow = NSLocalizedString("debugMenu.removeQuickStart", value: "Remove Current Tour", comment: "Remove current quick start tour menu item") - } +private enum Strings { + static let title = NSLocalizedString("debugMenu.title", value: "Developer", comment: "Title for debug menu screen") + static let sectionSettings = NSLocalizedString("debugMenu.section.settings", value: "Settings", comment: "Debug Menu section title") + static let sectionLogging = NSLocalizedString("debugMenu.section.logging", value: "Logging", comment: "Debug Menu section title") + static let sectionQuickStart = NSLocalizedString("debugMenu.section.quickStart", value: "Quick Start", comment: "Debug Menu section title") + static let sandboxStoreCookieSecretRow = NSLocalizedString("Sandbox Store", comment: "Title of a row displayed on the debug screen used to configure the sandbox store use in the App.") + static let quickStartForNewSiteRow = NSLocalizedString("Enable Quick Start for New Site", comment: "Title of a row displayed on the debug screen used in debug builds of the app") + static let quickStartForExistingSiteRow = NSLocalizedString("Enable Quick Start for Existing Site", comment: "Title of a row displayed on the debug screen used in debug builds of the app") + static let sendTestCrash = NSLocalizedString("Send Test Crash", comment: "Title of a row displayed on the debug screen used to crash the app and send a crash report to the crash logging provider to ensure everything is working correctly") + static let sendLogMessage = NSLocalizedString("Send Log Message", comment: "Title of a row displayed on the debug screen used to send a pretend error message to the crash logging provider to ensure everything is working correctly") + static let alwaysSendLogs = NSLocalizedString("Always Send Crash Logs", comment: "Title of a row displayed on the debug screen used to indicate whether crash logs should be forced to send, even if they otherwise wouldn't") + static let encryptedLogging = NSLocalizedString("Encrypted Logs", comment: "Title of a row displayed on the debug screen used to display a screen that shows a list of encrypted logs") + static let readerCssTitle = NSLocalizedString("debugMenu.readerCellTitle", value: "Reader CSS URL", comment: "Title of the screen that allows the user to change the Reader CSS URL for debug builds") + static let readerURLPlaceholder = NSLocalizedString("debugMenu.readerDefaultURL", value: "Default URL", comment: "Placeholder for the reader CSS URL") + static let readerURLHint = NSLocalizedString("debugMenu.readerHit", value: "Add a custom CSS URL here to be loaded in Reader. If you're running Calypso locally this can be something like: http://192.168.15.23:3000/calypso/reader-mobile.css", comment: "Hint for the reader CSS URL field") + static let remoteConfigTitle = NSLocalizedString("debugMenu.remoteConfig.title", value: "Remote Config", comment: "Remote Config debug menu title") + static let analyics = NSLocalizedString("debugMenu.analytics", value: "Analytics", comment: "Debug menu item title") + static let featureFlags = NSLocalizedString("debugMenu.featureFlags", value: "Feature Flags", comment: "Feature flags menu item") + static let removeQuickStartRow = NSLocalizedString("debugMenu.removeQuickStart", value: "Remove Current Tour", comment: "Remove current quick start tour menu item") + static let weeklyRoundup = NSLocalizedString("debugMenu.weeklyRoundup", value: "Weekly Roundup", comment: "Weekly Roundup debug menu item") } diff --git a/WordPress/Classes/ViewRelated/Me/App Settings/RemoteConfigDebugView.swift b/WordPress/Classes/ViewRelated/Me/App Settings/RemoteConfigDebugView.swift new file mode 100644 index 000000000000..cf29954e767c --- /dev/null +++ b/WordPress/Classes/ViewRelated/Me/App Settings/RemoteConfigDebugView.swift @@ -0,0 +1,154 @@ +import SwiftUI + +struct RemoteConfigDebugView: View { + @StateObject private var viewModel = RemoteConfigDebugViewModel() + + var body: some View { + List { + listContent + } + .listStyle(.plain) + .searchable(text: $viewModel.searchText, placement: .navigationBarDrawer(displayMode: .always)) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button(Strings.reset, action: viewModel.resetAll) + } + } + .navigationTitle(Strings.title) + } + + @ViewBuilder private var listContent: some View { + ForEach(viewModel.parameters, id: \.self) { parameter in + let (value, isOverriden) = viewModel.getValue(for: parameter) + NavigationLink { + RemoteConfigEditorView(viewModel: viewModel, parameter: parameter) + } label: { + RemoteConfigDebugRow(title: parameter.description, value: value ?? "–", isOverriden: isOverriden) + } + } + } +} + +private struct RemoteConfigDebugRow: View { + let title: String + let value: String + var isOverriden = false + + var body: some View { + HStack { + VStack(alignment: .leading, spacing: 4) { + HStack { + Text(title) + if isOverriden { + Circle() + .frame(width: 8, height: 8) + .foregroundStyle(.blue) + } + } + Text(value) + .font(.caption) + .foregroundStyle(.secondary) + } + } + } +} + +private struct RemoteConfigEditorView: View { + @ObservedObject var viewModel: RemoteConfigDebugViewModel + let parameter: RemoteConfigParameter + + @FocusState private var isTextFieldFocused: Bool + private var store: RemoteConfigStore { viewModel.store } + + var body: some View { + List { + let binding = viewModel.binding(for: parameter) + Section(Strings.overridenValue) { + TextField(viewModel.getOriginalValue(for: parameter) ?? "", text: binding) + .focused($isTextFieldFocused, equals: true) + Button(Strings.reset, role: .destructive) { + viewModel.reset(parameter) + }.disabled(binding.wrappedValue.isEmpty) + } + Section { + let value = store.value(for: parameter.key).map { String(describing: $0) } + RemoteConfigDebugRow(title: Strings.currentValue, value: viewModel.getValue(for: parameter).value ?? "–") + RemoteConfigDebugRow(title: Strings.remoteConfigValue, value: value ?? "–") + RemoteConfigDebugRow(title: Strings.defaultValue, value: parameter.defaultValue?.description ?? "–") + } + } + .onAppear { isTextFieldFocused = true } + .navigationTitle(parameter.description) + } +} + +private final class RemoteConfigDebugViewModel: ObservableObject { + @Published var parameters: [RemoteConfigParameter] = [] + @Published var searchText = "" { + didSet { reload() } + } + + let store = RemoteConfigStore() + let overrideStore = RemoteConfigOverrideStore() + + init() { + reload() + } + + func binding(for parameter: RemoteConfigParameter) -> Binding { + Binding(get: { + self.overrideStore.overriddenValue(for: parameter) ?? "" + }, set: { + if $0.isEmpty { + self.overrideStore.reset(parameter) + } else { + self.overrideStore.override(parameter, withValue: $0) + } + self.objectWillChange.send() + }) + } + + func getOriginalValue(for parameter: RemoteConfigParameter) -> String? { + parameter.originalValue(using: store).map { String(describing: $0) } + } + + func getValue(for parameter: RemoteConfigParameter) -> (value: String?, isOverriden: Bool) { + let overridenValue = overrideStore.overriddenValue(for: parameter) + let value = overridenValue ?? getOriginalValue(for: parameter) + return (value, overridenValue != nil) + } + + func reset(_ parameter: RemoteConfigParameter) { + overrideStore.reset(parameter) + objectWillChange.send() + } + + func resetAll() { + RemoteConfigParameter.allCases.forEach(reset) + } + + private func reload() { + parameters = RemoteConfigParameter.allCases.filter { + guard !searchText.isEmpty else { return true } + return $0.description.localizedCaseInsensitiveContains(searchText) + } + } +} + +private enum Strings { + static let title = NSLocalizedString("debugMenu.remoteConfig.title", value: "Remote Config", comment: "Remote Config Debug Menu screen title") + static let reset = NSLocalizedString("debugMenu.remoteConfig.reset", value: "Reset", comment: "Remote Config Debug Menu reset button title") + static let overridenValue = NSLocalizedString("debugMenu.remoteConfig.overridenValue", value: "Remote Config", comment: "Remote Config Debug Menu section title") + static let currentValue = NSLocalizedString("debugMenu.remoteConfig.currentValue", value: "Current Value", comment: "Remote Config Debug Menu section title") + static let remoteConfigValue = NSLocalizedString("debugMenu.remoteConfig.remoteConfigValue", value: "Remote Config Value", comment: "Remote Config Debug Menu section title") + static let defaultValue = NSLocalizedString("debugMenu.remoteConfig.defaultValue", value: "Default Value", comment: "Remote Config Debug Menu section title") +} + +private extension RemoteConfigParameter { + func originalValue(using store: RemoteConfigStore = .init()) -> Any? { + if let value = store.value(for: key) { + return value + } + return defaultValue + } +} diff --git a/WordPress/Classes/ViewRelated/Me/App Settings/RemoteConfigDebugViewController.swift b/WordPress/Classes/ViewRelated/Me/App Settings/RemoteConfigDebugViewController.swift deleted file mode 100644 index 7a8775625c35..000000000000 --- a/WordPress/Classes/ViewRelated/Me/App Settings/RemoteConfigDebugViewController.swift +++ /dev/null @@ -1,124 +0,0 @@ -import UIKit - -class RemoteConfigDebugViewController: UITableViewController { - - private var handler: ImmuTableViewHandler! - - override init(style: UITableView.Style) { - super.init(style: style) - - title = Strings.title - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - required convenience init() { - self.init(style: .grouped) - } - - override func viewDidLoad() { - super.viewDidLoad() - - ImmuTable.registerRows([ - CheckmarkRow.self - ], tableView: tableView) - - handler = ImmuTableViewHandler(takeOver: self) - reloadViewModel() - - WPStyleGuide.configureColors(view: view, tableView: tableView) - WPStyleGuide.configureAutomaticHeightRows(for: tableView) - } - - private func reloadViewModel() { - let remoteConfigStore = RemoteConfigStore() - let overrideStore = RemoteConfigOverrideStore() - let rows = RemoteConfigParameter.allCases.map({ - makeRemoteConfigParamRow(for: $0, remoteConfigStore: remoteConfigStore, overrideStore: overrideStore) - }) - - handler.viewModel = ImmuTable(sections: [ - ImmuTableSection(rows: rows, footerText: Strings.footer) - ]) - } - - private func makeRemoteConfigParamRow(for param: RemoteConfigParameter, - remoteConfigStore: RemoteConfigStore, - overrideStore: RemoteConfigOverrideStore) -> ImmuTableRow { - var overriddenValueText: String? - var currentValueText: String - var placeholderText: String - var isOverridden = false - - if let originalValue = param.originalValue(using: remoteConfigStore) { - placeholderText = String(describing: originalValue) - currentValueText = String(describing: originalValue) - } - else { - placeholderText = Strings.defaultPlaceholder - currentValueText = "nil" - } - - if let overriddenValue = overrideStore.overriddenValue(for: param) { - overriddenValueText = String(describing: overriddenValue) - currentValueText = String(describing: overriddenValue) - isOverridden = true - } - - return CheckmarkRow(title: param.description, subtitle: currentValueText, checked: isOverridden) { [weak self] row in - self?.displaySettingsTextViewController(for: param, - text: overriddenValueText, - placeholder: placeholderText, - overrideStore: overrideStore) - } - } - - private func displaySettingsTextViewController(for param: RemoteConfigParameter, - text: String?, - placeholder: String, - overrideStore: RemoteConfigOverrideStore) { - let textViewController = SettingsTextViewController(text: text, placeholder: placeholder, hint: Strings.hint) - textViewController.title = param.description - textViewController.mode = .lowerCaseText - textViewController.autocorrectionType = .no - textViewController.onAttributedValueChanged = { [weak self] newValue in - if newValue.string.isEmpty { - overrideStore.reset(param) - } else { - overrideStore.override(param, withValue: newValue.string) - } - self?.reloadViewModel() - } - - self.navigationController?.pushViewController(textViewController, animated: true) - } - -} - -private extension RemoteConfigDebugViewController { - enum Strings { - static let title = NSLocalizedString("debugMenu.remoteConfig.title", - value: "Remote Config", - comment: "Remote Config debug menu title") - static let defaultPlaceholder = NSLocalizedString("debugMenu.remoteConfig.placeholder", - value: "No remote or default value", - comment: "Placeholder for overriding remote config params") - static let hint = NSLocalizedString("debugMenu.remoteConfig.hint", - value: "Override the chosen param by defining a new value here.", - comment: "Hint for overriding remote config params") - static let footer = NSLocalizedString("debugMenu.remoteConfig.footer", - value: "Overridden parameters are denoted by a checkmark.", - comment: "Remote config params debug menu footer explaining the meaning of a cell with a checkmark.") - } -} - -private extension RemoteConfigParameter { - func originalValue(using store: RemoteConfigStore = .init()) -> Any? { - if let value = store.value(for: key) { - return value - } - return defaultValue - } -} diff --git a/WordPress/Classes/ViewRelated/Me/Domain Details/DomainDetailsWebViewController.swift b/WordPress/Classes/ViewRelated/Me/Domain Details/DomainDetailsWebViewController.swift new file mode 100644 index 000000000000..e2be93fa1ffb --- /dev/null +++ b/WordPress/Classes/ViewRelated/Me/Domain Details/DomainDetailsWebViewController.swift @@ -0,0 +1,109 @@ +import Foundation + +final class DomainDetailsWebViewController: WebKitViewController { + + // MARK: - Types + + private enum Constants { + static let basePath = "https://wordpress.com" + static let domainsPath = "\(basePath)/domains" + static let manageAllDomainsPath = "\(domainsPath)/manage/all" + } + + // MARK: - Properties + + private let domain: String + + private var observation: NSKeyValueObservation? + + // MARK: - Init + + init(domain: String, siteSlug: String, type: DomainType, analyticsSource: String? = nil) { + self.domain = domain + let url = Self.wpcomDetailsURL(domain: domain, siteSlug: siteSlug, type: type) + let configuration = WebViewControllerConfiguration(url: url) + configuration.customTitle = domain + configuration.analyticsSource = analyticsSource + configuration.secureInteraction = true + configuration.authenticateWithDefaultAccount() + super.init(configuration: configuration) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - View Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + self.observeURL() + self.trackWebViewShownEvent(url: url) + } + + // MARK: - Handling URL Changes + + private func observeURL() { + self.observation = webView.observe(\.url) { [weak self] webView, _ in + guard let self, let url = webView.url else { + return + } + if !self.shouldAllowNavigation(for: url) { + // Open URL in device browser then go back to Domain Management page. + self.open(url) + self.goBack() + } + } + } + + // MARK: - Navigation + + override func goBack() { + if webView.canGoBack { + self.webView.goBack() + } else { + self.popOrDismiss() + } + } + + private func popOrDismiss(animated: Bool = true) { + if let navigationController { + navigationController.popViewController(animated: animated) + } else { + dismiss(animated: animated) + } + } + + // MARK: - Helpers + + private func trackWebViewShownEvent(url: URL?) { + let properties = ["url": url?.absoluteString ?? ""] + WPAnalytics.track(.allDomainsDomainDetailsWebViewShown, properties: properties) + } + + private func shouldAllowNavigation(for url: URL) -> Bool { + return url.absoluteString == self.url?.absoluteString + } + + private func open(_ url: URL) { + UIApplication.shared.open(url) + } + + private static func wpcomDetailsURL(domain: String, siteSlug: String, type: DomainType) -> URL? { + let viewSlug = { + switch type { + case .siteRedirect: return "redirect" + case .transfer: return "transfer/in" + default: return "edit" + } + }() + + let url = "\(Constants.manageAllDomainsPath)/\(domain)/\(viewSlug)/\(siteSlug)" + + if let encodedURL = url.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) { + return URL(string: encodedURL) + } else { + return nil + } + } +} diff --git a/WordPress/Classes/ViewRelated/Me/Me Main/MeViewController.swift b/WordPress/Classes/ViewRelated/Me/Me Main/MeViewController.swift index fe1bec1b4c1b..de16534de8c1 100644 --- a/WordPress/Classes/ViewRelated/Me/Me Main/MeViewController.swift +++ b/WordPress/Classes/ViewRelated/Me/Me Main/MeViewController.swift @@ -182,9 +182,8 @@ class MeViewController: UITableViewController { let shouldShowQRLoginRow = AppConfiguration.qrLoginEnabled && !(account?.settings?.twoStepEnabled ?? false) - return ImmuTable(sections: [ - // first section - .init(rows: { + var sections: [ImmuTableSection] = [ + ImmuTableSection(rows: { var rows: [ImmuTableRow] = [appSettingsRow] if loggedIn { var loggedInRows = [myProfile, accountSettings] @@ -196,9 +195,8 @@ class MeViewController: UITableViewController { } return rows }()), - // middle section - .init(rows: { + ImmuTableSection(rows: { var rows: [ImmuTableRow] = [helpAndSupportIndicator] rows.append(NavigationItemRow(title: ShareAppContentPresenter.RowConstants.buttonTitle, @@ -212,15 +210,36 @@ class MeViewController: UITableViewController { accessoryType: accessoryType, action: pushAbout(), accessibilityIdentifier: "About")) - return rows - }()), + }()) + ] + + #if JETPACK + if RemoteFeatureFlag.domainManagement.enabled() && loggedIn { + sections.append(.init(rows: [ + NavigationItemRow( + title: AllDomainsListViewController.Strings.title, + icon: UIImage(systemName: "globe"), + accessoryType: accessoryType, + action: { action in + self.navigationController?.pushViewController(AllDomainsListViewController(), animated: true) + WPAnalytics.track(.meDomainsTapped) + }, + accessibilityIdentifier: "myDomains" + ) + ]) + ) + } + #endif - // last section + // last section + sections.append( .init(headerText: wordPressComAccount, rows: { return [loggedIn ? logOut : logIn] }()) - ]) + ) + + return ImmuTable(sections: sections) } // MARK: - UITableViewDelegate @@ -346,6 +365,14 @@ class MeViewController: UITableViewController { navigateToTarget(for: RowTitles.accountSettings) } + /// Selects the All Domains row and pushes the All Domains view controller + /// + public func navigateToAllDomains() { + #if JETPACK + navigateToTarget(for: AllDomainsListViewController.Strings.title) + #endif + } + /// Selects the App Settings row and pushes the App Settings view controller /// @objc public func navigateToAppSettings(completion: ((AppSettingsViewController) -> Void)? = nil) { @@ -374,8 +401,8 @@ class MeViewController: UITableViewController { } if let sections = handler?.viewModel.sections, - let section = sections.firstIndex(where: { $0.rows.contains(where: matchRow) }), - let row = sections[section].rows.firstIndex(where: matchRow) { + let section = sections.firstIndex(where: { $0.rows.contains(where: matchRow) }), + let row = sections[section].rows.firstIndex(where: matchRow) { let indexPath = IndexPath(row: row, section: section) tableView.selectRow(at: indexPath, animated: true, scrollPosition: .middle) diff --git a/WordPress/Classes/ViewRelated/Me/My Profile/Change Username/ChangeUsernameViewController.swift b/WordPress/Classes/ViewRelated/Me/My Profile/Change Username/ChangeUsernameViewController.swift index 8dd57cb91311..c01f77f8b44a 100644 --- a/WordPress/Classes/ViewRelated/Me/My Profile/Change Username/ChangeUsernameViewController.swift +++ b/WordPress/Classes/ViewRelated/Me/My Profile/Change Username/ChangeUsernameViewController.swift @@ -207,7 +207,6 @@ private extension ChangeUsernameViewController { enum Constants { static let actionButtonTitle = NSLocalizedString("Save", comment: "Settings Text save button title") static let username = NSLocalizedString("Username", comment: "The header and main title") - static let cellIdentifier = "SearchTableViewCell" enum Alert { static let loading = NSLocalizedString("Loading usernames", comment: "Shown while the app waits for the username suggestions web service to return during the site creation process.") diff --git a/WordPress/Classes/ViewRelated/Me/My Profile/Change Username/View Model/ChangeUsernameViewModel.swift b/WordPress/Classes/ViewRelated/Me/My Profile/Change Username/View Model/ChangeUsernameViewModel.swift index 0042ca4c573a..6d0d4697b122 100644 --- a/WordPress/Classes/ViewRelated/Me/My Profile/Change Username/View Model/ChangeUsernameViewModel.swift +++ b/WordPress/Classes/ViewRelated/Me/My Profile/Change Username/View Model/ChangeUsernameViewModel.swift @@ -128,10 +128,6 @@ private extension ChangeUsernameViewModel { static let highlight = NSLocalizedString("You will not be able to change your username back.", comment: "Paragraph text that needs to be highlighted") static let paragraph = NSLocalizedString("You are about to change your username, which is currently %@. %@", comment: "Paragraph displayed in the tableview header. The placholders are for the current username, highlight text and the current display name.") - enum Suggestions { - static let loading = NSLocalizedString("Loading usernames", comment: "Shown while the app waits for the username suggestions web service to return during the site creation process.") - } - enum Error { static let saveUsername = NSLocalizedString("There was an error saving the username", comment: "Text displayed when there is a failure saving the username.") } diff --git a/WordPress/Classes/ViewRelated/Me/My Profile/Gravatar/AvatarMenuController.swift b/WordPress/Classes/ViewRelated/Me/My Profile/Gravatar/AvatarMenuController.swift index 5b37ddc34bfe..e7c78aaef272 100644 --- a/WordPress/Classes/ViewRelated/Me/My Profile/Gravatar/AvatarMenuController.swift +++ b/WordPress/Classes/ViewRelated/Me/My Profile/Gravatar/AvatarMenuController.swift @@ -80,10 +80,6 @@ final class AvatarMenuController: PHPickerViewControllerDelegate, ImagePickerCon SVProgressHUD.showDismissibleError(withStatus: Strings.errorTitle) } - private func dismiss() { - presentingViewController?.dismiss(animated: true) - } - private var topViewController: UIViewController? { presentingViewController?.topmostPresentedViewController } diff --git a/WordPress/Classes/ViewRelated/Media/CachedAnimatedImageView.swift b/WordPress/Classes/ViewRelated/Media/CachedAnimatedImageView.swift index 13705cf27c7e..5e99342553a1 100644 --- a/WordPress/Classes/ViewRelated/Media/CachedAnimatedImageView.swift +++ b/WordPress/Classes/ViewRelated/Media/CachedAnimatedImageView.swift @@ -52,6 +52,10 @@ public class CachedAnimatedImageView: UIImageView, GIFAnimatable { private var customLoadingIndicator: ActivityIndicatorType? + private var isImageAnimated: Bool { + animatedGifData != nil + } + private lazy var defaultLoadingIndicator: UIActivityIndicatorView = { let loadingIndicator = UIActivityIndicatorView(style: .medium) layoutViewCentered(loadingIndicator, size: nil) @@ -143,7 +147,9 @@ public class CachedAnimatedImageView: UIImageView, GIFAnimatable { } @objc public func prepForReuse() { - self.prepareForReuse() + if isImageAnimated { + self.prepareForReuse() + } } @objc public func startLoadingAnimation() { diff --git a/WordPress/Classes/ViewRelated/Media/CircularProgressView.swift b/WordPress/Classes/ViewRelated/Media/CircularProgressView.swift index a07083334314..d0c2bc161f7e 100644 --- a/WordPress/Classes/ViewRelated/Media/CircularProgressView.swift +++ b/WordPress/Classes/ViewRelated/Media/CircularProgressView.swift @@ -73,8 +73,6 @@ class CircularProgressView: UIView { } } - var errorTintColor = UIColor.white - var state: State = .stopped { didSet { refreshState() @@ -220,7 +218,6 @@ class CircularProgressView: UIView { final class AccessoryView: UIView { enum Appearance { - static let horizontalPadding: CGFloat = 4.0 static let verticalSpacing: CGFloat = 3.0 static let fontSize: CGFloat = 14.0 } diff --git a/WordPress/Classes/ViewRelated/Media/ImageCropOverlayView.swift b/WordPress/Classes/ViewRelated/Media/Crop/ImageCropOverlayView.swift similarity index 100% rename from WordPress/Classes/ViewRelated/Media/ImageCropOverlayView.swift rename to WordPress/Classes/ViewRelated/Media/Crop/ImageCropOverlayView.swift diff --git a/WordPress/Classes/ViewRelated/Media/ImageCropViewController.swift b/WordPress/Classes/ViewRelated/Media/Crop/ImageCropViewController.swift similarity index 100% rename from WordPress/Classes/ViewRelated/Media/ImageCropViewController.swift rename to WordPress/Classes/ViewRelated/Media/Crop/ImageCropViewController.swift diff --git a/WordPress/Classes/ViewRelated/Media/ImageCropViewController.xib b/WordPress/Classes/ViewRelated/Media/Crop/ImageCropViewController.xib similarity index 100% rename from WordPress/Classes/ViewRelated/Media/ImageCropViewController.xib rename to WordPress/Classes/ViewRelated/Media/Crop/ImageCropViewController.xib diff --git a/WordPress/Classes/ViewRelated/Media/DeviceMediaPermissionsHeader.swift b/WordPress/Classes/ViewRelated/Media/DeviceMediaPermissionsHeader.swift deleted file mode 100644 index 89bde5265129..000000000000 --- a/WordPress/Classes/ViewRelated/Media/DeviceMediaPermissionsHeader.swift +++ /dev/null @@ -1,195 +0,0 @@ -import UIKit -import PhotosUI - -/// Displays a notice at the top of a media picker view in the event that the user has only given the app -/// limited photo library permissions. Contains buttons allowing the user to select more images or review their settings. -/// -class DeviceMediaPermissionsHeader: UICollectionReusableView { - - weak var presenter: UIViewController? - - private let label: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.adjustsFontForContentSizeCategory = true - label.setContentHuggingPriority(.defaultLow, for: .vertical) - label.setContentCompressionResistancePriority(.defaultHigh, for: .vertical) - label.text = TextContent.message - label.textColor = .invertedLabel - label.font = .preferredFont(forTextStyle: .subheadline) - label.numberOfLines = 0 - label.lineBreakMode = .byWordWrapping - - return label - }() - - private lazy var selectButton: UIButton = { - let selectButton = UIButton() - configureButton(selectButton) - selectButton.setTitle(TextContent.selectButtonTitle, for: .normal) - selectButton.addTarget(self, action: #selector(selectMoreTapped), for: .touchUpInside) - - return selectButton - }() - - private lazy var settingsButton: UIButton = { - let settingsButton = UIButton() - configureButton(settingsButton) - settingsButton.setTitle(TextContent.settingsButtonTitle, for: .normal) - settingsButton.addTarget(self, action: #selector(changeSettingsTapped), for: .touchUpInside) - - return settingsButton - }() - - private let infoIcon: UIImageView = { - let infoIcon = UIImageView(image: UIImage.gridicon(.info)) - infoIcon.translatesAutoresizingMaskIntoConstraints = false - infoIcon.tintColor = .invertedLabel - return infoIcon - }() - - private let buttonStackView: UIStackView = { - let buttonStackView = UIStackView() - buttonStackView.translatesAutoresizingMaskIntoConstraints = false - buttonStackView.distribution = .fillEqually - buttonStackView.spacing = Metrics.spacing - return buttonStackView - }() - - override init(frame: CGRect) { - super.init(frame: frame) - - commonInit() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - var background: UIView! - - private func commonInit() { - background = UIView() - background.translatesAutoresizingMaskIntoConstraints = false - background.backgroundColor = .invertedSystem5 - addSubview(background) - - background.layer.cornerRadius = Metrics.padding - - let outerStackView = UIStackView() - outerStackView.translatesAutoresizingMaskIntoConstraints = false - outerStackView.axis = .horizontal - outerStackView.alignment = .top - outerStackView.spacing = Metrics.padding - outerStackView.distribution = .fill - background.addSubview(outerStackView) - - let labelButtonsStackView = UIStackView() - labelButtonsStackView.translatesAutoresizingMaskIntoConstraints = false - labelButtonsStackView.axis = .vertical - labelButtonsStackView.alignment = .leading - labelButtonsStackView.distribution = .fillProportionally - labelButtonsStackView.spacing = Metrics.spacing - - outerStackView.addArrangedSubviews([infoIcon, labelButtonsStackView]) - labelButtonsStackView.addArrangedSubviews([label, buttonStackView]) - buttonStackView.addArrangedSubviews([selectButton, settingsButton]) - - activateBackgroundConstraints() - - NSLayoutConstraint.activate([ - outerStackView.leadingAnchor.constraint(equalTo: background.leadingAnchor, constant: Metrics.padding), - outerStackView.trailingAnchor.constraint(equalTo: background.trailingAnchor, constant: -Metrics.padding), - outerStackView.topAnchor.constraint(equalTo: background.topAnchor, constant: Metrics.padding), - outerStackView.bottomAnchor.constraint(equalTo: background.bottomAnchor, constant: -Metrics.padding), - - infoIcon.widthAnchor.constraint(equalTo: infoIcon.heightAnchor), - infoIcon.widthAnchor.constraint(equalToConstant: Metrics.iconSize) - ]) - - configureViewsForContentSizeCategoryChange() - } - - private func configureButton(_ button: UIButton) { - button.translatesAutoresizingMaskIntoConstraints = false - button.titleLabel?.font = .preferredFont(forTextStyle: .subheadline).bold() - button.titleLabel?.lineBreakMode = .byTruncatingTail - button.titleLabel?.adjustsFontForContentSizeCategory = true - button.contentHorizontalAlignment = .leading - button.setTitleColor(.invertedLink, for: .normal) - } - - private func activateBackgroundConstraints() { - NSLayoutConstraint.activate([ - background.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: Metrics.padding), - background.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -Metrics.padding), - background.topAnchor.constraint(equalTo: topAnchor, constant: Metrics.padding), - background.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -Metrics.padding) - ]) - } - - private func configureViewsForContentSizeCategoryChange() { - let isAccessibilityCategory = traitCollection.preferredContentSizeCategory.isAccessibilityCategory - - buttonStackView.axis = isAccessibilityCategory ? .vertical : .horizontal - buttonStackView.spacing = isAccessibilityCategory ? Metrics.spacing / 2.0 : Metrics.spacing - - infoIcon.isHidden = isAccessibilityCategory - } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - configureViewsForContentSizeCategoryChange() - } - - // MARK: - Actions - - @objc private func selectMoreTapped() { - if let presenter = presenter { - PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: presenter) - } - } - - @objc private func changeSettingsTapped() { - if let url = URL(string: UIApplication.openSettingsURLString) { - UIApplication.shared.open(url, options: [:], completionHandler: nil) - } - } - - /// Returns the correct size for the header view, accounting for multi-line labels. - /// We constrain it to the same width as the host view itself, and ask the system for the appropriate size. - func referenceSizeInView(_ view: UIView) -> CGSize { - // We'll work with just the background view, as iOS 14 has issues if we attempt to layout the header view itself. - // We need to remove the background view from the header view while we do our calculations. - background.removeFromSuperview() - - // Constrain the background view to match the width of the parent view - let width = view.frame.size.width - (Metrics.padding * 2) - let widthConstraint = background.widthAnchor.constraint(equalToConstant: width) - widthConstraint.isActive = true - background.layoutIfNeeded() - - // Ask the system to calculate the correct height - let size = background.systemLayoutSizeFitting(CGSize(width: width, height: UIView.layoutFittingCompressedSize.height)) - - // Put everything back how we found it - widthConstraint.isActive = false - addSubview(background) - activateBackgroundConstraints() - - return CGSize(width: size.width, height: size.height + (Metrics.padding * 2)) - } - - private enum Metrics { - static let padding: CGFloat = 8.0 - static let spacing: CGFloat = 16.0 - static let iconSize: CGFloat = 22.0 - } - - private enum TextContent { - static let message = NSLocalizedString("Only the selected photos you've given access to are available.", comment: "Message telling the user that they've only enabled limited photo library permissions for the app.") - static let selectButtonTitle = NSLocalizedString("Select More", comment: "Title of button that allows the user to select more photos to access within the app") - static let settingsButtonTitle = NSLocalizedString("Change Settings", comment: "Title of button that takes user to the system Settings section for the app") - } -} diff --git a/WordPress/Classes/ViewRelated/Media/External/ExternalMediaDataSource.swift b/WordPress/Classes/ViewRelated/Media/External/ExternalMediaDataSource.swift new file mode 100644 index 000000000000..48a89f538fb5 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Media/External/ExternalMediaDataSource.swift @@ -0,0 +1,20 @@ +import Foundation + +/// A media asset protocol used to export media from external sources +/// +protocol ExternalMediaAsset: AnyObject, ExportableAsset { + var id: String { get } + var thumbnailURL: URL { get } + var largeURL: URL { get } + var name: String { get } + var caption: String { get } +} + +protocol ExternalMediaDataSource: AnyObject { + var assets: [ExternalMediaAsset] { get } + var onUpdatedAssets: (() -> Void)? { get set } + var onStartLoading: (() -> Void)? { get set } + var onStopLoading: (() -> Void)? { get set } + func search(for searchTerm: String) + func loadMore() +} diff --git a/WordPress/Classes/ViewRelated/Media/External/ExternalMediaPickerCollectionCell.swift b/WordPress/Classes/ViewRelated/Media/External/ExternalMediaPickerCollectionCell.swift new file mode 100644 index 000000000000..1f4cf32597b3 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Media/External/ExternalMediaPickerCollectionCell.swift @@ -0,0 +1,51 @@ +import UIKit + +final class ExternalMediaPickerCollectionCell: UICollectionViewCell { + private let imageView = ImageView() + private var selectionView: SiteMediaCollectionCellSelectionOverlayView? + + override init(frame: CGRect) { + super.init(frame: frame) + + addSubview(imageView) + imageView.translatesAutoresizingMaskIntoConstraints = false + pinSubviewToAllEdges(imageView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func prepareForReuse() { + super.prepareForReuse() + + imageView.prepareForReuse() + } + + func configure(imageURL: URL, size: CGSize) { + imageView.setImage(with: imageURL, size: size) + } + + func setBadge(_ badge: SiteMediaCollectionCellViewModel.BadgeType?) { + if let badge { + let selectionView = getSelectionView() + selectionView.setBadge(badge) + selectionView.isHidden = false + } else { + selectionView?.isHidden = true + } + } + + private func getSelectionView() -> SiteMediaCollectionCellSelectionOverlayView { + if let selectionView { + return selectionView + } + let selectionView = SiteMediaCollectionCellSelectionOverlayView() + selectionView.setBadge(.unordered) + addSubview(selectionView) + selectionView.translatesAutoresizingMaskIntoConstraints = false + pinSubviewToAllEdges(selectionView) + self.selectionView = selectionView + return selectionView + } +} diff --git a/WordPress/Classes/ViewRelated/Media/External/ExternalMediaPickerViewController.swift b/WordPress/Classes/ViewRelated/Media/External/ExternalMediaPickerViewController.swift new file mode 100644 index 000000000000..145e490e1866 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Media/External/ExternalMediaPickerViewController.swift @@ -0,0 +1,285 @@ +import UIKit + +protocol ExternalMediaPickerViewDelegate: AnyObject { + /// If the user cancels the flow, the selection is empty. + func externalMediaPickerViewController(_ viewController: ExternalMediaPickerViewController, didFinishWithSelection selection: [ExternalMediaAsset]) +} + +final class ExternalMediaPickerViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate, UISearchResultsUpdating, MediaPreviewControllerDataSource { + private lazy var collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout) + private lazy var flowLayout = UICollectionViewFlowLayout() + private var collectionViewDataSource: UICollectionViewDiffableDataSource! + private let searchController = UISearchController() + private let activityIndicator = UIActivityIndicatorView() + private let toolbarItemTitle = ExternalMediaSelectionTitleView() + private lazy var buttonDone = UIBarButtonItem(title: Strings.add, style: .done, target: self, action: #selector(buttonDoneTapped)) + + private let dataSource: ExternalMediaDataSource + private var assets: [String: ExternalMediaAsset] = [:] + private var allowsMultipleSelection: Bool + private var selection = NSMutableOrderedSet() // of String + private var isFirstAppearance = true + + /// A view to show when the screen is first open and the search query + /// wasn't added. + var welcomeView: UIView? + + weak var delegate: ExternalMediaPickerViewDelegate? + + let source: MediaSource + + init(dataSource: ExternalMediaDataSource, + source: MediaSource, + allowsMultipleSelection: Bool = false) { + self.dataSource = dataSource + self.source = source + self.allowsMultipleSelection = allowsMultipleSelection + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + configureCollectionView() + configureSearchController() + configureActivityIndicator() + configureNavigationItems() + configureToolbarItems() + + if let welcomeView { + view.addSubview(welcomeView) + welcomeView.translatesAutoresizingMaskIntoConstraints = false + view.pinSubviewToAllEdges(welcomeView) + } + + dataSource.onUpdatedAssets = { [weak self] in self?.didUpdateAssets() } + dataSource.onStartLoading = { [weak self] in self?.didChangeLoading(true) } + dataSource.onStopLoading = { [weak self] in self?.didChangeLoading(false) } + + switch source { + case .stockPhotos: + WPAnalytics.track(.tenorAccessed) + case .tenor: + WPAnalytics.track(.stockMediaAccessed) + default: + assertionFailure("Unsupported source: \(source)") + } + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + if isFirstAppearance { + isFirstAppearance = false + searchController.searchBar.becomeFirstResponder() + } + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + updateFlowLayoutItemSize() + } + + private func configureCollectionView() { + collectionView.register(cell: ExternalMediaPickerCollectionCell.self) + + view.addSubview(collectionView) + collectionView.translatesAutoresizingMaskIntoConstraints = false + collectionView.pinSubviewToAllEdges(view) + collectionView.accessibilityIdentifier = "MediaCollection" + collectionView.allowsMultipleSelection = allowsMultipleSelection + + collectionViewDataSource = .init(collectionView: collectionView) { [weak self] collectionView, indexPath, _ in + self?.collectionView(collectionView, cellForItemAt: indexPath) + } + collectionView.delegate = self + } + + private func updateFlowLayoutItemSize() { + let spacing: CGFloat = 2 + let availableWidth = collectionView.bounds.width + let itemsPerRow = availableWidth < 500 ? 4 : 5 + let cellWidth = ((availableWidth - spacing * CGFloat(itemsPerRow - 1)) / CGFloat(itemsPerRow)).rounded(.down) + + flowLayout.minimumInteritemSpacing = spacing + flowLayout.minimumLineSpacing = spacing + flowLayout.sectionInset = UIEdgeInsets(top: spacing, left: 0.0, bottom: 0.0, right: 0.0) + flowLayout.itemSize = CGSize(width: cellWidth, height: cellWidth) + } + + private func configureActivityIndicator() { + view.addSubview(activityIndicator) + activityIndicator.translatesAutoresizingMaskIntoConstraints = false + view.pinSubviewAtCenter(activityIndicator) + } + + private func configureSearchController() { + searchController.searchResultsUpdater = self + searchController.searchBar.autocapitalizationType = .none + searchController.searchBar.autocorrectionType = .no + searchController.hidesNavigationBarDuringPresentation = false + + navigationItem.hidesSearchBarWhenScrolling = false + navigationItem.searchController = searchController + } + + private func configureNavigationItems() { + let buttonCancel = UIBarButtonItem(systemItem: .cancel, primaryAction: UIAction { [weak self] _ in + self?.buttonCancelTapped() + }) + navigationItem.leftBarButtonItem = buttonCancel + if allowsMultipleSelection { + navigationItem.rightBarButtonItems = [buttonDone] + } + navigationItem.hidesSearchBarWhenScrolling = false + } + + private func configureToolbarItems() { + guard allowsMultipleSelection, toolbarItems == nil else { return } + + var toolbarItems: [UIBarButtonItem] = [] + toolbarItems.append(UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)) + toolbarItems.append(UIBarButtonItem(customView: toolbarItemTitle)) + toolbarItems.append(UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)) + self.toolbarItems = toolbarItems + + toolbarItemTitle.buttonViewSelected.addTarget(self, action: #selector(buttonPreviewSelectionTapped), for: .touchUpInside) + } + + private func didUpdateAssets() { + self.assets = [:] + for asset in dataSource.assets { + self.assets[asset.id] = asset + } + + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([0]) + let assetIDs = dataSource.assets.map(\.id) + snapshot.appendItems(assetIDs, toSection: 0) + collectionViewDataSource.apply(snapshot, animatingDifferences: false) + + let remaning = NSOrderedSet(array: assetIDs) + selection.intersect(remaning) + didUpdateSelection() + } + + private func didChangeLoading(_ isLoading: Bool) { + if isLoading && dataSource.assets.isEmpty { + activityIndicator.startAnimating() + welcomeView?.isHidden = true + } else { + activityIndicator.stopAnimating() + } + } + + // MARK: - Selection + + private func setSelected(_ isSelected: Bool, for asset: ExternalMediaAsset) { + if isSelected { + selection.add(asset.id) + } else { + selection.remove(asset.id) + } + didUpdateSelection() + } + + private func didUpdateSelection() { + if allowsMultipleSelection { + toolbarItemTitle.setSelectionCount(selection.count) + navigationController?.setToolbarHidden(dataSource.assets.isEmpty, animated: true) + } + + // Update badges for visible items (might need to update count) + for indexPath in collectionView.indexPathsForVisibleItems { + let item = dataSource.assets[indexPath.item] + let index = selection.index(of: item.id) + if let cell = collectionView.cellForItem(at: indexPath) as? ExternalMediaPickerCollectionCell { + cell.setBadge(index == NSNotFound ? nil : .ordered(index: index)) + } + } + } + + // MARK: - Actions + + private func buttonCancelTapped() { + delegate?.externalMediaPickerViewController(self, didFinishWithSelection: []) + } + + @objc private func buttonDoneTapped() { + + + let selection = (selection.array as! [String]).compactMap { assets[$0] } + delegate?.externalMediaPickerViewController(self, didFinishWithSelection: selection) + } + + @objc private func buttonPreviewSelectionTapped() { + let viewController = MediaPreviewController() + viewController.dataSource = self + let navigation = UINavigationController(rootViewController: viewController) + present(navigation, animated: true) + } + + // MARK: - UICollectionViewDataSource + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + dataSource.assets.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeue(cell: ExternalMediaPickerCollectionCell.self, for: indexPath)! + let item = dataSource.assets[indexPath.item] + cell.configure(imageURL: item.thumbnailURL, size: flowLayout.itemSize.scaled(by: UIScreen.main.scale)) + return cell + } + + // MARK: - UICollectionViewDelegate + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + let item = dataSource.assets[indexPath.item] + if allowsMultipleSelection { + setSelected(true, for: item) + } else { + delegate?.externalMediaPickerViewController(self, didFinishWithSelection: [item]) + } + } + + func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) { + setSelected(false, for: dataSource.assets[indexPath.item]) + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if scrollView.contentOffset.y + scrollView.frame.size.height > scrollView.contentSize.height - 500 { + dataSource.loadMore() + } + } + + // MARK: - UISearchResultsUpdating + + func updateSearchResults(for searchController: UISearchController) { + let searchTerm = searchController.searchBar.text ?? "" + dataSource.search(for: searchTerm) + } + + // MARK: - MediaPreviewControllerDataSource + + func numberOfPreviewItems(in controller: MediaPreviewController) -> Int { + selection.count + } + + func previewController(_ controller: MediaPreviewController, previewItemAt index: Int) -> MediaPreviewItem? { + guard let id = selection.object(at: index) as? String, + let asset = assets[id] else { + return nil + } + return MediaPreviewItem(url: asset.largeURL) + } +} + +private enum Strings { + static let add = NSLocalizedString("externalMediaPicker.add", value: "Add", comment: "Title for confirmation navigation bar button item") +} diff --git a/WordPress/Classes/ViewRelated/Media/External/ExternalMediaSelectionTitleView.swift b/WordPress/Classes/ViewRelated/Media/External/ExternalMediaSelectionTitleView.swift new file mode 100644 index 000000000000..26cf1ea01e09 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Media/External/ExternalMediaSelectionTitleView.swift @@ -0,0 +1,51 @@ +import UIKit + +final class ExternalMediaSelectionTitleView: UIView { + private let textLabel = UILabel() + let buttonViewSelected = UIButton(type: .system) + + override init(frame: CGRect) { + super.init(frame: frame) + + buttonViewSelected.titleLabel?.font = WPStyleGuide.fontForTextStyle(.headline) + buttonViewSelected.tintColor = UIColor.primary + + textLabel.font = WPStyleGuide.fontForTextStyle(.headline) + textLabel.textAlignment = .center + + addSubview(textLabel) + textLabel.translatesAutoresizingMaskIntoConstraints = false + textLabel.text = Strings.toolbarSelectItems + pinSubviewAtCenter(textLabel) + + addSubview(buttonViewSelected) + buttonViewSelected.translatesAutoresizingMaskIntoConstraints = false + pinSubviewToAllEdges(buttonViewSelected) + + // Make sure it fits when displayed in `UIToolbar`. + buttonViewSelected.setTitle(String(format: Strings.toolbarViewSelected, String(999999)), for: []) + buttonViewSelected.sizeToFit() + + setSelectionCount(0) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setSelectionCount(_ count: Int) { + buttonViewSelected.isHidden = count == 0 + textLabel.isHidden = count != 0 + if count > 0 { + UIView.performWithoutAnimation { + buttonViewSelected.setTitle(String(format: Strings.toolbarViewSelected, String(count)), for: []) + buttonViewSelected.layoutIfNeeded() + } + } + } +} + +private enum Strings { + static let toolbarSelectItems = NSLocalizedString("externalMediaPicker.toolbarSelectItemsPrompt", value: "Select Images", comment: "Bottom toolbar title in the selection mode") + static let toolbarViewSelected = NSLocalizedString("externalMediaPicker.toolbarViewSelected", value: "View Selected (%@)", comment: "Bottom toolbar title in the selection mode") +} diff --git a/WordPress/Classes/ViewRelated/Media/MediaItemViewController.swift b/WordPress/Classes/ViewRelated/Media/MediaItemViewController.swift index 21613e7eb06b..783d9d571920 100644 --- a/WordPress/Classes/ViewRelated/Media/MediaItemViewController.swift +++ b/WordPress/Classes/ViewRelated/Media/MediaItemViewController.swift @@ -7,30 +7,27 @@ import WordPressShared /// Displays an image preview and metadata for a single Media asset. /// -class MediaItemViewController: UITableViewController { - - class DownloadDelegate: NSObject, AVAssetDownloadDelegate { - - } - - // swiftlint:disable:next weak_delegate - let delegate = DownloadDelegate() - - @objc let media: Media +final class MediaItemViewController: UITableViewController { + let media: Media - fileprivate var viewModel: ImmuTable! - fileprivate var mediaMetadata: MediaMetadata { + private var viewModel: ImmuTable! + private var mediaMetadata: MediaMetadata { didSet { - updateNavigationItem() + if !mediaMetadata.matches(media) { + saveChanges() + } } } - @objc init(media: Media) { + private let headerView = MediaItemHeaderView() + private lazy var headerMaxHeightConstraint = headerView.heightAnchor.constraint(lessThanOrEqualToConstant: 320) + + init(media: Media) { self.media = media self.mediaMetadata = MediaMetadata(media: media) - super.init(style: .grouped) + super.init(style: .insetGrouped) } required init?(coder aDecoder: NSCoder) { @@ -40,9 +37,10 @@ class MediaItemViewController: UITableViewController { override func viewDidLoad() { super.viewDidLoad() - WPStyleGuide.configureColors(view: view, tableView: tableView) - ImmuTable.registerRows([TextRow.self, EditableTextRow.self, MediaImageRow.self, MediaDocumentRow.self], - tableView: tableView) + tableView.showsVerticalScrollIndicator = false + tableView.cellLayoutMarginsFollowReadableWidth = true + + ImmuTable.registerRows([TextRow.self, EditableTextRow.self], tableView: tableView) updateViewModel() updateNavigationItem() @@ -73,41 +71,25 @@ class MediaItemViewController: UITableViewController { } viewModel = ImmuTable(sections: [ - ImmuTableSection(rows: [ headerRow ]), ImmuTableSection(headerText: nil, rows: mediaInfoRows, footerText: nil), - ImmuTableSection(headerText: NSLocalizedString("Metadata", comment: "Title of section containing image / video metadata such as size and file type"), - rows: metadataRows, - footerText: nil) - ]) + ImmuTableSection(headerText: nil, rows: metadataRows, footerText: nil) + ]) + + headerView.heightAnchor.constraint(greaterThanOrEqualToConstant: 280).isActive = true + headerMaxHeightConstraint.isActive = true + headerView.configure(with: media) + tableView.tableHeaderView = headerView + + headerView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTapHeaderView))) } - private var headerRow: ImmuTableRow { - switch media.mediaType { - case .image, .video: - return MediaImageRow(media: media, action: { [weak self] row in - guard let media = self?.media else { return } - - switch media.mediaType { - case .image: - if self?.isMediaLoaded() == true { - self?.presentImageViewControllerForMedia() - } - case .video: - self?.presentVideoViewControllerForMedia() - default: break - } - }) - default: - return MediaDocumentRow(media: media, action: { [weak self] _ in - guard let media = self?.media else { return } - - // We're currently not presenting previews for audio until - // we can resolve an auth issue. @frosty 2017-05-02 - if media.mediaType != .audio { - self?.presentDocumentViewControllerForMedia() - } - }) - } + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + // Using a constant instead of a `multiplier` because the multiplier-based + // constraint doesn't seem to go into effect until after `viewDidLayoutSubviews`. + headerMaxHeightConstraint.constant = view.bounds.height * 0.75 + tableView.sizeToFitHeaderView() } private var metadataRows: [ImmuTableRow] { @@ -158,43 +140,36 @@ class MediaItemViewController: UITableViewController { } private func updateNavigationItem() { - if mediaMetadata.matches(media) { - navigationItem.leftBarButtonItem = nil - let shareItem = UIBarButtonItem(image: .gridicon(.shareiOS), - style: .plain, - target: self, - action: #selector(shareTapped(_:))) - shareItem.accessibilityLabel = NSLocalizedString("Share", comment: "Accessibility label for share buttons in nav bars") - - let trashItem = UIBarButtonItem(image: .gridicon(.trash), - style: .plain, - target: self, - action: #selector(trashTapped(_:))) - trashItem.accessibilityLabel = NSLocalizedString("Trash", comment: "Accessibility label for trash buttons in nav bars") - - if media.blog.supports(.mediaDeletion) { - navigationItem.rightBarButtonItems = [ shareItem, trashItem ] - } else { - navigationItem.rightBarButtonItems = [ shareItem ] - } + let shareItem = UIBarButtonItem(image: UIImage(systemName: "square.and.arrow.up"), + style: .plain, + target: self, + action: #selector(shareTapped)) + shareItem.accessibilityLabel = NSLocalizedString("Share", comment: "Accessibility label for share buttons in nav bars") + + let trashItem = UIBarButtonItem(image: UIImage(systemName: "trash"), + style: .plain, + target: self, + action: #selector(trashTapped)) + trashItem.accessibilityLabel = NSLocalizedString("Trash", comment: "Accessibility label for trash buttons in nav bars") + + if media.blog.supports(.mediaDeletion) { + navigationItem.rightBarButtonItems = [ shareItem, trashItem ] } else { - navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelTapped)) - navigationItem.rightBarButtonItems = [ UIBarButtonItem(barButtonSystemItem: .save, target: self, action: #selector(saveTapped)) ] + navigationItem.rightBarButtonItems = [ shareItem ] } } - private func isMediaLoaded() -> Bool { - let headerIndexPath = IndexPath(row: 0, section: 0) - // Check in case of future changes. - assert(viewModel.sections[headerIndexPath.section].rows[headerIndexPath.row].cellClass == headerRow.cellClass, "Wrong index path for headerRow") - - guard - let cell = tableView.cellForRow(at: headerIndexPath) as? MediaItemImageTableViewCell, - cell.customImageView.image != nil else { - - return false + @objc private func didTapHeaderView() { + switch media.mediaType { + case .image: + presentImageViewControllerForMedia() + case .video: + presentVideoViewControllerForMedia() + case .document: + presentDocumentViewControllerForMedia() + default: + break } - return true } private func presentImageViewControllerForMedia() { @@ -221,7 +196,6 @@ class MediaItemViewController: UITableViewController { } } - private var documentInteractionController: UIDocumentInteractionController? private func presentDocumentViewControllerForMedia() { guard let remoteURL = media.remoteURL, let url = URL(string: remoteURL) else { return } @@ -247,35 +221,32 @@ class MediaItemViewController: UITableViewController { // MARK: - Actions - private var shareVideoCancellable: AnyCancellable? = nil - @objc private func shareTapped(_ sender: UIBarButtonItem) { - switch media.mediaType { - case .image: - media.image(with: .zero) { [weak self] image, error in - guard let image = image else { - if let error = error { - DDLogError("Error when attempting to share image: \(error)") - } - return - } - - self?.share(media: image, sender: sender) + func setPreparingToShare(_ isSharing: Bool) { + if isSharing { + let indicator = UIActivityIndicatorView() + indicator.startAnimating() + indicator.frame = CGRect(origin: .zero, size: CGSize(width: 43, height: 44)) + sender.customView = indicator + } else { + sender.customView = nil } - case .audio, .video: - shareVideoCancellable = media.videoURLPublisher(skipTransformCheck: true).sink { [weak self] completion in - if case .failure(let error) = completion { - DDLogError("Error when attempting to share video: \(error)") - } - - self?.shareVideoCancellable = nil - } receiveValue: { [weak self] url in - DispatchQueue.main.async { [weak self] in - self?.share(media: url, sender: sender) - } + sender.isEnabled = !isSharing + } + + setPreparingToShare(true) + + WPAnalytics.track(.siteMediaShareTapped, properties: ["number_of_items": 1]) + + Task { + do { + let fileURLs = try await Media.downloadRemoteData(for: [media], blog: media.blog) + self.share(fileURLs, sender: sender) + } catch { + SVProgressHUD.showError(withStatus: SiteMediaViewController.sharingFailureMessage) } - default: - break + + setPreparingToShare(false) } } @@ -305,36 +276,28 @@ class MediaItemViewController: UITableViewController { SVProgressHUD.setMinimumDismissTimeInterval(1.0) SVProgressHUD.show(withStatus: NSLocalizedString("Deleting...", comment: "Text displayed in HUD while a media item is being deleted.")) - let service = MediaService(managedObjectContext: ContextManager.sharedInstance().mainContext) - service.delete(media, success: { [weak self] in - WPAppAnalytics.track(.mediaLibraryDeletedItems, withProperties: ["number_of_items_deleted": 1], with: self?.media.blog) - SVProgressHUD.showSuccess(withStatus: NSLocalizedString("Deleted!", comment: "Text displayed in HUD after successfully deleting a media item")) - }, failure: { error in - SVProgressHUD.showError(withStatus: NSLocalizedString("Unable to delete media item.", comment: "Text displayed in HUD if there was an error attempting to delete a media item.")) - }) - } + let repository = MediaRepository(coreDataStack: ContextManager.shared) + let mediaID = TaggedManagedObjectID(media) + Task { @MainActor in + do { + try await repository.delete(mediaID) - @objc private func cancelTapped() { - mediaMetadata = MediaMetadata(media: media) - reloadViewModel() - updateTitle() + WPAppAnalytics.track(.mediaLibraryDeletedItems, withProperties: ["number_of_items_deleted": 1], with: self.media.blog) + SVProgressHUD.showSuccess(withStatus: NSLocalizedString("Deleted!", comment: "Text displayed in HUD after successfully deleting a media item")) + } catch { + SVProgressHUD.showError(withStatus: NSLocalizedString("Unable to delete media item.", comment: "Text displayed in HUD if there was an error attempting to delete a media item.")) + } + } } - @objc private func saveTapped() { - SVProgressHUD.setDefaultMaskType(.clear) - SVProgressHUD.setMinimumDismissTimeInterval(1.0) - SVProgressHUD.show(withStatus: NSLocalizedString("Saving...", comment: "Text displayed in HUD while a media item's metadata (title, etc) is being saved.")) - + private func saveChanges() { mediaMetadata.update(media) let service = MediaService(managedObjectContext: ContextManager.sharedInstance().mainContext) service.update(media, success: { [weak self] in WPAppAnalytics.track(.mediaLibraryEditedItemMetadata, with: self?.media.blog) - SVProgressHUD.showSuccess(withStatus: NSLocalizedString("Saved!", comment: "Text displayed in HUD when a media item's metadata (title, etc) is saved successfully.")) - self?.updateNavigationItem() - }, failure: { error in + }, failure: { _ in SVProgressHUD.showError(withStatus: NSLocalizedString("Unable to save media item.", comment: "Text displayed in HUD when a media item's metadata (title, etc) couldn't be saved.")) - self.updateNavigationItem() }) } @@ -344,6 +307,7 @@ class MediaItemViewController: UITableViewController { self?.pushSettingsController(for: editableRow, hint: NSLocalizedString("Image title", comment: "Hint for image title on image settings."), onValueChanged: { value in self?.title = value + (self?.parent as? SiteMediaPageViewController)?.title = value self?.mediaMetadata.title = value self?.reloadViewModel() }) @@ -396,19 +360,6 @@ class MediaItemViewController: UITableViewController { // MARK: - Sharing Logic - private func mediaURL() -> URL? { - guard let remoteURL = media.remoteURL, - let url = URL(string: remoteURL) else { - return nil - } - - return url - } - - private func share(media: Any, sender: UIBarButtonItem) { - share([media], sender: sender) - } - private func share(_ activityItems: [Any], sender: UIBarButtonItem) { let activityController = UIActivityViewController(activityItems: activityItems, applicationActivities: nil) activityController.modalPresentationStyle = .popover @@ -418,7 +369,6 @@ class MediaItemViewController: UITableViewController { WPAppAnalytics.track(.mediaLibrarySharedItemLink, with: self?.media.blog) } } - present(activityController, animated: true) } } @@ -436,7 +386,6 @@ extension MediaItemViewController { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let row = viewModel.rowAtIndexPath(indexPath) let cell = tableView.dequeueReusableCell(withIdentifier: row.reusableIdentifier, for: indexPath) - row.configureCell(cell) return cell @@ -449,37 +398,6 @@ extension MediaItemViewController { // MARK: - UITableViewDelegate extension MediaItemViewController { - override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool { - let row = viewModel.rowAtIndexPath(indexPath) - if row is MediaDocumentRow && media.mediaType == .audio { - return false - } - - return true - } - - override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - let row = viewModel.rowAtIndexPath(indexPath) - if let customHeight = type(of: row).customHeight { - return CGFloat(customHeight) - } else if row is MediaImageRow { - return UITableView.automaticDimension - } - - return tableView.rowHeight - } - - override func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { - let row = viewModel.rowAtIndexPath(indexPath) - if let customHeight = type(of: row).customHeight { - return CGFloat(customHeight) - } else if row is MediaImageRow { - return view.readableContentGuide.layoutFrame.width - } - - return tableView.rowHeight - } - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let row = viewModel.rowAtIndexPath(indexPath) row.action?(row) @@ -497,7 +415,7 @@ private struct MediaMetadataPresenter { let width = media.width ?? 0 let height = media.height ?? 0 - return "\(width) ✕ \(height)" + return "\(width) × \(height)" } /// A String containing the uppercased file extension of the asset (.JPG, .PNG, etc) diff --git a/WordPress/Classes/ViewRelated/Media/MediaLibraryMediaPickingCoordinator.swift b/WordPress/Classes/ViewRelated/Media/MediaLibraryMediaPickingCoordinator.swift deleted file mode 100644 index 50af727aa71a..000000000000 --- a/WordPress/Classes/ViewRelated/Media/MediaLibraryMediaPickingCoordinator.swift +++ /dev/null @@ -1,153 +0,0 @@ -import MobileCoreServices -import WPMediaPicker -import PhotosUI - -/// Prepares the alert controller that will be presented when tapping the "+" button in Media Library -final class MediaLibraryMediaPickingCoordinator { - typealias PickersDelegate = StockPhotosPickerDelegate & WPMediaPickerViewControllerDelegate & TenorPickerDelegate & PHPickerViewControllerDelegate & ImagePickerControllerDelegate - private weak var delegate: PickersDelegate? - private var tenor: TenorPicker? - - private var stockPhotos: StockPhotosPicker? - - init(delegate: PickersDelegate) { - self.delegate = delegate - } - - func present(context: MediaPickingContext) { - let origin = context.origin - let blog = context.blog - let fromView = context.view - let buttonItem = context.barButtonItem - - let menuAlert = UIAlertController(title: nil, message: nil, preferredStyle: UIAlertController.Style.actionSheet) - - if let quotaUsageDescription = blog.quotaUsageDescription { - menuAlert.title = quotaUsageDescription - } - - if UIImagePickerController.isSourceTypeAvailable(.camera) { - menuAlert.addAction(cameraAction(origin: origin, blog: blog)) - } - - menuAlert.addAction(photoLibraryAction(origin: origin, blog: blog)) - - if blog.supports(.stockPhotos) { - menuAlert.addAction(freePhotoAction(origin: origin, blog: blog)) - } - if blog.supports(.tenor) { - menuAlert.addAction(tenorAction(origin: origin, blog: blog)) - } - - menuAlert.addAction(otherAppsAction(origin: origin, blog: blog)) - menuAlert.addAction(cancelAction()) - - menuAlert.popoverPresentationController?.sourceView = fromView - menuAlert.popoverPresentationController?.sourceRect = fromView.bounds - menuAlert.popoverPresentationController?.barButtonItem = buttonItem - - origin.present(menuAlert, animated: true) - } - - private func cameraAction(origin: UIViewController, blog: Blog) -> UIAlertAction { - return UIAlertAction(title: .takePhotoOrVideo, style: .default, handler: { [weak self] action in - self?.showCameraCapture(origin: origin, blog: blog) - }) - } - - private func photoLibraryAction(origin: UIViewController, blog: Blog) -> UIAlertAction { - return UIAlertAction(title: .importFromPhotoLibrary, style: .default, handler: { [weak self] action in - self?.showMediaPicker(origin: origin, blog: blog) - }) - } - - private func freePhotoAction(origin: UIViewController, blog: Blog) -> UIAlertAction { - return UIAlertAction(title: .freePhotosLibrary, style: .default, handler: { [weak self] action in - self?.showStockPhotos(origin: origin, blog: blog) - }) - } - - private func tenorAction(origin: UIViewController, blog: Blog) -> UIAlertAction { - return UIAlertAction(title: .tenor, style: .default, handler: { [weak self] action in - self?.showTenor(origin: origin, blog: blog) - }) - } - - private func otherAppsAction(origin: UIViewController & UIDocumentPickerDelegate, blog: Blog) -> UIAlertAction { - return UIAlertAction(title: .otherApps, style: .default, handler: { [weak self] action in - self?.showDocumentPicker(origin: origin, blog: blog) - }) - } - - private func cancelAction() -> UIAlertAction { - return UIAlertAction(title: .cancelMoreOptions, style: .cancel, handler: nil) - } - - private func showCameraCapture(origin: UIViewController, blog: Blog) { - MediaPickerMenu(viewController: origin) - .showCamera(delegate: self) - } - - private func showStockPhotos(origin: UIViewController, blog: Blog) { - let picker = StockPhotosPicker() - // add delegate conformance, allow release of picker in the same manner as the tenor picker - // in order to prevent duplicated uploads and botched de-selection on second upload - picker.delegate = self - picker.presentPicker(origin: origin, blog: blog) - stockPhotos = picker - } - - private func showTenor(origin: UIViewController, blog: Blog) { - let picker = TenorPicker() - // Delegate to the PickerCoordinator so we can release the Tenor instance - picker.delegate = self - picker.presentPicker(origin: origin, blog: blog) - tenor = picker - } - - - private func showDocumentPicker(origin: UIViewController & UIDocumentPickerDelegate, blog: Blog) { - let docTypes = blog.allowedTypeIdentifiers - let docPicker = UIDocumentPickerViewController(documentTypes: docTypes, in: .import) - docPicker.delegate = origin - docPicker.allowsMultipleSelection = true - origin.present(docPicker, animated: true) - } - - private func showMediaPicker(origin: UIViewController, blog: Blog) { - var configuration = PHPickerConfiguration() - configuration.preferredAssetRepresentationMode = .current - configuration.selection = .ordered - configuration.selectionLimit = 0 // Unlimited - - let picker = PHPickerViewController(configuration: configuration) - picker.delegate = self - origin.present(picker, animated: true) - } -} - -extension MediaLibraryMediaPickingCoordinator: TenorPickerDelegate { - func tenorPicker(_ picker: TenorPicker, didFinishPicking assets: [TenorMedia]) { - delegate?.tenorPicker(picker, didFinishPicking: assets) - tenor = nil - } -} - -extension MediaLibraryMediaPickingCoordinator: StockPhotosPickerDelegate { - func stockPhotosPicker(_ picker: StockPhotosPicker, didFinishPicking assets: [StockPhotosMedia]) { - delegate?.stockPhotosPicker(picker, didFinishPicking: assets) - stockPhotos = nil - } -} - -extension MediaLibraryMediaPickingCoordinator: PHPickerViewControllerDelegate { - func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { - delegate?.picker(picker, didFinishPicking: results) - } -} - -extension MediaLibraryMediaPickingCoordinator: ImagePickerControllerDelegate { - func imagePicker(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { - delegate?.imagePicker(picker, didFinishPickingMediaWithInfo: info) - } -} diff --git a/WordPress/Classes/ViewRelated/Media/MediaLibraryPickerDataSource.h b/WordPress/Classes/ViewRelated/Media/MediaLibraryPickerDataSource.h deleted file mode 100644 index b15e8156bd88..000000000000 --- a/WordPress/Classes/ViewRelated/Media/MediaLibraryPickerDataSource.h +++ /dev/null @@ -1,50 +0,0 @@ -#import -#import -#import "Media.h" -#import "Media+WPMediaAsset.h" - -@class Blog; -@class AbstractPost; - -typedef NS_ENUM(NSUInteger, MediaPickerDataSourceType) { - MediaPickerDataSourceTypeDevice, - MediaPickerDataSourceTypeMediaLibrary -}; - -@interface MediaLibraryGroup: NSObject - -- (nonnull instancetype)initWithBlog:(Blog *_Nonnull)blog; - -@property (nonatomic, assign) WPMediaType filter; - -@end - -@interface MediaLibraryPickerDataSource : NSObject - -- (nonnull instancetype)initWithBlog:(Blog *_Nonnull)blog; - -- (nonnull instancetype)initWithPost:(AbstractPost *_Nonnull)post; - -/// If a search query is set, the media assets fetched by the data source -/// will be filtered to only those whose name, caption, or description -/// contain the search query. -@property (nonatomic, copy, nullable) NSString *searchQuery; - -/// Defaults to `NO`. -/// By default, the data source will only show media that has been synced to the -/// remote. Set this to `YES` to include local-only media, or media that is -/// currently being processed or uploaded. -@property (nonatomic) BOOL includeUnsyncedMedia; - -/// Defaults to `NO`. -/// By default, errors (causes by e.g. devices being offline, or user using a slow network) when syncing -/// will cause `-[WPMediaCollectionDataSource loadDataWithOptions:success:failure:]` to call the -/// failure block. Setting this to `YES` will override this behavior, and will call the `successBlock` instead. -/// Note: this only applies to the fetching operation — write/upload operations will still return errors as -/// normal. -@property (nonatomic) BOOL ignoreSyncErrors; - -/// The total asset account, ignoring the current search query if there is one. -@property (nonatomic, readonly) NSInteger totalAssetCount; - -@end diff --git a/WordPress/Classes/ViewRelated/Media/MediaLibraryPickerDataSource.m b/WordPress/Classes/ViewRelated/Media/MediaLibraryPickerDataSource.m deleted file mode 100644 index 742115c18901..000000000000 --- a/WordPress/Classes/ViewRelated/Media/MediaLibraryPickerDataSource.m +++ /dev/null @@ -1,631 +0,0 @@ -#import "MediaLibraryPickerDataSource.h" -#import "Media.h" -#import "MediaService.h" -#import "Blog.h" -#import "CoreDataStack.h" -#import "WordPress-Swift.h" -#import - -@interface MediaLibraryPickerDataSource() - -@property (nonatomic, strong) MediaLibraryGroup *mediaGroup; -@property (nonatomic, strong) Blog *blog; -@property (nonatomic, strong) AbstractPost *post; -@property (nonatomic, assign) WPMediaType filter; -@property (nonatomic, assign) BOOL ascendingOrdering; -@property (nonatomic, strong) NSMutableDictionary *observers; -@property (nonatomic, strong) NSMutableDictionary *groupObservers; -@property (nonatomic, strong) NSFetchedResultsController *fetchController; -@property (nonatomic, strong) id groupObserverHandler; -#pragma mark - change trackers -@property (nonatomic, strong) NSMutableIndexSet *mediaRemoved; -@property (nonatomic, strong) NSMutableIndexSet *mediaInserted; -@property (nonatomic, strong) NSMutableIndexSet *mediaChanged; -@property (nonatomic, strong) NSMutableArray *mediaMoved; - -@end - -@interface MediaLibraryGroup() -@property (nonatomic, strong) Blog *blog; -@property (nonatomic, assign) NSInteger itemsCount; -@property (nonatomic, strong) NSManagedObjectID *imageMediaID; - -- (void)refreshImageMedia; -@end - -@implementation MediaLibraryPickerDataSource - -- (instancetype)initWithBlog:(Blog *)blog -{ - /// Temporary logging to try and narrow down an issue: - /// - /// REF: https://github.com/wordpress-mobile/WordPress-iOS/issues/15335 - /// - if (blog == nil || blog.objectID == nil) { - DDLogError(@"🔴 Error: missing object ID (please contact @diegoreymendez with this log)"); - DDLogError(@"%@", [NSThread callStackSymbols]); - } - - self = [super init]; - if (self) { - _mediaGroup = [[MediaLibraryGroup alloc] initWithBlog:blog]; - _blog = blog; - _observers = [NSMutableDictionary dictionary]; - _groupObservers = [NSMutableDictionary dictionary]; - _mediaRemoved = [[NSMutableIndexSet alloc] init]; - _mediaInserted = [[NSMutableIndexSet alloc] init]; - _mediaChanged = [[NSMutableIndexSet alloc] init]; - _mediaMoved = [[NSMutableArray alloc] init]; - - } - return self; -} - -- (instancetype)initWithPost:(AbstractPost *)post -{ - self = [self initWithBlog:post.blog]; - if (self) { - _post = post; - } - return self; -} - -#pragma mark - WPMediaCollectionDataSource - -- (void)searchFor:(NSString *)searchText -{ - if (![_searchQuery isEqualToString:searchText]) { - _searchQuery = [searchText copy]; - - _fetchController = nil; - [self.fetchController performFetch:nil]; - [self notifyObserversReloadData]; - } -} - -- (void)setFilter:(WPMediaType)filter { - if ( _filter != filter ) { - _filter = filter; - - _fetchController = nil; - [self.fetchController performFetch:nil]; - } -} - -- (void)searchCancelled -{ - _searchQuery = nil; - _fetchController = nil; - [self.fetchController performFetch:nil]; -} - --(NSInteger)numberOfAssets -{ - if ([[self.fetchController sections] count] > 0) { - id sectionInfo = [[self.fetchController sections] objectAtIndex:0]; - return [sectionInfo numberOfObjects]; - } else - return 0; -} - --(NSInteger)numberOfGroups -{ - return 1; -} - --(void)setSelectedGroup:(id)group -{ - //There is only one group in the media library for now so don't do anything -} - --(id)selectedGroup -{ - //There is only one group in the media library for now so don't do anything - return self.mediaGroup; -} - -- (id)groupAtIndex:(NSInteger)index -{ - return self.mediaGroup; -} - -- (void)loadDataWithOptions:(WPMediaLoadOptions)options success:(WPMediaSuccessBlock)successBlock failure:(WPMediaFailureBlock)failureBlock -{ - // let's check if we already have fetched results before - if (self.fetchController.fetchedObjects == nil) { - NSError *error; - if (![self.fetchController performFetch:&error]) { - if (failureBlock) { - failureBlock(error); - } - return; - } - } - BOOL localResultsAvailable = NO; - if (self.fetchController.fetchedObjects.count > 0) { - localResultsAvailable = YES; - if (successBlock) { - successBlock(); - } - } - // try to sync from the server - MediaCoordinator *mediaCoordinator = [MediaCoordinator shared]; - - /// Temporary logging to try and narrow down an issue: - /// - /// REF: https://github.com/wordpress-mobile/WordPress-iOS/issues/15335 - /// - if (self.blog == nil || self.blog.objectID == nil) { - DDLogError(@"🔴 Error: missing object ID (please contact @diegoreymendez with this log)"); - DDLogError(@"%@", [NSThread callStackSymbols]); - } - - __block BOOL ignoreSyncError = self.ignoreSyncErrors; - [mediaCoordinator syncMediaFor:self.blog - success:^{ - if (!localResultsAvailable && successBlock) { - successBlock(); - } - } failure:^(NSError * _Nonnull error) { - if (ignoreSyncError && successBlock) { - successBlock(); - return; - } - - if (failureBlock) { - failureBlock(error); - } - }]; -} - -- (void)notifyObserversWithIncrementalChanges:(BOOL)incrementalChanges removed:(NSIndexSet *)removed inserted:(NSIndexSet *)inserted changed:(NSIndexSet *)changed moved:(NSArray> *)moved -{ - for ( WPMediaChangesBlock callback in [self.observers allValues]) { - callback(incrementalChanges, removed, inserted, changed, moved); - } -} - -- (void)notifyGroupObservers -{ - for ( WPMediaGroupChangesBlock callback in [self.groupObservers allValues]) { - callback(); - } -} - -- (void)notifyObserversReloadData -{ - [self notifyObserversWithIncrementalChanges:NO - removed:[NSIndexSet new] - inserted:[NSIndexSet new] - changed:[NSIndexSet new] - moved:@[]]; -} - --(id)registerChangeObserverBlock:(WPMediaChangesBlock)callback -{ - NSUUID *blockKey = [NSUUID UUID]; - [self.observers setObject:[callback copy] forKey:blockKey]; - return blockKey; - -} - --(void)unregisterChangeObserver:(id)blockKey -{ - if (blockKey) { - [self.observers removeObjectForKey:blockKey]; - } -} - --(id)registerGroupChangeObserverBlock:(WPMediaGroupChangesBlock)callback -{ - NSUUID *blockKey = [NSUUID UUID]; - [self.groupObservers setObject:[callback copy] forKey:blockKey]; - return blockKey;} - --(void)unregisterGroupChangeObserver:(id)blockKey -{ - if (blockKey) { - [self.groupObservers removeObjectForKey:blockKey]; - } -} - -- (void)addImage:(UIImage *)image - metadata:(NSDictionary *)metadata - completionBlock:(WPMediaAddedBlock)completionBlock -{ - if ( PHPhotoLibrary.authorizationStatus == PHAuthorizationStatusAuthorized ) { - [self addAssetWithChangeRequest:^PHAssetChangeRequest *{ - NSString *fileName = [NSString stringWithFormat:@"%@_%@", [[NSProcessInfo processInfo] globallyUniqueString], @".jpg"]; - NSURL *fileURL = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:fileName]]; - NSError *error; - if ([image writeToURL:fileURL type:UTTypeJPEG.identifier compressionQuality:MediaImportService.preferredImageCompressionQuality metadata:metadata error:&error]){ - return [PHAssetChangeRequest creationRequestForAssetFromImageAtFileURL:fileURL]; - } - return nil; - } completionBlock:completionBlock]; - } else { - NSString *fileName = [NSString stringWithFormat:@"%@_%@", [[NSProcessInfo processInfo] globallyUniqueString], @".jpg"]; - NSURL *fileURL = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:fileName]]; - NSError *error; - if ([image writeToURL:fileURL type:UTTypeJPEG.identifier compressionQuality:MediaImportService.preferredImageCompressionQuality metadata:metadata error:&error]){ - [self addMediaFromURL:fileURL completionBlock:completionBlock]; - } else { - if (completionBlock) { - completionBlock(nil, error); - } - } - } -} - -- (void)addVideoFromURL:(NSURL *)url - completionBlock:(WPMediaAddedBlock)completionBlock -{ - if ( PHPhotoLibrary.authorizationStatus == PHAuthorizationStatusAuthorized ) { - [self addAssetWithChangeRequest:^PHAssetChangeRequest *{ - return [PHAssetChangeRequest creationRequestForAssetFromVideoAtFileURL:url]; - } completionBlock:completionBlock]; - } else { - [self addMediaFromURL:url completionBlock:completionBlock]; - } -} - -- (void)addAssetWithChangeRequest:(PHAssetChangeRequest *(^)(void))changeRequestBlock - completionBlock:(WPMediaAddedBlock)completionBlock -{ - NSParameterAssert(changeRequestBlock); - __block NSString * assetIdentifier = nil; - __weak __typeof__(self) weakSelf = self; - [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{ - // Request creating an asset from the image. - PHAssetChangeRequest *createAssetRequest = changeRequestBlock(); - PHObjectPlaceholder *assetPlaceholder = [createAssetRequest placeholderForCreatedAsset]; - assetIdentifier = [assetPlaceholder localIdentifier]; - } completionHandler:^(BOOL success, NSError *error) { - if (!success) { - if (completionBlock){ - dispatch_async(dispatch_get_main_queue(), ^{ - completionBlock(nil, error); - }); - } - return; - } - [weakSelf addMediaFromAssetIdentifier:assetIdentifier completionBlock:completionBlock]; - }]; -} - --(void)addMediaFromAssetIdentifier:(NSString *)assetIdentifier - completionBlock:(WPMediaAddedBlock)completionBlock -{ - PHFetchResult *result = [PHAsset fetchAssetsWithLocalIdentifiers:@[assetIdentifier] options:nil]; - PHAsset *asset = [result firstObject]; - MediaImportService *service = [[MediaImportService alloc] initWithContextManager:[ContextManager sharedInstance]]; - [service createMediaWith:asset blog:self.blog post: self.post receiveUpdate:nil thumbnailCallback:nil completion:^(Media *media, NSError *error) { - [self loadDataWithOptions:WPMediaLoadOptionsAssets success:^{ - completionBlock(media, error); - } failure:^(NSError *error) { - if (completionBlock) { - completionBlock(nil, error); - } - }]; - }]; -} - --(void)addMediaFromURL:(NSURL *)url - completionBlock:(WPMediaAddedBlock)completionBlock -{ - MediaImportService *service = [[MediaImportService alloc] initWithContextManager:[ContextManager sharedInstance]]; - [service createMediaWith:url - blog:self.blog - post:self.post - receiveUpdate:nil - thumbnailCallback:nil - completion:^(Media *media, NSError *error) { - [self loadDataWithOptions:WPMediaLoadOptionsAssets success:^{ - completionBlock(media, error); - } failure:^(NSError *error) { - if (completionBlock) { - completionBlock(nil, error); - } - }]; - }]; -} - --(void)setMediaTypeFilter:(WPMediaType)filter -{ - self.filter = filter; - self.mediaGroup.filter = filter; -} - --(WPMediaType)mediaTypeFilter -{ - return self.filter; -} - --(id)mediaAtIndex:(NSInteger)index -{ - return [self.fetchController objectAtIndexPath:[NSIndexPath indexPathForRow:index inSection:0]]; -} - --(id)mediaWithIdentifier:(NSString *)identifier -{ - if (!identifier) { - return nil; - } - NSManagedObjectContext *mainContext = [[ContextManager sharedInstance] mainContext]; - __block Media *media = nil; - NSURL *assetURL = [NSURL URLWithString:identifier]; - if (!assetURL) { - return nil; - } - if (![[assetURL scheme] isEqualToString:@"x-coredata"]){ - return nil; - } - [mainContext performBlockAndWait:^{ - NSManagedObjectID *assetID = [[mainContext persistentStoreCoordinator] managedObjectIDForURIRepresentation:assetURL]; - media = (Media *)[mainContext objectWithID:assetID]; - }]; - - return (!media.isDeleted) ? media : nil; -} - -#pragma mark - NSFetchedResultsController helpers - -+ (NSPredicate *)predicateForFilter:(WPMediaType)filter blog:(Blog *)blog -{ - NSPredicate *predicate = [NSPredicate predicateWithFormat:@"%K == %@", @"blog", blog]; - NSMutableArray *mediaPredicates = [NSMutableArray new]; - - if ((filter & WPMediaTypeAll) == WPMediaTypeAll) { - [mediaPredicates addObject:[NSPredicate predicateWithValue:YES]]; - } else { - if (filter & WPMediaTypeImage) { - [mediaPredicates addObject:[NSPredicate predicateWithFormat:@"mediaTypeString == %@", [Media stringFromMediaType:MediaTypeImage]]]; - } - if ( filter & WPMediaTypeVideo) { - [mediaPredicates addObject:[NSPredicate predicateWithFormat:@"mediaTypeString == %@", [Media stringFromMediaType:MediaTypeVideo]]]; - } - if ( filter & WPMediaTypeAudio) { - [mediaPredicates addObject:[NSPredicate predicateWithFormat:@"mediaTypeString == %@", [Media stringFromMediaType:MediaTypeAudio]]]; - } - } - - NSCompoundPredicate *mediaPredicate = [NSCompoundPredicate orPredicateWithSubpredicates:mediaPredicates]; - - return [NSCompoundPredicate andPredicateWithSubpredicates:@[predicate, mediaPredicate]]; -} - -- (NSPredicate *)predicateForSearchQuery -{ - if (self.searchQuery && [self.searchQuery length] > 0) { - return [NSPredicate predicateWithFormat:@"(title CONTAINS[cd] %@) OR (caption CONTAINS[cd] %@) OR (desc CONTAINS[cd] %@)", self.searchQuery, self.searchQuery, self.searchQuery]; - } - - return nil; -} - -- (void)setSearchQuery:(NSString *)searchQuery -{ - if (![_searchQuery isEqualToString:searchQuery]) { - _searchQuery = [searchQuery copy]; - - _fetchController = nil; - [self.fetchController performFetch:nil]; - } -} - -- (NSFetchedResultsController *)fetchController -{ - if (_fetchController) { - return _fetchController; - } - - NSManagedObjectContext *mainContext = [[ContextManager sharedInstance] mainContext]; - NSString *entityName = NSStringFromClass([Media class]); - NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:entityName]; - - NSPredicate *filterPredicate = [[self class] predicateForFilter:self.filter blog:self.blog]; - NSPredicate *searchPredicate = [self predicateForSearchQuery]; - if (searchPredicate) { - fetchRequest.predicate = [NSCompoundPredicate andPredicateWithSubpredicates:@[filterPredicate, searchPredicate]]; - } else { - fetchRequest.predicate = filterPredicate; - } - - if (!self.includeUnsyncedMedia) { - NSPredicate *statusPredicate = [NSPredicate predicateWithFormat:@"%K == %@", @"remoteStatusNumber", @(MediaRemoteStatusSync)]; - fetchRequest.predicate = [NSCompoundPredicate andPredicateWithSubpredicates:@[fetchRequest.predicate, statusPredicate]]; - } - - fetchRequest.sortDescriptors = @[ - [NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:self.ascendingOrdering], - [NSSortDescriptor sortDescriptorWithKey:@"mediaID" ascending:self.ascendingOrdering] - ]; - - _fetchController = [[NSFetchedResultsController alloc] - initWithFetchRequest:fetchRequest - managedObjectContext:mainContext - sectionNameKeyPath:nil - cacheName:nil]; - _fetchController.delegate = self; - - return _fetchController; -} - -- (NSInteger)totalAssetCount -{ - NSManagedObjectContext *mainContext = [[ContextManager sharedInstance] mainContext]; - NSString *entityName = NSStringFromClass([Media class]); - NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:entityName]; - fetchRequest.predicate = [[self class] predicateForFilter:self.filter blog:self.blog]; - - return (NSInteger)[mainContext countForFetchRequest:fetchRequest - error:nil]; -} - -#pragma mark - NSFetchedResultsControllerDelegate - -- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller { - [self.mediaRemoved removeAllIndexes]; - [self.mediaInserted removeAllIndexes]; - [self.mediaChanged removeAllIndexes]; - [self.mediaMoved removeAllObjects]; -} - - -- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id )sectionInfo - atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type { - //This shouldn't be called because we don't have changes to sections. -} - - -- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject - atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type - newIndexPath:(NSIndexPath *)newIndexPath -{ - switch(type) { - - case NSFetchedResultsChangeInsert: - [self.mediaInserted addIndex:newIndexPath.row]; - break; - case NSFetchedResultsChangeDelete: - [self.mediaRemoved addIndex:indexPath.row]; - break; - case NSFetchedResultsChangeUpdate: - [self.mediaChanged addIndex:indexPath.row]; - break; - - case NSFetchedResultsChangeMove: { - WPIndexMove *mediaMove = [[WPIndexMove alloc] init:indexPath.row to:newIndexPath.row]; - [self.mediaMoved addObject:mediaMove]; - } - break; - } -} - - -- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller { - NSManagedObjectID *oldGroupMediaID = self.mediaGroup.imageMediaID; - [self.mediaGroup refreshImageMedia]; - if (![oldGroupMediaID isEqual:self.mediaGroup.imageMediaID]) { - [self notifyGroupObservers]; - } - [self notifyObserversWithIncrementalChanges:YES - removed:self.mediaRemoved - inserted:self.mediaInserted - changed:self.mediaChanged - moved:self.mediaMoved]; -} - -@end - -@implementation MediaLibraryGroup - -- (instancetype)initWithBlog:(Blog *)blog -{ - self = [super init]; - if (self) { - _blog = blog; - _filter = WPMediaTypeAll; - _itemsCount = NSNotFound; - [self refreshImageMedia]; - } - return self; -} - -- (void)setFilter:(WPMediaType)filter { - if (_filter != filter) { - _filter = filter; - - [self refreshImageMedia]; - } -} - -- (id)baseGroup -{ - return self; -} - -- (NSString *)name -{ - return NSLocalizedString(@"WordPress Media", @"Name for the WordPress Media Library"); -} - -- (void)refreshImageMedia -{ - NSString *entityName = NSStringFromClass([Media class]); - NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:entityName]; - request.predicate = [MediaLibraryPickerDataSource predicateForFilter:self.filter blog:self.blog]; - NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:NO]; - request.sortDescriptors = @[sortDescriptor]; - request.fetchLimit = 1; - NSError *error; - NSArray *mediaAssets = [[[ContextManager sharedInstance] mainContext] executeFetchRequest:request error:&error]; - Media *media = [mediaAssets firstObject]; - self.imageMediaID = media.objectID; -} - -- (WPMediaRequestID)imageWithSize:(CGSize)size completionHandler:(WPMediaImageBlock)completionHandler -{ - Media *media = nil; - - if (self.imageMediaID == nil) { - [self refreshImageMedia]; - } - - if (self.imageMediaID != nil) { - media = [[[ContextManager sharedInstance] mainContext] existingObjectWithID:self.imageMediaID error:nil]; - } - - if (media == nil) { - UIImage *placeholderImage = [UIImage imageNamed:@"WordPress-share"]; - completionHandler(placeholderImage, nil); - return 0; - } - - return [media imageWithSize:size completionHandler:completionHandler]; -} - -- (void)cancelImageRequest:(WPMediaRequestID)requestID -{ - -} - -- (NSString *)identifier -{ - return @"org.wordpress.medialibrary"; -} - -- (NSInteger)numberOfAssetsOfType:(WPMediaType)mediaType completionHandler:(WPMediaCountBlock)completionHandler -{ - NSMutableSet *mediaTypes = [NSMutableSet set]; - if (mediaType & WPMediaTypeImage) { - [mediaTypes addObject:@(MediaTypeImage)]; - } - if (mediaType & WPMediaTypeVideo) { - [mediaTypes addObject:@(MediaTypeVideo)]; - } - if (mediaType & WPMediaTypeAudio) { - [mediaTypes addObject:@(MediaTypeAudio)]; - } - - NSInteger count = [self.blog mediaLibraryCountForTypes:mediaTypes]; - // If we have a count difference of zero, we assume it's correct. But we still sync with the server in the background. - if (count != 0) { - self.itemsCount = count; - } - - __weak __typeof__(self) weakSelf = self; - - [self getMediaLibraryCountForMediaTypes:mediaTypes ofBlog:self.blog success:^(NSInteger count) { - weakSelf.itemsCount = count; - completionHandler(count, nil); - } failure:^(NSError *error) { - DDLogError(@"%@", [error localizedDescription]); - weakSelf.itemsCount = count; - completionHandler(count, error); - }]; - - return self.itemsCount; -} - -@end diff --git a/WordPress/Classes/ViewRelated/Media/MediaLibraryStrings.swift b/WordPress/Classes/ViewRelated/Media/MediaLibraryStrings.swift deleted file mode 100644 index 4ef5b8d6bb93..000000000000 --- a/WordPress/Classes/ViewRelated/Media/MediaLibraryStrings.swift +++ /dev/null @@ -1,10 +0,0 @@ -// MARK: - Strings specific to media pickers launched from the Media Library -extension String { - static var takePhotoOrVideo: String { - return NSLocalizedString("Take Photo or Video", comment: "Menu option for taking an image or video with the device's camera.") - } - - static var importFromPhotoLibrary: String { - return NSLocalizedString("Choose from My Device", comment: "Menu option for selecting media from the device's photo library.") - } -} diff --git a/WordPress/Classes/ViewRelated/Media/MediaLibraryViewController.swift b/WordPress/Classes/ViewRelated/Media/MediaLibraryViewController.swift deleted file mode 100644 index 6fae1678bb56..000000000000 --- a/WordPress/Classes/ViewRelated/Media/MediaLibraryViewController.swift +++ /dev/null @@ -1,832 +0,0 @@ -import UIKit -import Gridicons -import SVProgressHUD -import WordPressShared -import WPMediaPicker -import MobileCoreServices -import UniformTypeIdentifiers -import PhotosUI - -/// Displays the user's media library in a grid -/// -class MediaLibraryViewController: WPMediaPickerViewController { - fileprivate static let restorationIdentifier = "MediaLibraryViewController" - - @objc let blog: Blog - - fileprivate let pickerDataSource: MediaLibraryPickerDataSource - - fileprivate var isLoading: Bool = false - fileprivate let noResultsView = NoResultsViewController.controller() - fileprivate let addButton: SpotlightableButton = SpotlightableButton(type: .custom) - - fileprivate var kvoTokens: [NSKeyValueObservation]? - - fileprivate var selectedAsset: Media? = nil - - // After 99% progress, we'll count a media item as being uploaded, and we'll - // show an indeterminate spinner as the server processes it. - fileprivate static let uploadCompleteProgress: Double = 0.99 - - fileprivate var uploadObserverUUID: UUID? - - fileprivate lazy var mediaPickingCoordinator: MediaLibraryMediaPickingCoordinator = { - return MediaLibraryMediaPickingCoordinator(delegate: self) - }() - - // MARK: - Initializers - - @objc init(blog: Blog) { - WPMediaCollectionViewCell.appearance().placeholderTintColor = .neutral(.shade5) - WPMediaCollectionViewCell.appearance().placeholderBackgroundColor = .neutral(.shade70) - WPMediaCollectionViewCell.appearance().loadingBackgroundColor = .listBackground - - self.blog = blog - self.pickerDataSource = MediaLibraryPickerDataSource(blog: blog) - self.pickerDataSource.includeUnsyncedMedia = true - - super.init(options: MediaLibraryViewController.pickerOptions()) - - registerClass(forReusableCellOverlayViews: CircularProgressView.self) - - super.restorationIdentifier = MediaLibraryViewController.restorationIdentifier - restorationClass = MediaLibraryViewController.self - - self.dataSource = pickerDataSource - self.mediaPickerDelegate = self - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - static func showForBlog(_ blog: Blog, from sourceController: UIViewController) { - if FeatureFlag.mediaModernization.enabled { - let controller = SiteMediaViewController(blog: blog) - sourceController.show(controller, sender: nil) - } else { - let controller = MediaLibraryViewController(blog: blog) - controller.navigationItem.largeTitleDisplayMode = .never - sourceController.navigationController?.pushViewController(controller, animated: true) - } - - QuickStartTourGuide.shared.visited(.mediaScreen) - } - - deinit { - unregisterChangeObserver() - unregisterUploadCoordinatorObserver() - stopObservingNavigationBarClipsToBounds() - } - - private class func pickerOptions() -> WPMediaPickerOptions { - let options = WPMediaPickerOptions() - options.showMostRecentFirst = true - options.filter = [.all] - options.allowMultipleSelection = false - options.allowCaptureOfMedia = false - options.showSearchBar = true - options.showActionBar = false - options.badgedUTTypes = [UTType.gif.identifier] - options.preferredStatusBarStyle = WPStyleGuide.preferredStatusBarStyle - - return options - } - - // MARK: - View Loading - - override func viewDidLoad() { - super.viewDidLoad() - - title = NSLocalizedString("Media", comment: "Title for Media Library section of the app.") - - extendedLayoutIncludesOpaqueBars = true - - registerChangeObserver() - registerUploadCoordinatorObserver() - - noResultsView.configureForNoAssets(userCanUploadMedia: blog.userCanUploadMedia) - noResultsView.delegate = self - - updateViewState(for: pickerDataSource.totalAssetCount) - - if let collectionView = collectionView { - WPStyleGuide.configureColors(view: view, collectionView: collectionView) - } - - navigationController?.navigationBar.subviews.forEach ({ $0.clipsToBounds = false }) - startObservingNavigationBarClipsToBounds() - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - selectedAsset = nil - } - - override func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - - if searchBar?.isFirstResponder == true { - searchBar?.resignFirstResponder() - } - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - addButton.shouldShowSpotlight = QuickStartTourGuide.shared.isCurrentElement(.mediaUpload) - } - - // MARK: - Update view state - - fileprivate func updateViewState(for assetCount: Int) { - updateNavigationItemButtons(for: assetCount) - updateNoResultsView(for: assetCount) - updateSearchBar(for: assetCount) - } - - private func updateNavigationItemButtons(for assetCount: Int) { - if isEditing { - navigationItem.setLeftBarButton(UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(editTapped)), animated: false) - - let trashButton = UIBarButtonItem(image: .gridicon(.trash), style: .plain, target: self, action: #selector(trashTapped)) - trashButton.accessibilityLabel = NSLocalizedString("Trash", comment: "Accessibility label for trash button to delete items from the user's media library") - trashButton.accessibilityHint = NSLocalizedString("Trash selected media", comment: "Accessibility hint for trash button to delete items from the user's media library") - navigationItem.setRightBarButtonItems([trashButton], animated: true) - navigationItem.rightBarButtonItem?.isEnabled = false - } else { - navigationItem.setLeftBarButton(nil, animated: false) - - var barButtonItems = [UIBarButtonItem]() - - if blog.userCanUploadMedia { - addButton.spotlightOffset = Constants.addButtonSpotlightOffset - let config = UIImage.SymbolConfiguration(textStyle: .body, scale: .large) - let image = UIImage(systemName: "plus", withConfiguration: config) ?? .gridicon(.plus) - addButton.setImage(image, for: .normal) - addButton.contentEdgeInsets = Constants.addButtonContentInset - addButton.addTarget(self, action: #selector(addTapped), for: .touchUpInside) - addButton.accessibilityLabel = NSLocalizedString("Add", comment: "Accessibility label for add button to add items to the user's media library") - addButton.accessibilityHint = NSLocalizedString("Add new media", comment: "Accessibility hint for add button to add items to the user's media library") - - let addBarButton = UIBarButtonItem(customView: addButton) - barButtonItems.append(addBarButton) - } - - if blog.supports(.mediaDeletion) && assetCount > 0 { - let editButton = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(editTapped)) - editButton.accessibilityLabel = NSLocalizedString("Edit", comment: "Accessibility label for edit button to enable multi selection mode in the user's media library") - editButton.accessibilityHint = NSLocalizedString("Enter edit mode to enable multi select to delete", comment: "Accessibility hint for edit button to enable multi selection mode in the user's media library") - - barButtonItems.append(editButton) - - } - navigationItem.setRightBarButtonItems(barButtonItems, animated: false) - } - } - - fileprivate func updateNoResultsView(for assetCount: Int) { - - guard assetCount == 0 else { return } - - if isLoading { - noResultsView.configureForFetching() - } else { - noResultsView.removeFromView() - - if hasSearchQuery { - noResultsView.configureForNoSearchResult() - } else { - noResultsView.configureForNoAssets(userCanUploadMedia: blog.userCanUploadMedia) - } - } - } - - private func updateSearchBar(for assetCount: Int) { - let shouldShowBar = hasSearchQuery || assetCount > 0 - - if shouldShowBar { - showSearchBar() - if let searchBar = self.searchBar { - WPStyleGuide.configureSearchBar(searchBar) - } - } else { - hideSearchBar() - } - } - - private func reloadCell(for media: Media) { - visibleCells(for: media).forEach { cell in - cell.overlayView = nil - cell.asset = media - } - } - - private func updateCellProgress(_ progress: Double, for media: Media) { - visibleCells(for: media).forEach { cell in - if let overlayView = cell.overlayView as? CircularProgressView { - if progress < MediaLibraryViewController.uploadCompleteProgress { - overlayView.state = .progress(progress) - } else { - overlayView.state = .indeterminate - } - - configureAppearance(for: overlayView, with: media) - } - } - } - - private func configureAppearance(for overlayView: CircularProgressView, with media: Media) { - if media.localThumbnailURL != nil { - overlayView.backgroundColor = overlayView.backgroundColor?.withAlphaComponent(0.5) - } else { - overlayView.backgroundColor = overlayView.backgroundColor?.withAlphaComponent(1) - } - } - - private func showUploadingStateForCell(for media: Media) { - visibleCells(for: media).forEach { cell in - if let overlayView = cell.overlayView as? CircularProgressView { - overlayView.state = .indeterminate - } - } - } - - private func showFailedStateForCell(for media: Media) { - visibleCells(for: media).forEach { cell in - if let overlayView = cell.overlayView as? CircularProgressView { - overlayView.state = .retry - configureAppearance(for: overlayView, with: media) - } - } - } - - private func visibleCells(for media: Media) -> [WPMediaCollectionViewCell] { - guard let cells = collectionView?.visibleCells as? [WPMediaCollectionViewCell] else { - return [] - } - - return cells.filter({ ($0.asset as? Media) == media }) - } - - private var hasSearchQuery: Bool { - return (pickerDataSource.searchQuery ?? "").count > 0 - } - - // MARK: - Actions - - @objc fileprivate func addTapped() { - QuickStartTourGuide.shared.visited(.mediaUpload) - addButton.shouldShowSpotlight = QuickStartTourGuide.shared.isCurrentElement(.mediaUpload) - showOptionsMenu() - } - - private func showOptionsMenu() { - - let pickingContext: MediaPickingContext - if pickerDataSource.totalAssetCount > 0 { - pickingContext = MediaPickingContext(origin: self, view: view, barButtonItem: navigationItem.rightBarButtonItem, blog: blog) - } else { - pickingContext = MediaPickingContext(origin: self, view: noResultsView.actionButton, blog: blog) - } - - mediaPickingCoordinator.present(context: pickingContext) - } - - @objc private func editTapped() { - isEditing = !isEditing - } - - @objc private func trashTapped() { - let message: String - if selectedAssets.count == 1 { - message = NSLocalizedString("Are you sure you want to permanently delete this item?", comment: "Message prompting the user to confirm that they want to permanently delete a media item. Should match Calypso.") - } else { - message = NSLocalizedString("Are you sure you want to permanently delete these items?", comment: "Message prompting the user to confirm that they want to permanently delete a group of media items.") - } - - let alertController = UIAlertController(title: nil, - message: message, - preferredStyle: .alert) - alertController.addCancelActionWithTitle(NSLocalizedString("Cancel", comment: "Verb. Button title. Tapping cancels an action.")) - alertController.addDestructiveActionWithTitle(NSLocalizedString("Delete", comment: "Title for button that permanently deletes one or more media items (photos / videos)"), handler: { action in - self.deleteSelectedItems() - }) - - present(alertController, animated: true) - } - - private func deleteSelectedItems() { - guard selectedAssets.count > 0 else { return } - guard let assets = selectedAssets as? [Media] else { return } - - let deletedItemsCount = assets.count - - let updateProgress = { (progress: Progress?) in - let fractionCompleted = progress?.fractionCompleted ?? 0 - SVProgressHUD.showProgress(Float(fractionCompleted), status: NSLocalizedString("Deleting...", comment: "Text displayed in HUD while a media item is being deleted.")) - } - - SVProgressHUD.setDefaultMaskType(.clear) - SVProgressHUD.setMinimumDismissTimeInterval(1.0) - - // Initialize the progress HUD before we start - updateProgress(nil) - isEditing = false - - MediaCoordinator.shared.delete(media: assets, - onProgress: updateProgress, - success: { [weak self] in - WPAppAnalytics.track(.mediaLibraryDeletedItems, withProperties: ["number_of_items_deleted": deletedItemsCount], with: self?.blog) - SVProgressHUD.showSuccess(withStatus: NSLocalizedString("Deleted!", comment: "Text displayed in HUD after successfully deleting a media item")) - }, - failure: { - SVProgressHUD.showError(withStatus: NSLocalizedString("Unable to delete all media items.", comment: "Text displayed in HUD if there was an error attempting to delete a group of media items.")) - }) - } - - fileprivate func presentRetryOptions(for media: Media) { - let style: UIAlertController.Style = UIDevice.isPad() ? .alert : .actionSheet - let alertController = UIAlertController(title: nil, message: nil, preferredStyle: style) - alertController.addDestructiveActionWithTitle(NSLocalizedString("Cancel Upload", comment: "Media Library option to cancel an in-progress or failed upload.")) { _ in - MediaCoordinator.shared.delete(media: [media]) - } - - if media.remoteStatus == .failed { - if let error = media.error { - alertController.message = error.localizedDescription - } - if media.canRetry { - alertController.addDefaultActionWithTitle(NSLocalizedString("Retry Upload", comment: "User action to retry media upload.")) { _ in - let info = MediaAnalyticsInfo(origin: .mediaLibrary(.wpMediaLibrary)) - MediaCoordinator.shared.retryMedia(media, analyticsInfo: info) - } - } - } - - alertController.addCancelActionWithTitle( - NSLocalizedString( - "mediaLibrary.retryOptionsAlert.dismissButton", - value: "Dismiss", - comment: "Verb. Button title. Tapping dismisses a prompt." - ) - ) - - present(alertController, animated: true) - } - - override var isEditing: Bool { - didSet { - updateNavigationItemButtons(for: pickerDataSource.totalAssetCount) - let options = self.options.copy() as! WPMediaPickerOptions - options.allowMultipleSelection = isEditing - self.options = options - clearSelectedAssets(false) - } - } - - // MARK: - Media Library Change Observer - - private var mediaLibraryChangeObserverKey: NSObjectProtocol? = nil - - private func registerChangeObserver() { - assert(mediaLibraryChangeObserverKey == nil) - mediaLibraryChangeObserverKey = pickerDataSource.registerChangeObserverBlock({ [weak self] _, removed, inserted, _, _ in - guard let strongSelf = self else { return } - guard removed.count > 0 || inserted.count > 0 else { return } - - strongSelf.updateViewState(for: strongSelf.pickerDataSource.numberOfAssets()) - - if strongSelf.pickerDataSource.totalAssetCount > 0 { - strongSelf.updateNavigationItemButtonsForCurrentAssetSelection() - } else { - strongSelf.isEditing = false - } - - // If we're presenting an item and it's been deleted, pop the - // detail view off the stack - if let navigationController = strongSelf.navigationController, - navigationController.topViewController != strongSelf, - let asset = strongSelf.selectedAsset, - asset.isDeleted { - _ = strongSelf.navigationController?.popToViewController(strongSelf, animated: true) - } - }) - } - - private func unregisterChangeObserver() { - if let mediaLibraryChangeObserverKey = mediaLibraryChangeObserverKey { - pickerDataSource.unregisterChangeObserver(mediaLibraryChangeObserverKey) - } - } - - // MARK: - Upload Coordinator Observer - - private func registerUploadCoordinatorObserver() { - uploadObserverUUID = MediaCoordinator.shared.addObserver({ [weak self] (media, state) in - switch state { - case .progress(let progress): - if media.remoteStatus == .failed { - self?.showFailedStateForCell(for: media) - } else { - self?.updateCellProgress(progress, for: media) - } - case .processing, .uploading: - self?.showUploadingStateForCell(for: media) - case .ended: - self?.reloadCell(for: media) - case .failed: - self?.showFailedStateForCell(for: media) - case .thumbnailReady: - if media.remoteStatus == .failed { - self?.showFailedStateForCell(for: media) - } else { - self?.showUploadingStateForCell(for: media) - } - } - }, for: nil) - } - - private func unregisterUploadCoordinatorObserver() { - if let uuid = uploadObserverUUID { - MediaCoordinator.shared.removeObserver(withUUID: uuid) - } - } - - // MARK: ClipsToBounds KVO Observer - - /// The content view of the navigation bar causes the spotlight view on the add button to be clipped. - /// This ensures that `clipsToBounds` of the content view is always `false`. - /// Without this, `clipsToBounds` reverts to `true` at some point during the view lifecycle. This happens asynchronously, - /// so we can't confidently reset it. Hence the need for KVO. - private func startObservingNavigationBarClipsToBounds() { - kvoTokens = navigationController?.navigationBar.subviews.map({ subview in - return subview.observe(\.clipsToBounds, options: .new, changeHandler: { view, change in - guard let newValue = change.newValue, newValue else { return } - view.clipsToBounds = false - }) - }) - } - - private func stopObservingNavigationBarClipsToBounds() { - kvoTokens?.forEach({ $0.invalidate() }) - } -} - -// MARK: - PHPickerViewControllerDelegate - -extension MediaLibraryViewController: PHPickerViewControllerDelegate { - func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { - dismiss(animated: true) - - for result in results { - let info = MediaAnalyticsInfo(origin: .mediaLibrary(.deviceLibrary), selectionMethod: .fullScreenPicker) - MediaCoordinator.shared.addMedia(from: result.itemProvider, to: blog, analyticsInfo: info) - } - } -} - -// MARK: - ImagePickerControllerDelegate - -extension MediaLibraryViewController: ImagePickerControllerDelegate { - func imagePicker(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { - dismiss(animated: true) - - func addAsset(from exportableAsset: ExportableAsset) { - let info = MediaAnalyticsInfo(origin: .mediaLibrary(.camera), selectionMethod: .fullScreenPicker) - MediaCoordinator.shared.addMedia(from: exportableAsset, to: blog, analyticsInfo: info) - } - - guard let mediaType = info[.mediaType] as? String else { - return - } - switch mediaType { - case UTType.image.identifier: - if let image = info[.originalImage] as? UIImage { - addAsset(from: image) - } - - case UTType.movie.identifier: - guard let videoURL = info[.mediaURL] as? URL else { - return - } - guard self.blog.canUploadVideo(from: videoURL) else { - self.presentVideoLimitExceededAfterCapture(on: self) - return - } - addAsset(from: videoURL as NSURL) - default: - break - } - } -} - - -// MARK: - UIDocumentPickerDelegate - -extension MediaLibraryViewController: UIDocumentPickerDelegate { - func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { - for documentURL in urls as [NSURL] { - let info = MediaAnalyticsInfo(origin: .mediaLibrary(.otherApps), selectionMethod: .documentPicker) - MediaCoordinator.shared.addMedia(from: documentURL, to: blog, analyticsInfo: info) - } - } - - func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) { - dismiss(animated: true) - } -} - -// MARK: - NoResultsViewControllerDelegate - -extension MediaLibraryViewController: NoResultsViewControllerDelegate { - func actionButtonPressed() { - addTapped() - } -} - -// MARK: - User messages for video limits allowances -extension MediaLibraryViewController: VideoLimitsAlertPresenter {} - -// MARK: - WPMediaPickerViewControllerDelegate - -extension MediaLibraryViewController: WPMediaPickerViewControllerDelegate { - - func emptyViewController(forMediaPickerController picker: WPMediaPickerViewController) -> UIViewController? { - return noResultsView - } - - func mediaPickerController(_ picker: WPMediaPickerViewController, didUpdateSearchWithAssetCount assetCount: Int) { - updateNoResultsView(for: assetCount) - } - - func mediaPickerController(_ picker: WPMediaPickerViewController, didFinishPicking assets: [WPMediaAsset]) { - // We're only interested in the upload picker - guard picker != self else { return } - pickerDataSource.searchCancelled() - - dismiss(animated: true) - - guard ReachabilityUtils.isInternetReachable() else { - ReachabilityUtils.showAlertNoInternetConnection() - return - } - - guard let assets = assets as? [PHAsset], - assets.count > 0 else { return } - - for asset in assets { - let info = MediaAnalyticsInfo(origin: .mediaLibrary(.deviceLibrary), selectionMethod: .fullScreenPicker) - MediaCoordinator.shared.addMedia(from: asset, to: blog, analyticsInfo: info) - } - } - - func mediaPickerControllerDidCancel(_ picker: WPMediaPickerViewController) { - pickerDataSource.searchCancelled() - - dismiss(animated: true) - } - - func mediaPickerController(_ picker: WPMediaPickerViewController, willShowOverlayView overlayView: UIView, forCellFor asset: WPMediaAsset) { - guard let overlayView = overlayView as? CircularProgressView, - let media = asset as? Media else { - return - } - WPStyleGuide.styleProgressViewForMediaCell(overlayView) - switch media.remoteStatus { - case .processing: - if let progress = MediaCoordinator.shared.progress(for: media) { - overlayView.state = .progress(progress.fractionCompleted) - } else { - overlayView.state = .indeterminate - } - case .pushing: - if let progress = MediaCoordinator.shared.progress(for: media) { - overlayView.state = .progress(progress.fractionCompleted) - } - case .failed: - overlayView.state = .retry - default: break - } - configureAppearance(for: overlayView, with: media) - } - - func mediaPickerController(_ picker: WPMediaPickerViewController, shouldShowOverlayViewForCellFor asset: WPMediaAsset) -> Bool { - if picker != self, !blog.canUploadAsset(asset) { - return true - } - if let media = asset as? Media { - return media.remoteStatus != .sync - } - - return false - } - - func mediaPickerControllerShouldShowCustomHeaderView(_ picker: WPMediaPickerViewController) -> Bool { - guard picker != self else { - return false - } - - // Show the device media permissions header if photo library access is limited - return PHPhotoLibrary.authorizationStatus(for: .readWrite) == .limited - } - - func mediaPickerControllerReferenceSize(forCustomHeaderView picker: WPMediaPickerViewController) -> CGSize { - let header = DeviceMediaPermissionsHeader() - header.translatesAutoresizingMaskIntoConstraints = false - - return header.referenceSizeInView(picker.view) - } - - func mediaPickerController(_ picker: WPMediaPickerViewController, configureCustomHeaderView headerView: UICollectionReusableView) { - guard let headerView = headerView as? DeviceMediaPermissionsHeader else { - return - } - - headerView.presenter = picker - } - - func mediaPickerController(_ picker: WPMediaPickerViewController, previewViewControllerFor asset: WPMediaAsset) -> UIViewController? { - guard picker == self else { return WPAssetViewController(asset: asset) } - - guard let media = asset as? Media, - media.remoteStatus == .sync else { - return nil - } - - WPAppAnalytics.track(.mediaLibraryPreviewedItem, with: blog) - return mediaItemViewController(for: asset) - } - - func mediaPickerController(_ picker: WPMediaPickerViewController, shouldSelect asset: WPMediaAsset) -> Bool { - if picker != self, !blog.canUploadAsset(asset) { - presentVideoLimitExceededFromPicker(on: picker) - return false - } - - guard picker == self else { - return true - } - - guard let media = asset as? Media else { - return false - } - - guard !isEditing else { - return media.remoteStatus == .sync || media.remoteStatus == .failed - } - - switch media.remoteStatus { - case .failed, .pushing, .processing: - presentRetryOptions(for: media) - case .sync: - if let viewController = mediaItemViewController(for: asset) { - WPAppAnalytics.track(.mediaLibraryPreviewedItem, with: blog) - navigationController?.pushViewController(viewController, animated: true) - } - default: break - } - - return false - } - - func mediaPickerController(_ picker: WPMediaPickerViewController, didSelect asset: WPMediaAsset) { - guard picker == self else { return } - - updateNavigationItemButtonsForCurrentAssetSelection() - } - - func mediaPickerController(_ picker: WPMediaPickerViewController, didDeselect asset: WPMediaAsset) { - guard picker == self else { return } - - updateNavigationItemButtonsForCurrentAssetSelection() - } - - @objc func updateNavigationItemButtonsForCurrentAssetSelection() { - if isEditing { - // Check that our selected items haven't been deleted – we're notified - // of changes to the data source before the collection view has - // updated its selected assets. - guard let assets = (selectedAssets as? [Media]) else { return } - let existingAssets = assets.filter({ !$0.isDeleted }) - - navigationItem.rightBarButtonItem?.isEnabled = (existingAssets.count > 0) - } - } - - private func mediaItemViewController(for asset: WPMediaAsset) -> UIViewController? { - if isEditing { return nil } - - guard let asset = asset as? Media else { - return nil - } - - selectedAsset = asset - - return MediaItemViewController(media: asset) - } - - func mediaPickerControllerWillBeginLoadingData(_ picker: WPMediaPickerViewController) { - guard picker == self else { return } - - isLoading = true - - updateNoResultsView(for: pickerDataSource.numberOfAssets()) - } - - func mediaPickerControllerDidEndLoadingData(_ picker: WPMediaPickerViewController) { - guard picker == self else { return } - - isLoading = false - - updateViewState(for: pickerDataSource.numberOfAssets()) - } - - func mediaPickerController(_ picker: WPMediaPickerViewController, handleError error: Error) -> Bool { - guard picker == self else { return false } - - let nserror = error as NSError - if let mediaLibrary = self.blog.media, !mediaLibrary.isEmpty { - let title = NSLocalizedString("Unable to Sync", comment: "Title of error prompt shown when a sync the user initiated fails.") - WPError.showNetworkingNotice(title: title, error: nserror) - } - return true - } -} - -// MARK: - State restoration - -extension MediaLibraryViewController: UIViewControllerRestoration { - enum EncodingKey { - static let blogURL = "blogURL" - } - - static func viewController(withRestorationIdentifierPath identifierComponents: [String], - coder: NSCoder) -> UIViewController? { - guard let identifier = identifierComponents.last, - identifier == MediaLibraryViewController.restorationIdentifier else { - return nil - } - - guard let blogURL = coder.decodeObject(forKey: EncodingKey.blogURL) as? URL else { - return nil - } - - let context = ContextManager.sharedInstance().mainContext - guard let objectID = context.persistentStoreCoordinator?.managedObjectID(forURIRepresentation: blogURL), - let object = try? context.existingObject(with: objectID), - let blog = object as? Blog else { - return nil - } - return MediaLibraryViewController(blog: blog) - } - - override func encodeRestorableState(with coder: NSCoder) { - super.encodeRestorableState(with: coder) - - coder.encode(blog.objectID.uriRepresentation(), forKey: EncodingKey.blogURL) - } -} - -// MARK: Stock Photos Picker Delegate - -extension MediaLibraryViewController: StockPhotosPickerDelegate { - func stockPhotosPicker(_ picker: StockPhotosPicker, didFinishPicking assets: [StockPhotosMedia]) { - guard assets.count > 0 else { - return - } - - let mediaCoordinator = MediaCoordinator.shared - assets.forEach { stockPhoto in - let info = MediaAnalyticsInfo(origin: .mediaLibrary(.stockPhotos), selectionMethod: .fullScreenPicker) - mediaCoordinator.addMedia(from: stockPhoto, to: blog, analyticsInfo: info) - WPAnalytics.track(.stockMediaUploaded) - } - } -} - -// MARK: Tenor Picker Delegate - -extension MediaLibraryViewController: TenorPickerDelegate { - func tenorPicker(_ picker: TenorPicker, didFinishPicking assets: [TenorMedia]) { - guard assets.count > 0 else { - return - } - - let mediaCoordinator = MediaCoordinator.shared - assets.forEach { tenorMedia in - let info = MediaAnalyticsInfo(origin: .mediaLibrary(.tenor), selectionMethod: .fullScreenPicker) - mediaCoordinator.addMedia(from: tenorMedia, to: blog, analyticsInfo: info) - WPAnalytics.track(.tenorUploaded) - } - } -} - -// MARK: Constants - -extension MediaLibraryViewController { - private enum Constants { - static let addButtonSpotlightOffset = UIOffset(horizontal: 20, vertical: -10) - static let addButtonContentInset = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 0) - } -} diff --git a/WordPress/Classes/ViewRelated/Media/MediaPickerMenu.swift b/WordPress/Classes/ViewRelated/Media/MediaPickerMenu.swift index 90772c9a987d..dba110ab86d3 100644 --- a/WordPress/Classes/ViewRelated/Media/MediaPickerMenu.swift +++ b/WordPress/Classes/ViewRelated/Media/MediaPickerMenu.swift @@ -1,6 +1,5 @@ import UIKit import PhotosUI -import WPMediaPicker import UniformTypeIdentifiers import AVFoundation import CocoaLumberjack @@ -170,82 +169,37 @@ extension MediaPickerMenu { private static var strongDelegateKey: UInt8 = 0 } -// MARK: - MediaPickerMenu (WordPress Media) +// MARK: - MediaPickerMenu (Site Media) extension MediaPickerMenu { - func makeMediaAction(blog: Blog, delegate: MediaPickerViewControllerDelegate) -> UIAction { + /// Returns an action for selecting media from the media uploaded by the user + /// to their site. + func makeSiteMediaAction(blog: Blog, delegate: SiteMediaPickerViewControllerDelegate) -> UIAction { UIAction( title: Strings.pickFromMedia, image: UIImage(systemName: "photo.stack"), attributes: [], - handler: { _ in showMediaPicker(blog: blog, delegate: delegate) } + handler: { _ in showSiteMediaPicker(blog: blog, delegate: delegate) } ) } - func showMediaPicker(blog: Blog, delegate: MediaPickerViewControllerDelegate) { - let options = WPMediaPickerOptions() - options.showMostRecentFirst = true - if let filter { - switch filter { - case .images: - options.filter = [.image] - case .videos: - options.filter = [.video] - } - } - options.allowMultipleSelection = isMultipleSelectionEnabled - options.showSearchBar = true - options.badgedUTTypes = [UTType.gif.identifier] - options.preferredStatusBarStyle = WPStyleGuide.preferredStatusBarStyle - options.allowCaptureOfMedia = false - - let dataSource = MediaLibraryPickerDataSource(blog: blog) - dataSource.ignoreSyncErrors = true - - let delegate = PickerMenuMediaPickerViewControllerDelegate(delegate: delegate) - - let picker = WPNavigationMediaPickerViewController(options: options) - picker.showGroupSelector = false - picker.dataSource = dataSource - picker.delegate = delegate - picker.modalPresentationStyle = .formSheet - - objc_setAssociatedObject(picker, &MediaPickerMenu.dataSourceAssociatedKey, dataSource, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - objc_setAssociatedObject(picker, &MediaPickerMenu.delegateAssociatedKey, delegate, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - - presentingViewController?.present(picker, animated: true) + func showSiteMediaPicker(blog: Blog, delegate: SiteMediaPickerViewControllerDelegate) { + let viewController = SiteMediaPickerViewController( + blog: blog, + filter: filter.map { [$0.mediaType] }, + allowsMultipleSelection: isMultipleSelectionEnabled + ) + viewController.delegate = delegate + let navigation = UINavigationController(rootViewController: viewController) + presentingViewController?.present(navigation, animated: true) } - - private static var dataSourceAssociatedKey: UInt8 = 0 - private static var delegateAssociatedKey: UInt8 = 0 -} - -/// Exposes only a subset of `WPMediaPickerViewControllerDelegate` to the users. -protocol MediaPickerViewControllerDelegate: AnyObject { - func mediaPickerController(_ picker: WPMediaPickerViewController, didFinishPicking assets: [WPMediaAsset]) - func mediaPickerControllerDidCancel(_ picker: WPMediaPickerViewController) } -private final class PickerMenuMediaPickerViewControllerDelegate: NSObject, WPMediaPickerViewControllerDelegate { - weak var delegate: MediaPickerViewControllerDelegate? - - init(delegate: MediaPickerViewControllerDelegate) { - self.delegate = delegate - } - - func mediaPickerController(_ picker: WPMediaPickerViewController, didFinishPicking assets: [WPMediaAsset]) { - delegate?.mediaPickerController(picker, didFinishPicking: assets) - } - - func mediaPickerControllerDidCancel(_ picker: WPMediaPickerViewController) { - delegate?.mediaPickerControllerDidCancel(picker) - } -} // MARK: - MediaPickerMenu (Stock Photo) extension MediaPickerMenu { - func makeStockPhotos(blog: Blog, delegate: StockPhotosPickerDelegate) -> UIAction { + func makeStockPhotos(blog: Blog, delegate: ExternalMediaPickerViewDelegate) -> UIAction { UIAction( title: Strings.pickFromStockPhotos, image: UIImage(systemName: "photo.on.rectangle"), @@ -254,22 +208,30 @@ extension MediaPickerMenu { ) } - func showStockPhotosPicker(blog: Blog, delegate: StockPhotosPickerDelegate) { - guard let presentingViewController else { return } + func showStockPhotosPicker(blog: Blog, delegate: ExternalMediaPickerViewDelegate) { + guard let presentingViewController, + let api = blog.wordPressComRestApi() else { + return + } - let picker = StockPhotosPicker() + let picker = ExternalMediaPickerViewController( + dataSource: StockPhotosDataSource(service: DefaultStockPhotosService(api: api)), + source: .stockPhotos, + allowsMultipleSelection: isMultipleSelectionEnabled + ) + picker.title = Strings.pickFromStockPhotos + picker.welcomeView = StockPhotosWelcomeView() picker.delegate = delegate - picker.allowMultipleSelection = isMultipleSelectionEnabled - let stockPhotosViewController = picker.presentPicker(origin: presentingViewController, blog: blog) - objc_setAssociatedObject(stockPhotosViewController, &MediaPickerMenu.dataSourceAssociatedKey, picker, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + let navigation = UINavigationController(rootViewController: picker) + presentingViewController.present(navigation, animated: true) } } // MARK: - MediaPickerMenu (Free GIF, Tenor) extension MediaPickerMenu { - func makeFreeGIFAction(blog: Blog, delegate: TenorPickerDelegate) -> UIAction { + func makeFreeGIFAction(blog: Blog, delegate: ExternalMediaPickerViewDelegate) -> UIAction { UIAction( title: Strings.pickFromTenor, image: UIImage(systemName: "play.square.stack"), @@ -278,18 +240,21 @@ extension MediaPickerMenu { ) } - func showFreeGIFPicker(blog: Blog, delegate: TenorPickerDelegate) { + func showFreeGIFPicker(blog: Blog, delegate: ExternalMediaPickerViewDelegate) { guard let presentingViewController else { return } - let picker = TenorPicker() - picker.allowMultipleSelection = isMultipleSelectionEnabled + let picker = ExternalMediaPickerViewController( + dataSource: TenorDataSource(service: TenorService()), + source: .tenor, + allowsMultipleSelection: isMultipleSelectionEnabled + ) + picker.title = Strings.pickFromTenor + picker.welcomeView = TenorWelcomeView() picker.delegate = delegate - let tenorViewController = picker.presentPicker(origin: presentingViewController, blog: blog) - objc_setAssociatedObject(tenorViewController, &MediaPickerMenu.dataSourceAssociatedKey, picker, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + let navigation = UINavigationController(rootViewController: picker) + presentingViewController.present(navigation, animated: true) } - - private static var tenorPickereAssociatedKey: UInt8 = 0 } extension MediaPickerMenu.MediaFilter { @@ -300,6 +265,13 @@ extension MediaPickerMenu.MediaFilter { default: return nil } } + + var mediaType: MediaType { + switch self { + case .images: return .image + case .videos: return .video + } + } } private enum Strings { diff --git a/WordPress/Classes/ViewRelated/Media/MediaPreviewHelper.swift b/WordPress/Classes/ViewRelated/Media/MediaPreviewHelper.swift deleted file mode 100644 index 6ccf47d508eb..000000000000 --- a/WordPress/Classes/ViewRelated/Media/MediaPreviewHelper.swift +++ /dev/null @@ -1,83 +0,0 @@ - -import Foundation -import WPMediaPicker - -/// This class is intended to be used with WPMediaPicker delegate `mediaPickerController(_:previewViewControllerFor:selectedIndex:)` -/// making this implementation reusable with all the instances of the media picker. -/// -class MediaPreviewHelper: NSObject { - - let assets: [WPMediaAsset] - - init(assets: [WPMediaAsset]) { - self.assets = assets - super.init() - } - - /// Return a controller to show the given assets. - /// - /// - Parameters: - /// - selected: The selected index to be displayed by default. - /// - Returns: The controller to be displayed or nil if the asset is not an image. - func previewViewController(selectedIndex selected: Int) -> UIViewController? { - guard assets.count > 0, selected < assets.endIndex else { - return nil - } - - if assets.count > 1 { - return carouselController(with: assets, selectedIndex: selected) - } - - let selectedAsset = assets[selected] - return self.viewController(for: selectedAsset) - } - - private func carouselController(with assets: [WPMediaAsset], selectedIndex selected: Int) -> UIViewController { - let carouselViewController = WPCarouselAssetsViewController(assets: assets) - carouselViewController.setPreviewingAssetAt(selected, animated: false) - carouselViewController.carouselDelegate = self - return carouselViewController - } - - fileprivate func imageViewController(with mediaAsset: Media) -> UIViewController { - let imageController = WPImageViewController(media: mediaAsset) - imageController.shouldDismissWithGestures = false - return imageController - } - - private func viewController(for asset: WPMediaAsset) -> UIViewController? { - guard asset.assetType() == .image else { - return nil - } - - if let mediaAsset = asset as? Media { - return imageViewController(with: mediaAsset) - } else if let phasset = asset as? PHAsset { - let imageController = WPImageViewController(asset: phasset) - imageController.shouldDismissWithGestures = false - return imageController - } else if let mediaAsset = asset as? MediaExternalAsset { - let imageController = WPImageViewController(externalMediaURL: mediaAsset.URL, - andAsset: asset) - imageController.shouldDismissWithGestures = false - return imageController - } - - return nil - } -} - -extension MediaPreviewHelper: WPCarouselAssetsViewControllerDelegate { - func carouselController(_ controller: WPCarouselAssetsViewController, viewControllerFor asset: WPMediaAsset) -> UIViewController? { - return viewController(for: asset) - } - - func carouselController(_ controller: WPCarouselAssetsViewController, assetFor viewController: UIViewController) -> WPMediaAsset { - guard - let imageViewController = viewController as? WPImageViewController, - let asset = imageViewController.mediaAsset else { - fatalError() - } - return asset - } -} diff --git a/WordPress/Classes/ViewRelated/Media/MediaProgressCoordinatorNoticeViewModel.swift b/WordPress/Classes/ViewRelated/Media/MediaProgressCoordinatorNoticeViewModel.swift index eb26ad656da7..fdcc1cd8f71a 100644 --- a/WordPress/Classes/ViewRelated/Media/MediaProgressCoordinatorNoticeViewModel.swift +++ b/WordPress/Classes/ViewRelated/Media/MediaProgressCoordinatorNoticeViewModel.swift @@ -59,7 +59,7 @@ struct MediaProgressCoordinatorNoticeViewModel { } private var failureNotice: Notice { - var canRetry = failedMedia.allSatisfy(\.canRetry) + let canRetry = failedMedia.allSatisfy(\.canRetry) guard canRetry else { return Notice(title: title, message: message, diff --git a/WordPress/Classes/ViewRelated/Media/NSItemProvider+Exportable.swift b/WordPress/Classes/ViewRelated/Media/NSItemProvider+Exportable.swift index 93c8e09ac589..aa85e9be59a5 100644 --- a/WordPress/Classes/ViewRelated/Media/NSItemProvider+Exportable.swift +++ b/WordPress/Classes/ViewRelated/Media/NSItemProvider+Exportable.swift @@ -1,4 +1,5 @@ import UIKit +import UniformTypeIdentifiers extension NSItemProvider: ExportableAsset { public var assetMediaType: MediaType { @@ -12,3 +13,9 @@ extension NSItemProvider: ExportableAsset { } } } + +extension NSItemProvider { + func hasConformingType(_ type: UTType) -> Bool { + hasItemConformingToTypeIdentifier(type.identifier) + } +} diff --git a/WordPress/Classes/ViewRelated/Media/PHPickerController+Extensions.swift b/WordPress/Classes/ViewRelated/Media/PHPickerController+Extensions.swift index 8b43e4c3fbbe..80798bbec199 100644 --- a/WordPress/Classes/ViewRelated/Media/PHPickerController+Extensions.swift +++ b/WordPress/Classes/ViewRelated/Media/PHPickerController+Extensions.swift @@ -1,5 +1,6 @@ import UIKit import PhotosUI +import UniformTypeIdentifiers extension PHPickerFilter { init?(_ type: WPMediaType) { @@ -24,7 +25,24 @@ extension PHPickerResult { /// /// - parameter completion: The completion closure that gets called on the main thread. static func loadImage(for result: PHPickerResult, _ completion: @escaping (UIImage?, Error?) -> Void) { - loadImage(for: result.itemProvider, completion) + NSItemProvider.loadImage(for: result.itemProvider, completion) + } +} + +extension NSItemProvider { + // MARK: - Images + + @MainActor + static func image(for result: NSItemProvider) async throws -> UIImage { + try await withUnsafeThrowingContinuation { continuation in + NSItemProvider.loadImage(for: result) { image, error in + if let image { + continuation.resume(returning: image) + } else { + continuation.resume(throwing: error ?? URLError(.unknown)) + } + } + } } /// Retrieves an image for the given picker result. @@ -64,4 +82,45 @@ extension PHPickerResult { } } } + + // MARK: - Videos + + /// Exports video to the given URL. + /// + /// - returns: Returns a location of the exported file in the temporary directory. + @MainActor + static func video(for provider: NSItemProvider) async throws -> URL { + return try await withUnsafeThrowingContinuation { continuation in + provider.loadFileRepresentation(forTypeIdentifier: UTType.data.identifier) { url, error in + guard let url else { + continuation.resume(throwing: error ?? URLError(.unknown)) + return + } + do { + // important: The video has to be copied as it's get deleted + // the moment this function returns. + let copyURL = getTemporaryFolderURL() + .appendingPathComponent(url.lastPathComponent) + .incrementalFilename() + try FileManager.default.moveItem(at: url, to: copyURL) + continuation.resume(returning: copyURL) + } catch { + continuation.resume(throwing: error) + } + } + } + } + + static func removeTemporaryData() { + try? FileManager.default.removeItem(at: temporaryFolderURL) + } +} + +private let temporaryFolderURL = URL(fileURLWithPath: NSTemporaryDirectory()) + .appendingPathComponent("org.automattic.NSItemProvider", isDirectory: true) + +private func getTemporaryFolderURL() -> URL { + let folderURL = temporaryFolderURL + try? FileManager.default.createDirectory(at: folderURL, withIntermediateDirectories: true) + return folderURL } diff --git a/WordPress/Classes/ViewRelated/Media/Preview/MediaPreviewController.swift b/WordPress/Classes/ViewRelated/Media/Preview/MediaPreviewController.swift new file mode 100644 index 000000000000..6407c281c991 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Media/Preview/MediaPreviewController.swift @@ -0,0 +1,98 @@ +import UIKit + +protocol MediaPreviewControllerDataSource: AnyObject { + func numberOfPreviewItems(in controller: MediaPreviewController) -> Int + func previewController(_ controller: MediaPreviewController, previewItemAt index: Int) -> MediaPreviewItem? +} + +struct MediaPreviewItem { + let url: URL +} + +/// Allows you to preview media fullscreen. +final class MediaPreviewController: UIViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate { + weak var dataSource: MediaPreviewControllerDataSource? + + /// Sets the initial index of the preview item. + var currentPreviewItemIndex = 0 + private var numberOfItems: Int = 0 + + private let pageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal) + + override func viewDidLoad() { + super.viewDidLoad() + + configureNavigationItems() + configurePageViewController() + updateNavigationForCurrentViewController() + } + + private func configureNavigationItems() { + navigationItem.leftBarButtonItem = UIBarButtonItem(systemItem: .close, primaryAction: .init { [weak self] _ in + self?.presentingViewController?.dismiss(animated: true) + }) + } + + private func configurePageViewController() { + addChild(pageViewController) + view.addSubview(pageViewController.view) + pageViewController.view.translatesAutoresizingMaskIntoConstraints = false + view.pinSubviewToAllEdges(pageViewController.view) + + pageViewController.dataSource = self + pageViewController.delegate = self + + reloadData() + } + + private func reloadData() { + numberOfItems = dataSource?.numberOfPreviewItems(in: self) ?? 0 + if let page = makePageViewController(at: currentPreviewItemIndex) { + pageViewController.setViewControllers([page], direction: .forward, animated: false) + } + } + + private func makePageViewController(at index: Int) -> MediaPreviewItemViewController? { + guard index >= 0 && index < numberOfItems, + let item = dataSource?.previewController(self, previewItemAt: index) else { + return nil + } + let viewController = MediaPreviewItemViewController(externalMediaURL: item.url) + viewController.shouldDismissWithGestures = false + viewController.index = index + return viewController + } + + // MARK: - UIPageViewControllerDataSource + + func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { + let index = (viewController as! MediaPreviewItemViewController).index + return makePageViewController(at: index - 1) + } + + func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { + let index = (viewController as! MediaPreviewItemViewController).index + return makePageViewController(at: index + 1) + } + + // MARK: - UIPageViewControllerDelegate + + func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) { + updateNavigationForCurrentViewController() + } + + private func updateNavigationForCurrentViewController() { + guard let viewController = pageViewController.viewControllers?.first as? MediaPreviewItemViewController else { + return + } + navigationItem.title = String(format: Strings.title, String(viewController.index + 1), String(numberOfItems)) + } +} + +private final class MediaPreviewItemViewController: WPImageViewController { + var index = 0 +} + +private enum Strings { + static let title = NSLocalizedString("mediaPreview.NofM", value: "%@ of %@", comment: "Navigation title for media preview. Example: 1 of 3") +} diff --git a/WordPress/Classes/ViewRelated/Media/SiteMedia/Controllers/SiteMediaAddMediaMenuController.swift b/WordPress/Classes/ViewRelated/Media/SiteMedia/Controllers/SiteMediaAddMediaMenuController.swift index 3f9dfbbbd1ad..e4f5bc35f905 100644 --- a/WordPress/Classes/ViewRelated/Media/SiteMedia/Controllers/SiteMediaAddMediaMenuController.swift +++ b/WordPress/Classes/ViewRelated/Media/SiteMedia/Controllers/SiteMediaAddMediaMenuController.swift @@ -2,7 +2,7 @@ import UIKit import Photos import PhotosUI -final class SiteMediaAddMediaMenuController: NSObject, PHPickerViewControllerDelegate, ImagePickerControllerDelegate, StockPhotosPickerDelegate, TenorPickerDelegate, UIDocumentPickerDelegate { +final class SiteMediaAddMediaMenuController: NSObject, PHPickerViewControllerDelegate, ImagePickerControllerDelegate, ExternalMediaPickerViewDelegate, UIDocumentPickerDelegate { let blog: Blog let coordinator: MediaCoordinator @@ -13,7 +13,7 @@ final class SiteMediaAddMediaMenuController: NSObject, PHPickerViewControllerDel func makeMenu(for viewController: UIViewController) -> UIMenu { let menu = MediaPickerMenu(viewController: viewController, isMultipleSelectionEnabled: true) - return UIMenu(options: [.displayInline], children: [ + var children: [UIMenuElement] = [ UIMenu(options: [.displayInline], children: [ menu.makePhotosAction(delegate: self), ]), @@ -25,7 +25,18 @@ final class SiteMediaAddMediaMenuController: NSObject, PHPickerViewControllerDel menu.makeStockPhotos(blog: blog, delegate: self), menu.makeFreeGIFAction(blog: blog, delegate: self) ]) - ]) + ] + if let quotaUsageDescription = blog.quotaUsageDescription { + children += [ + UIAction(subtitle: quotaUsageDescription, handler: { _ in }) + ] + } + return UIMenu(options: [.displayInline], children: children) + } + + func showPhotosPicker(from viewController: UIViewController) { + MediaPickerMenu(viewController: viewController, isMultipleSelectionEnabled: true) + .showPhotosPicker(delegate: self) } // MARK: - PHPickerViewControllerDelegate @@ -33,9 +44,15 @@ final class SiteMediaAddMediaMenuController: NSObject, PHPickerViewControllerDel func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { picker.presentingViewController?.dismiss(animated: true) - for result in results { - let info = MediaAnalyticsInfo(origin: .mediaLibrary(.deviceLibrary), selectionMethod: .fullScreenPicker) - coordinator.addMedia(from: result.itemProvider, to: blog, analyticsInfo: info) + guard results.count > 0 else { + return + } + + MediaHelper.advertiseImageOptimization() { [self] in + for result in results { + let info = MediaAnalyticsInfo(origin: .mediaLibrary(.deviceLibrary), selectionMethod: .fullScreenPicker) + coordinator.addMedia(from: result.itemProvider, to: blog, analyticsInfo: info) + } } } @@ -54,7 +71,9 @@ final class SiteMediaAddMediaMenuController: NSObject, PHPickerViewControllerDel switch mediaType { case UTType.image.identifier: if let image = info[.originalImage] as? UIImage { - addAsset(from: image) + MediaHelper.advertiseImageOptimization() { + addAsset(from: image) + } } case UTType.movie.identifier: if let videoURL = info[.mediaURL] as? URL { @@ -65,23 +84,22 @@ final class SiteMediaAddMediaMenuController: NSObject, PHPickerViewControllerDel } } - // MARK: - StockPhotosPickerDelegate + // MARK: - ExternalMediaPickerViewDelegate - func stockPhotosPicker(_ picker: StockPhotosPicker, didFinishPicking assets: [StockPhotosMedia]) { + func externalMediaPickerViewController(_ viewController: ExternalMediaPickerViewController, didFinishWithSelection assets: [ExternalMediaAsset]) { + viewController.presentingViewController?.dismiss(animated: true) for asset in assets { - let info = MediaAnalyticsInfo(origin: .mediaLibrary(.stockPhotos), selectionMethod: .fullScreenPicker) + let info = MediaAnalyticsInfo(origin: .mediaLibrary(viewController.source), selectionMethod: .fullScreenPicker) coordinator.addMedia(from: asset, to: blog, analyticsInfo: info) - WPAnalytics.track(.stockMediaUploaded) - } - } - - // MARK: - TenorPickerDelegate - func tenorPicker(_ picker: TenorPicker, didFinishPicking assets: [TenorMedia]) { - for asset in assets { - let info = MediaAnalyticsInfo(origin: .mediaLibrary(.tenor), selectionMethod: .fullScreenPicker) - coordinator.addMedia(from: asset, to: blog, analyticsInfo: info) - WPAnalytics.track(.tenorUploaded) + switch viewController.source { + case .stockPhotos: + WPAnalytics.track(.stockMediaUploaded) + case .tenor: + WPAnalytics.track(.tenorUploaded) + default: + assertionFailure("Unsupported source: \(viewController.source)") + } } } diff --git a/WordPress/Classes/ViewRelated/Media/SiteMedia/Controllers/SiteMediaCollectionViewController.swift b/WordPress/Classes/ViewRelated/Media/SiteMedia/Controllers/SiteMediaCollectionViewController.swift index 0437124fc775..b161839b365c 100644 --- a/WordPress/Classes/ViewRelated/Media/SiteMedia/Controllers/SiteMediaCollectionViewController.swift +++ b/WordPress/Classes/ViewRelated/Media/SiteMedia/Controllers/SiteMediaCollectionViewController.swift @@ -5,15 +5,17 @@ protocol SiteMediaCollectionViewControllerDelegate: AnyObject { func siteMediaViewController(_ viewController: SiteMediaCollectionViewController, didUpdateSelection selection: [Media]) /// Return a non-nil value to allow adding media using the empty state. func makeAddMediaMenu(for viewController: SiteMediaCollectionViewController) -> UIMenu? + func siteMediaViewController(_ viewController: SiteMediaCollectionViewController, contextMenuFor media: Media, sourceView: UIView) -> UIMenu? } extension SiteMediaCollectionViewControllerDelegate { func siteMediaViewController(_ viewController: SiteMediaCollectionViewController, didUpdateSelection: [Media]) {} func makeAddMediaMenu(for viewController: SiteMediaCollectionViewController) -> UIMenu? { nil } + func siteMediaViewController(_ viewController: SiteMediaCollectionViewController, contextMenuFor media: Media, sourceView: UIView) -> UIMenu? { nil } } /// The internal view controller for managing the media collection view. -final class SiteMediaCollectionViewController: UIViewController, NSFetchedResultsControllerDelegate, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDataSourcePrefetching, UISearchResultsUpdating { +final class SiteMediaCollectionViewController: UIViewController, NSFetchedResultsControllerDelegate, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDataSourcePrefetching, UISearchResultsUpdating, UIGestureRecognizerDelegate, SiteMediaPageViewControllerDelegate { weak var delegate: SiteMediaCollectionViewControllerDelegate? private lazy var collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout) @@ -26,14 +28,21 @@ final class SiteMediaCollectionViewController: UIViewController, NSFetchedResult private var isSyncing = false private var syncError: Error? private var pendingChanges: [(UICollectionView) -> Void] = [] - private var selection = NSMutableOrderedSet() // `Media` private var viewModels: [NSManagedObjectID: SiteMediaCollectionCellViewModel] = [:] private let blog: Blog - private let filter: Set? + private var filter: Set? private let isShowingPendingUploads: Bool - private var isSelectionOrdered = false private let coordinator = MediaCoordinator.shared + // Selection management + private var selection = NSMutableOrderedSet() // `Media` + private var allowsMultipleSelection = false + private var isSelectionOrdered = false + private var isBatchSelectionUpdate = false + private var panGestureInitialIndexPath: IndexPath? + private var panGesturePeviousSelection: NSOrderedSet? + private lazy var panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(didRecognizePanGesture)) + private var emptyViewState: EmptyViewState = .hidden { didSet { guard oldValue != emptyViewState else { return } @@ -42,6 +51,7 @@ final class SiteMediaCollectionViewController: UIViewController, NSFetchedResult } static let spacing: CGFloat = 2 + static let spacingAspectRatio: CGFloat = 8 var selectedMedia: [Media] { guard let selection = selection.array as? [Media] else { @@ -101,6 +111,7 @@ final class SiteMediaCollectionViewController: UIViewController, NSFetchedResult view.addSubview(collectionView) collectionView.translatesAutoresizingMaskIntoConstraints = false collectionView.pinSubviewToAllEdges(view) + collectionView.accessibilityIdentifier = "MediaCollection" collectionView.dataSource = self collectionView.delegate = self @@ -108,6 +119,9 @@ final class SiteMediaCollectionViewController: UIViewController, NSFetchedResult collectionView.refreshControl = refreshControl refreshControl.addTarget(self, action: #selector(syncMedia), for: .valueChanged) + + collectionView.addGestureRecognizer(panGestureRecognizer) + panGestureRecognizer.delegate = self } private func configureSearchController() { @@ -117,9 +131,9 @@ final class SiteMediaCollectionViewController: UIViewController, NSFetchedResult } private func updateFlowLayoutItemSize() { - let spacing = SiteMediaCollectionViewController.spacing + let spacing = UserDefaults.standard.isMediaAspectRatioModeEnabled ? SiteMediaCollectionViewController.spacingAspectRatio : SiteMediaCollectionViewController.spacing let availableWidth = collectionView.bounds.width - let itemsPerRow = availableWidth < 450 ? 4 : 5 + let itemsPerRow = availableWidth < 500 ? 4 : 5 let cellWidth = ((availableWidth - spacing * CGFloat(itemsPerRow - 1)) / CGFloat(itemsPerRow)).rounded(.down) flowLayout.minimumInteritemSpacing = spacing @@ -128,7 +142,18 @@ final class SiteMediaCollectionViewController: UIViewController, NSFetchedResult flowLayout.itemSize = CGSize(width: cellWidth, height: cellWidth) } - // MARK: - Editing + func toggleAspectRatioMode() { + UserDefaults.standard.isMediaAspectRatioModeEnabled.toggle() + UIView.animate(withDuration: 0.33) { + self.updateFlowLayoutItemSize() + for cell in self.collectionView.visibleCells { + guard let cell = cell as? SiteMediaCollectionCell else { continue } + cell.configure(isAspectRatioModeEnabled: UserDefaults.standard.isMediaAspectRatioModeEnabled) + } + } + } + + // MARK: - Editing (Selection) func setEditing( _ isEditing: Bool, @@ -137,20 +162,27 @@ final class SiteMediaCollectionViewController: UIViewController, NSFetchedResult ) { guard self.isEditing != isEditing else { return } self.isEditing = isEditing + self.allowsMultipleSelection = allowsMultipleSelection self.isSelectionOrdered = isSelectionOrdered - self.collectionView.allowsMultipleSelection = isEditing && allowsMultipleSelection deselectAll() } - private func setSelect(_ isSelected: Bool, for media: Media) { - if isSelected { - selection.add(media) - } else { - selection.remove(media) + private func updateSelection(_ perform: () -> Void) { + guard !isBatchSelectionUpdate else { + return perform() + } + + let previousSelection = selectedMedia + + isBatchSelectionUpdate = true + perform() + isBatchSelectionUpdate = false + + for media in previousSelection where !selection.contains(media) { getViewModel(for: media).badge = nil } - if collectionView.allowsMultipleSelection { + if allowsMultipleSelection { for (index, media) in selection.enumerated() { if let media = media as? Media { getViewModel(for: media).badge = isSelectionOrdered ? .ordered(index: index) : .unordered @@ -158,22 +190,98 @@ final class SiteMediaCollectionViewController: UIViewController, NSFetchedResult assertionFailure("Invalid selection") } } - } else { - // Don't display a badge } delegate?.siteMediaViewController(self, didUpdateSelection: selectedMedia) + if !allowsMultipleSelection { + selection = [] + } } - private func deselectAll() { - for media in selection { - if let media = media as? Media { - getViewModel(for: media).badge = nil + func isSelected(_ media: Media) -> Bool { + selection.contains(media) + } + + func toggleSelection(for media: Media) { + setSelected(!selection.contains(media), for: media) + } + + private func setSelected(_ isSelected: Bool, for media: Media) { + updateSelection { + if isSelected { + selection.add(media) } else { - assertionFailure("Invalid selection") + selection.remove(media) } } - selection.removeAllObjects() - delegate?.siteMediaViewController(self, didUpdateSelection: []) + } + + private func deselectAll() { + updateSelection { + selection.removeAllObjects() + } + } + + @objc private func didRecognizePanGesture(_ gesture: UIPanGestureRecognizer) { + guard isEditing, allowsMultipleSelection else { return } + + switch gesture.state { + case .began: + panGestureInitialIndexPath = collectionView.indexPathForItem(at: gesture.location(in: collectionView)) + panGesturePeviousSelection = selection.copy() as? NSOrderedSet + case .changed: + guard let currentIndexPath = collectionView.indexPathForItem(at: gesture.location(in: collectionView)), + let panGestureInitialIndexPath, + let panGesturePeviousSelection else { return } + + let isDeselecting = panGesturePeviousSelection.contains(fetchController.object(at: panGestureInitialIndexPath)) + + updateSelection { + selection = NSMutableOrderedSet(orderedSet: panGesturePeviousSelection) + for index in stride(from: panGestureInitialIndexPath.item, through: currentIndexPath.item, by: currentIndexPath.item > panGestureInitialIndexPath.item ? 1 : -1) { + let media = fetchController.object(at: IndexPath(item: index, section: 0)) + isDeselecting ? selection.remove(media) : selection.add(media) + } + } + case .ended: + break + default: + break + } + } + + // MARK: - Filter + + func setMediaType(_ mediaType: MediaType?) { + if let mediaType { + self.filter = [mediaType] + } else { + self.filter = nil + } + reloadFetchController() + } + + // MARK: - UIGestureRecognizerDelegate (Selection) + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if gestureRecognizer === panGestureRecognizer { + return otherGestureRecognizer === collectionView.panGestureRecognizer + } + return true + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { + if gestureRecognizer === panGestureRecognizer { + return isEditing + } + return true + } + + func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + if gestureRecognizer === panGestureRecognizer { + let translation = panGestureRecognizer.translation(in: panGestureRecognizer.view) + return abs(translation.x) > abs(translation.y) + } + return true } // MARK: - Refresh @@ -248,6 +356,18 @@ final class SiteMediaCollectionViewController: UIViewController, NSFetchedResult return predicates.count == 1 ? predicates[0] : NSCompoundPredicate(andPredicateWithSubpredicates: predicates) } + private func reloadFetchController() { + let searchTerm = searchController.searchBar.text ?? "" + fetchController.fetchRequest.predicate = makePredicate(searchTerm: searchTerm) + do { + try fetchController.performFetch() + collectionView.reloadData() + updateEmptyViewState() + } catch { + WordPressAppDelegate.crashLogging?.logError(error) // Should never happen + } + } + // MARK: - NSFetchedResultsControllerDelegate func controllerWillChangeContent(_ controller: NSFetchedResultsController) { @@ -263,14 +383,8 @@ final class SiteMediaCollectionViewController: UIViewController, NSFetchedResult guard let indexPath else { return } pendingChanges.append({ $0.deleteItems(at: [indexPath]) }) if let media = anObject as? Media { - setSelect(false, for: media) - - if let viewController = navigationController?.topViewController, - viewController !== self, - let detailsViewController = viewController as? MediaItemViewController, - detailsViewController.media.objectID == media.objectID { - navigationController?.popViewController(animated: true) - } + setSelected(false, for: media) + didDeleteMedia(media, at: indexPath) } else { assertionFailure("Invalid object: \(anObject)") } @@ -286,6 +400,16 @@ final class SiteMediaCollectionViewController: UIViewController, NSFetchedResult } } + private func didDeleteMedia(_ media: Media, at indexPath: IndexPath) { + if let viewController = navigationController?.topViewController, + let detailsViewController = viewController as? SiteMediaPageViewController { + let before = indexPath.item > 0 ? fetchController.object(at: IndexPath(item: indexPath.item - 1, section: 0)) : nil + let after = indexPath.item < (fetchController.fetchedObjects?.count ?? 0) ? fetchController.object(at: IndexPath(item: indexPath.item, section: 0)) : nil + + detailsViewController.didDeleteItem(media, before: before, after: after) + } + } + func controllerDidChangeContent(_ controller: NSFetchedResultsController) { guard !pendingChanges.isEmpty else { return @@ -318,6 +442,7 @@ final class SiteMediaCollectionViewController: UIViewController, NSFetchedResult let media = fetchController.object(at: indexPath) let viewModel = getViewModel(for: media) cell.configure(viewModel: viewModel) + cell.configure(isAspectRatioModeEnabled: UserDefaults.standard.isMediaAspectRatioModeEnabled) return cell } @@ -326,25 +451,49 @@ final class SiteMediaCollectionViewController: UIViewController, NSFetchedResult func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { let media = fetchController.object(at: indexPath) if isEditing { - setSelect(true, for: media) + toggleSelection(for: media) } else { switch media.remoteStatus { case .failed, .pushing, .processing: showRetryOptions(for: media) case .sync: - let viewController = MediaItemViewController(media: media) WPAppAnalytics.track(.mediaLibraryPreviewedItem, with: blog) - navigationController?.pushViewController(viewController, animated: true) + + let viewController = SiteMediaPageViewController(media: media, delegate: self) + self.navigationController?.pushViewController(viewController, animated: true) default: break } } } - func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) { - if isEditing { - let media = fetchController.object(at: indexPath) - setSelect(false, for: media) + func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemsAt indexPaths: [IndexPath], point: CGPoint) -> UIContextMenuConfiguration? { + guard let indexPath = indexPaths.first else { + return nil + } + let media = fetchController.object(at: indexPath) + return UIContextMenuConfiguration(previewProvider: { [weak self] in + guard let self else { return nil } + return self.makePreviewViewController(for: media) + }, actionProvider: { [weak self] _ in + guard let self else { return nil } + let cell = collectionView.cellForItem(at: indexPath) + return self.delegate?.siteMediaViewController(self, contextMenuFor: media, sourceView: cell ?? self.view) + }) + } + + private func makePreviewViewController(for media: Media) -> UIViewController? { + let viewModel = getViewModel(for: media) + guard let image = viewModel.getCachedThubmnail() else { + return nil } + let imageView = UIImageView(image: image) + imageView.accessibilityIgnoresInvertColors = true + + let viewController = UIViewController() + viewController.view.addSubview(imageView) + viewController.view.pinSubviewToAllEdges(imageView) + viewController.preferredContentSize = image.size + return viewController } // MARK: - UICollectionViewDataSourcePrefetching @@ -357,7 +506,8 @@ final class SiteMediaCollectionViewController: UIViewController, NSFetchedResult } func collectionView(_ collectionView: UICollectionView, cancelPrefetchingForItemsAt indexPaths: [IndexPath]) { - for indexPath in indexPaths { + let count = fetchController.fetchedObjects?.count ?? 0 + for indexPath in indexPaths where indexPath.row < count { let media = fetchController.object(at: indexPath) getViewModel(for: media).cancelPrefetching() } @@ -366,15 +516,27 @@ final class SiteMediaCollectionViewController: UIViewController, NSFetchedResult // MARK: - UISearchResultsUpdating func updateSearchResults(for searchController: UISearchController) { - let searchTerm = searchController.searchBar.text ?? "" - fetchController.fetchRequest.predicate = makePredicate(searchTerm: searchTerm) - do { - try fetchController.performFetch() - collectionView.reloadData() - updateEmptyViewState() - } catch { - WordPressAppDelegate.crashLogging?.logError(error) // Should never happen + reloadFetchController() + } + + // MARK: - SiteMediaPageViewControllerDelegate + + func siteMediaPageViewController(_ viewController: SiteMediaPageViewController, getMediaBeforeMedia media: Media) -> Media? { + guard let fetchedObjects = fetchController.fetchedObjects, + let index = fetchedObjects.firstIndex(of: media), + index > 0 else { + return nil + } + return fetchedObjects[index - 1] + } + + func siteMediaPageViewController(_ viewController: SiteMediaPageViewController, getMediaAfterMedia media: Media) -> Media? { + guard let fetchedObjects = fetchController.fetchedObjects, + let index = fetchedObjects.firstIndex(of: media), + index < (fetchedObjects.count - 1) else { + return nil } + return fetchedObjects[index + 1] } // MARK: - Menus @@ -464,6 +626,7 @@ extension SiteMediaCollectionViewController: NoResultsViewHost { } private enum Strings { + static let title = NSLocalizedString("mediaLibrary.title", value: "Media", comment: "Media screen navigation title") static let syncFailed = NSLocalizedString("media.syncFailed", value: "Unable to sync media", comment: "Title of error prompt shown when a sync fails.") static let retryMenuRetry = NSLocalizedString("mediaLibrary.retryOptionsAlert.retry", value: "Retry Upload", comment: "User action to retry media upload.") static let retryMenuDelete = NSLocalizedString("mediaLibrary.retryOptionsAlert.delete", value: "Delete", comment: "User action to delete un-uploaded media.") diff --git a/WordPress/Classes/ViewRelated/Media/SiteMedia/Helpers/SiteMediaFilterButtonView.swift b/WordPress/Classes/ViewRelated/Media/SiteMedia/Helpers/SiteMediaFilterButtonView.swift new file mode 100644 index 000000000000..2245638237ce --- /dev/null +++ b/WordPress/Classes/ViewRelated/Media/SiteMedia/Helpers/SiteMediaFilterButtonView.swift @@ -0,0 +1,25 @@ +import UIKit + +struct SiteMediaFilter { + let mediaType: MediaType? + let title: String + let imageName: String? + + var image: UIImage? { imageName.flatMap { UIImage(systemName: $0) } } + + static let allFilters: [SiteMediaFilter] = [ + SiteMediaFilter(mediaType: nil, title: Strings.filterAll, imageName: nil), + SiteMediaFilter(mediaType: .image, title: Strings.filterImages, imageName: "photo"), + SiteMediaFilter(mediaType: .video, title: Strings.filterVideos, imageName: "video"), + SiteMediaFilter(mediaType: .document, title: Strings.filterDocuments, imageName: "folder"), + SiteMediaFilter(mediaType: .audio, title: Strings.filterAudio, imageName: "waveform") + ] +} + +private enum Strings { + static let filterAll = NSLocalizedString("mediaLibrary.filterAll", value: "All", comment: "The name of the media filter") + static let filterImages = NSLocalizedString("mediaLibrary.filterImages", value: "Images", comment: "The name of the media filter") + static let filterVideos = NSLocalizedString("mediaLibrary.filterVideos", value: "Videos", comment: "The name of the media filter") + static let filterDocuments = NSLocalizedString("mediaLibrary.filterDocuments", value: "Documents", comment: "The name of the media filter") + static let filterAudio = NSLocalizedString("mediaLibrary.filterAudio", value: "Audio", comment: "The name of the media filter") +} diff --git a/WordPress/Classes/ViewRelated/Media/SiteMedia/SiteMediaPageViewController.swift b/WordPress/Classes/ViewRelated/Media/SiteMedia/SiteMediaPageViewController.swift new file mode 100644 index 000000000000..82f50386f40d --- /dev/null +++ b/WordPress/Classes/ViewRelated/Media/SiteMedia/SiteMediaPageViewController.swift @@ -0,0 +1,96 @@ +import UIKit + +protocol SiteMediaPageViewControllerDelegate: AnyObject { + func siteMediaPageViewController(_ viewController: SiteMediaPageViewController, getMediaBeforeMedia media: Media) -> Media? + func siteMediaPageViewController(_ viewController: SiteMediaPageViewController, getMediaAfterMedia media: Media) -> Media? +} + +final class SiteMediaPageViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate { + private weak var siteMediaDelegate: SiteMediaPageViewControllerDelegate? + + init(media: Media, delegate: SiteMediaPageViewControllerDelegate) { + super.init(transitionStyle: .scroll, navigationOrientation: .horizontal) + siteMediaDelegate = delegate + + dataSource = self + self.delegate = self + + let page = makePageViewController(with: media) + setViewControllers([page], direction: .forward, animated: false) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .systemBackground + updateNavigationForCurrentViewController() + } + + func didDeleteItem(_ media: Media, before: Media?, after: Media?) { + guard let viewController = viewControllers?.first as? MediaItemViewController, + viewController.media == media else { + return + } + func showAdjacentPage() { + if let before { + setViewControllers([makePageViewController(with: before)], direction: .reverse, animated: true, completion: nil) + updateNavigationForCurrentViewController() + } else if let after { + setViewControllers([makePageViewController(with: after)], direction: .forward, animated: true, completion: nil) + updateNavigationForCurrentViewController() + } else { + navigationController?.popViewController(animated: true) + } + } + if let cell = viewController.tableView.cellForRow(at: IndexPath(row: 0, section: 0)) { + UIView.animate(withDuration: 0.4) { + cell.transform = CGAffineTransform(scaleX: 0.1, y: 0.1) + cell.alpha = 0.0 + } + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(300)) { + showAdjacentPage() + } + } else { + showAdjacentPage() + } + } + + private func makePageViewController(with media: Media) -> MediaItemViewController { + MediaItemViewController(media: media) + } + + // MARK: - UIPageViewControllerDataSource + + func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { + let current = (viewController as! MediaItemViewController).media + guard let media = siteMediaDelegate?.siteMediaPageViewController(self, getMediaBeforeMedia: current) else { + return nil + } + return makePageViewController(with: media) + } + + func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { + let current = (viewController as! MediaItemViewController).media + guard let media = siteMediaDelegate?.siteMediaPageViewController(self, getMediaAfterMedia: current) else { + return nil + } + return makePageViewController(with: media) + } + + // MARK: - UIPageViewControllerDelegate + + func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) { + updateNavigationForCurrentViewController() + } + + private func updateNavigationForCurrentViewController() { + if let viewController = viewControllers?.first { + navigationItem.title = viewController.title + navigationItem.rightBarButtonItems = viewController.navigationItem.rightBarButtonItems + } + } +} diff --git a/WordPress/Classes/ViewRelated/Media/SiteMedia/SiteMediaPickerViewController.swift b/WordPress/Classes/ViewRelated/Media/SiteMedia/SiteMediaPickerViewController.swift index 9a869f74676f..a42fd979d13c 100644 --- a/WordPress/Classes/ViewRelated/Media/SiteMedia/SiteMediaPickerViewController.swift +++ b/WordPress/Classes/ViewRelated/Media/SiteMedia/SiteMediaPickerViewController.swift @@ -12,7 +12,7 @@ final class SiteMediaPickerViewController: UIViewController, SiteMediaCollection private let collectionViewController: SiteMediaCollectionViewController private let toolbarItemTitle = SiteMediaSelectionTitleView() - private lazy var buttonDone = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(buttonDoneTapped)) + private lazy var buttonDone = UIBarButtonItem(title: Strings.add, style: .done, target: self, action: #selector(buttonDoneTapped)) weak var delegate: SiteMediaPickerViewControllerDelegate? @@ -31,6 +31,7 @@ final class SiteMediaPickerViewController: UIViewController, SiteMediaCollection title = Strings.title extendedLayoutIncludesOpaqueBars = true + modalPresentationStyle = .formSheet } required init?(coder: NSCoder) { @@ -43,24 +44,16 @@ final class SiteMediaPickerViewController: UIViewController, SiteMediaCollection collectionViewController.embed(in: self) collectionViewController.delegate = self - configureNavigationBarAppearance() - configurationNavigationItems() + configureDefaultNavigationBarAppearance() + configureNavigationItems() startSelection() } // MARK: - Configuration - private func configureNavigationBarAppearance() { - let appearance = UINavigationBarAppearance() - navigationItem.standardAppearance = appearance - navigationItem.compactAppearance = appearance - navigationItem.scrollEdgeAppearance = appearance - navigationItem.compactScrollEdgeAppearance = appearance - } - - private func configurationNavigationItems() { + private func configureNavigationItems() { let buttonCancel = UIBarButtonItem(systemItem: .cancel, primaryAction: UIAction { [weak self] _ in - self?.buttonDoneTapped() + self?.buttonCancelTapped() }) navigationItem.leftBarButtonItem = buttonCancel @@ -106,6 +99,13 @@ final class SiteMediaPickerViewController: UIViewController, SiteMediaCollection // MARK: - SiteMediaCollectionViewControllerDelegate + func siteMediaViewController(_ viewController: SiteMediaCollectionViewController, contextMenuFor media: Media, sourceView: UIView) -> UIMenu? { + let title = viewController.isSelected(media) ? Strings.deselect : Strings.select + return UIMenu(children: [UIAction(title: title, image: UIImage(systemName: "checkmark.circle")) { [weak self] _ in + self?.collectionViewController.toggleSelection(for: media) + }]) + } + func siteMediaViewController(_ viewController: SiteMediaCollectionViewController, didUpdateSelection selection: [Media]) { if !allowsMultipleSelection { if !selection.isEmpty { @@ -119,4 +119,7 @@ final class SiteMediaPickerViewController: UIViewController, SiteMediaCollection private enum Strings { static let title = NSLocalizedString("siteMediaPicker.title", value: "Media", comment: "Media screen navigation title") + static let add = NSLocalizedString("siteMediaPicker.add", value: "Add", comment: "Title for confirmation navigation bar button item") + static let select = NSLocalizedString("siteMediaPicker.select", value: "Select", comment: "Button selection media in media picker") + static let deselect = NSLocalizedString("siteMediaPicker.deselect", value: "Deselect", comment: "Button selection media in media picker") } diff --git a/WordPress/Classes/ViewRelated/Media/SiteMedia/SiteMediaViewController.swift b/WordPress/Classes/ViewRelated/Media/SiteMedia/SiteMediaViewController.swift index 129fed0b7e66..28bbafe3ab82 100644 --- a/WordPress/Classes/ViewRelated/Media/SiteMedia/SiteMediaViewController.swift +++ b/WordPress/Classes/ViewRelated/Media/SiteMedia/SiteMediaViewController.swift @@ -9,17 +9,23 @@ final class SiteMediaViewController: UIViewController, SiteMediaCollectionViewCo private lazy var collectionViewController = SiteMediaCollectionViewController(blog: blog) private lazy var buttonAddMedia = SpotlightableButton(type: .custom) private lazy var buttonAddMediaMenuController = SiteMediaAddMediaMenuController(blog: blog, coordinator: coordinator) + private var buttonFilter: UIButton? private lazy var toolbarItemDelete = UIBarButtonItem(barButtonSystemItem: .trash, target: self, action: #selector(buttonDeleteTapped)) private lazy var toolbarItemTitle = SiteMediaSelectionTitleView() private lazy var toolbarItemShare = UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(buttonShareTapped)) - @objc init(blog: Blog) { + private var isPreparingToShare = false + private var isFirstAppearance = true + private var showPicker = false + + @objc init(blog: Blog, showPicker: Bool = false) { self.blog = blog + self.showPicker = showPicker + super.init(nibName: nil, bundle: nil) hidesBottomBarWhenPushed = true - title = Strings.title extendedLayoutIncludesOpaqueBars = true } @@ -36,18 +42,63 @@ final class SiteMediaViewController: UIViewController, SiteMediaCollectionViewCo collectionViewController.delegate = self configureAddMediaButton() - configureNavigationBarAppearance() + configureDefaultNavigationBarAppearance() + configureNavigationTitle() refreshNavigationItems() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) + if isFirstAppearance { + navigationItem.hidesSearchBarWhenScrolling = false + } buttonAddMedia.shouldShowSpotlight = QuickStartTourGuide.shared.isCurrentElement(.mediaUpload) + + if showPicker && blog.userCanUploadMedia { + buttonAddMediaMenuController.showPhotosPicker(from: self) + showPicker = false + } + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if isFirstAppearance { + navigationItem.hidesSearchBarWhenScrolling = true + isFirstAppearance = false + } } // MARK: - Configuration + private func configureNavigationTitle() { + let menu = UIMenu(children: [ + UIMenu(options: [.displayInline], children: SiteMediaFilter.allFilters.map { filter in + UIAction(title: filter.title, image: filter.image) { [weak self] _ in + self?.didUpdateFilter(filter) + } + }), + UIDeferredMenuElement.uncached { [weak self] in + let isAspect = UserDefaults.standard.isMediaAspectRatioModeEnabled + let action = UIAction( + title: isAspect ? Strings.squareGrid : Strings.aspectRatioGrid, + image: UIImage(systemName: isAspect ? "rectangle.arrowtriangle.2.outward" : "rectangle.arrowtriangle.2.inward")) { [weak self] _ in + self?.collectionViewController.toggleAspectRatioMode() + } + $0([action]) + } + ]) + + let button = UIButton.makeMenu(title: Strings.title, menu: menu) + self.buttonFilter = button + if UIDevice.isPad() { + navigationItem.leftBarButtonItem = UIBarButtonItem(customView: button) + } else { + navigationItem.titleView = button + } + } + private func configureAddMediaButton() { let button = self.buttonAddMedia @@ -65,14 +116,6 @@ final class SiteMediaViewController: UIViewController, SiteMediaCollectionViewCo button.accessibilityHint = Strings.addButtonAccessibilityHint } - private func configureNavigationBarAppearance() { - let appearance = UINavigationBarAppearance() - navigationItem.standardAppearance = appearance - navigationItem.compactAppearance = appearance - navigationItem.scrollEdgeAppearance = appearance - navigationItem.compactScrollEdgeAppearance = appearance - } - private func refreshNavigationItems() { navigationItem.hidesBackButton = isEditing @@ -91,10 +134,17 @@ final class SiteMediaViewController: UIViewController, SiteMediaCollectionViewCo let selectButton = UIBarButtonItem(title: Strings.select, style: .plain, target: self, action: #selector(buttonSelectTapped)) rightBarButtonItems.append(selectButton) } + return rightBarButtonItems }() } + private func didUpdateFilter(_ filter: SiteMediaFilter) { + buttonFilter?.setTitle(filter.title, for: .normal) + buttonFilter?.sizeToFit() // Important! + collectionViewController.setMediaType(filter.mediaType) + } + // MARK: - Actions @objc private func buttonSelectTapped() { @@ -121,26 +171,32 @@ final class SiteMediaViewController: UIViewController, SiteMediaCollectionViewCo collectionViewController.setEditing(isEditing, allowsMultipleSelection: true) refreshNavigationItems() + updateToolbarItems() + navigationController?.setToolbarHidden(!isEditing, animated: true) + } - if isEditing && toolbarItems == nil { - var toolbarItems: [UIBarButtonItem] = [] - if blog.supports(.mediaDeletion) { - toolbarItems.append(toolbarItemDelete) - } - toolbarItems.append(UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)) - toolbarItems.append(UIBarButtonItem(customView: toolbarItemTitle)) - toolbarItems.append(UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)) + private func updateToolbarItems() { + guard isEditing else { return } + + var toolbarItems: [UIBarButtonItem] = [] + if blog.supports(.mediaDeletion) { + toolbarItems.append(toolbarItemDelete) + } + toolbarItems.append(UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)) + toolbarItems.append(UIBarButtonItem(customView: toolbarItemTitle)) + toolbarItems.append(UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)) + if isPreparingToShare { + toolbarItems.append(.activityIndicator) + } else { toolbarItems.append(toolbarItemShare) - self.toolbarItems = toolbarItems } - - navigationController?.setToolbarHidden(!isEditing, animated: true) + self.toolbarItems = toolbarItems } private func updateToolbarItemsState(for selection: [Media]) { - for toolbarItem in toolbarItems ?? [] { - toolbarItem.isEnabled = selection.count > 0 - } + toolbarItemDelete.isEnabled = selection.count > 0 + toolbarItemShare.isEnabled = selection.count > 0 + toolbarItemTitle.setSelection(selection) } @@ -185,18 +241,34 @@ final class SiteMediaViewController: UIViewController, SiteMediaCollectionViewCo // MARK: - Actions (Share) - private func shareSelectedMedia(_ selection: [Media], barButtonItem: UIBarButtonItem? = nil) { + private func shareSelectedMedia(_ selection: [Media], barButtonItem: UIBarButtonItem? = nil, sourceView: UIView? = nil) { guard !selection.isEmpty else { return } - // TODO: Add spinner (cancellable?) + + func setPreparingToShare(_ isSharing: Bool) { + isPreparingToShare = isSharing + updateToolbarItems() + } + + setPreparingToShare(true) + + WPAnalytics.track(.siteMediaShareTapped, properties: [ + "number_of_items": selection.count + ]) + Task { do { - // TODO: Add analytics let fileURLs = try await Media.downloadRemoteData(for: selection, blog: blog) let activityViewController = UIActivityViewController(activityItems: fileURLs, applicationActivities: nil) - activityViewController.popoverPresentationController?.barButtonItem = barButtonItem + if let popover = activityViewController.popoverPresentationController { + if let barButtonItem { + popover.barButtonItem = barButtonItem + } else { + popover.sourceView = sourceView ?? view + } + } activityViewController.completionWithItemsHandler = { [weak self] _, isCompleted, _, _ in if isCompleted { self?.setEditing(false) @@ -204,8 +276,10 @@ final class SiteMediaViewController: UIViewController, SiteMediaCollectionViewCo } present(activityViewController, animated: true, completion: nil) } catch { - // TODO: Add error handling + SVProgressHUD.showError(withStatus: Strings.sharingFailureMessage) } + + setPreparingToShare(false) } } @@ -218,6 +292,24 @@ final class SiteMediaViewController: UIViewController, SiteMediaCollectionViewCo func makeAddMediaMenu(for viewController: SiteMediaCollectionViewController) -> UIMenu? { buttonAddMediaMenuController.makeMenu(for: self) } + + func siteMediaViewController(_ viewController: SiteMediaCollectionViewController, contextMenuFor media: Media, sourceView: UIView) -> UIMenu? { + var actions: [UIAction] = [] + + actions.append(UIAction(title: Strings.buttonShare, image: UIImage(systemName: "square.and.arrow.up")) { [weak self] _ in + self?.shareSelectedMedia([media], sourceView: sourceView) + }) + if blog.supports(.mediaDeletion) { + actions.append(UIAction(title: Strings.buttonDelete, image: UIImage(systemName: "trash"), attributes: [.destructive]) { [weak self] _ in + self?.deleteSelectedMedia([media]) + }) + } + return UIMenu(children: actions) + } +} + +extension SiteMediaViewController { + static var sharingFailureMessage: String { Strings.sharingFailureMessage } } private enum Strings { @@ -232,4 +324,9 @@ private enum Strings { static let deletionProgressViewTitle = NSLocalizedString("mediaLibrary.deletionProgressViewTitle", value: "Deleting...", comment: "Text displayed in HUD while a media item is being deleted.") static let deletionSuccessMessage = NSLocalizedString("mediaLibrary.deletionSuccessMessage", value: "Deleted!", comment: "Text displayed in HUD after successfully deleting a media item") static let deletionFailureMessage = NSLocalizedString("mediaLibrary.deletionFailureMessage", value: "Unable to delete all media items.", comment: "Text displayed in HUD if there was an error attempting to delete a group of media items.") + static let sharingFailureMessage = NSLocalizedString("mediaLibrary.sharingFailureMessage", value: "Unable to share the selected items.", comment: "Text displayed in HUD if there was an error attempting to share a group of media items.") + static let buttonShare = NSLocalizedString("mediaLibrary.buttonShare", value: "Share", comment: "Context menu button") + static let buttonDelete = NSLocalizedString("mediaLibrary.buttonDelete", value: "Delete", comment: "Context menu button") + static let aspectRatioGrid = NSLocalizedString("mediaLibrary.aspectRatioGrid", value: "Aspect Ratio Grid", comment: "Button name in the more menu") + static let squareGrid = NSLocalizedString("mediaLibrary.squareGrid", value: "Square Grid", comment: "Button name in the more menu") } diff --git a/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCell.swift b/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCell.swift index b20bf21a922b..6291dc9c1ab7 100644 --- a/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCell.swift +++ b/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCell.swift @@ -3,15 +3,17 @@ import Combine import Gifu final class SiteMediaCollectionCell: UICollectionViewCell, Reusable { + private let imageContainerView = UIView() private let imageView = GIFImageView() private let overlayView = CircularProgressView() private let placeholderView = UIView() private var durationView: SiteMediaVideoDurationView? private var documentInfoView: SiteMediaDocumentInfoView? - private var badgeView: SiteMediaCollectionCellBadgeView? + private var selectionView: SiteMediaCollectionCellSelectionOverlayView? private var viewModel: SiteMediaCollectionCellViewModel? private var cancellables: [AnyCancellable] = [] + private var aspectRatioConstraint: NSLayoutConstraint? override init(frame: CGRect) { super.init(frame: frame) @@ -22,19 +24,36 @@ final class SiteMediaCollectionCell: UICollectionViewCell, Reusable { imageView.contentMode = .scaleAspectFill imageView.accessibilityIgnoresInvertColors = true + contentView.addSubview(imageContainerView) + imageContainerView.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + imageContainerView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), + imageContainerView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + ]) + NSLayoutConstraint.activate([ + imageContainerView.topAnchor.constraint(equalTo: contentView.topAnchor), + imageContainerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + imageContainerView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + imageContainerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor) + ].map { + $0.priority = .init(rawValue: 900) + return $0 + }) + overlayView.backgroundColor = .neutral(.shade70).withAlphaComponent(0.5) - contentView.addSubview(placeholderView) + imageContainerView.addSubview(placeholderView) placeholderView.translatesAutoresizingMaskIntoConstraints = false - contentView.pinSubviewToAllEdges(placeholderView) + imageContainerView.pinSubviewToAllEdges(placeholderView) - contentView.addSubview(imageView) + imageContainerView.addSubview(imageView) imageView.translatesAutoresizingMaskIntoConstraints = false - contentView.pinSubviewToAllEdges(imageView) + imageContainerView.pinSubviewToAllEdges(imageView) - contentView.addSubview(overlayView) + imageContainerView.addSubview(overlayView) overlayView.translatesAutoresizingMaskIntoConstraints = false - contentView.pinSubviewToAllEdges(overlayView) + imageContainerView.pinSubviewToAllEdges(overlayView) } required init?(coder: NSCoder) { @@ -51,8 +70,9 @@ final class SiteMediaCollectionCell: UICollectionViewCell, Reusable { imageView.prepareForReuse() imageView.image = nil imageView.alpha = 0 + placeholderView.alpha = 1 - badgeView?.isHidden = true + selectionView?.isHidden = true durationView?.isHidden = true documentInfoView?.isHidden = true } @@ -74,12 +94,8 @@ final class SiteMediaCollectionCell: UICollectionViewCell, Reusable { .sink { [weak self] in self?.didUpdateOverlayState($0) } .store(in: &cancellables) - viewModel.$badge - .sink { [weak self] in self?.didUpdateBadge($0) } - .store(in: &cancellables) - - viewModel.$durationText - .sink { [weak self] in self?.didUpdateDurationText($0) } + viewModel.$badge.combineLatest(viewModel.$durationText) + .sink { [weak self] in self?.didUpdate(badge: $0, durationText: $1) } .store(in: &cancellables) viewModel.$documentInfo @@ -91,37 +107,46 @@ final class SiteMediaCollectionCell: UICollectionViewCell, Reusable { viewModel.onAppear() } - // MARK: - Refresh + func configure(isAspectRatioModeEnabled: Bool) { + aspectRatioConstraint?.isActive = false + aspectRatioConstraint = nil - private func didUpdateOverlayState(_ state: CircularProgressView.State?) { - if let state { - overlayView.state = state - overlayView.isHidden = false - } else { - overlayView.isHidden = true + if isAspectRatioModeEnabled, let aspectRatio = viewModel?.aspectRatio { + let aspectRatioConstraint = imageContainerView.widthAnchor.constraint(equalTo: imageContainerView.heightAnchor, multiplier: aspectRatio) + aspectRatioConstraint.isActive = true + self.aspectRatioConstraint = aspectRatioConstraint } } - private func didUpdateBadge(_ badge: SiteMediaCollectionCellViewModel.BadgeType?) { + // MARK: - Refresh + + private func didUpdate(badge: SiteMediaCollectionCellViewModel.BadgeType?, durationText: String?) { if let badge { - let badgeView = getBadgeView() - badgeView.isHidden = false - badgeView.setBadge(badge) + let selectionView = getSelectionView() + selectionView.isHidden = false + selectionView.setBadge(badge) } else { - badgeView?.isHidden = true + selectionView?.isHidden = true } - } - private func didUpdateDurationText(_ text: String?) { - if let text { + if let durationText, badge == nil { let durationView = getDurationView() durationView.isHidden = false - durationView.textLabel.text = text + durationView.textLabel.text = durationText } else { durationView?.isHidden = true } } + private func didUpdateOverlayState(_ state: CircularProgressView.State?) { + if let state { + overlayView.state = state + overlayView.isHidden = false + } else { + overlayView.isHidden = true + } + } + private func didUpdateDocumentInfo(_ viewModel: SiteMediaDocumentInfoViewModel?) { if let viewModel { let documentInfoView = getDocumentInfoView() @@ -142,7 +167,7 @@ final class SiteMediaCollectionCell: UICollectionViewCell, Reusable { } private func setImage(_ image: UIImage) { - if let gif = image as? AnimatedImageWrapper, let data = gif.gifData { + if let gif = image as? AnimatedImage, let data = gif.gifData { imageView.animate(withGIFData: data) } else { imageView.image = image @@ -166,13 +191,13 @@ final class SiteMediaCollectionCell: UICollectionViewCell, Reusable { return documentInfoView } let documentInfoView = SiteMediaDocumentInfoView() - contentView.addSubview(documentInfoView) + imageContainerView.addSubview(documentInfoView) documentInfoView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - documentInfoView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor, constant: 0), - documentInfoView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: 0), - documentInfoView.leadingAnchor.constraint(greaterThanOrEqualTo: contentView.leadingAnchor, constant: 4), - documentInfoView.trailingAnchor.constraint(lessThanOrEqualTo: contentView.trailingAnchor, constant: -4) + documentInfoView.centerXAnchor.constraint(equalTo: imageContainerView.centerXAnchor, constant: 0), + documentInfoView.centerYAnchor.constraint(equalTo: imageContainerView.centerYAnchor, constant: 0), + documentInfoView.leadingAnchor.constraint(greaterThanOrEqualTo: imageContainerView.leadingAnchor, constant: 4), + documentInfoView.trailingAnchor.constraint(lessThanOrEqualTo: imageContainerView.trailingAnchor, constant: -4) ]) self.documentInfoView = documentInfoView return documentInfoView @@ -183,28 +208,25 @@ final class SiteMediaCollectionCell: UICollectionViewCell, Reusable { return durationView } let durationView = SiteMediaVideoDurationView() - contentView.addSubview(durationView) + imageContainerView.addSubview(durationView) durationView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - durationView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: 0), - durationView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: 0) + durationView.trailingAnchor.constraint(equalTo: imageContainerView.trailingAnchor, constant: 0), + durationView.bottomAnchor.constraint(equalTo: imageContainerView.bottomAnchor, constant: 0) ]) self.durationView = durationView return durationView } - private func getBadgeView() -> SiteMediaCollectionCellBadgeView { - if let badgeView { - return badgeView + private func getSelectionView() -> SiteMediaCollectionCellSelectionOverlayView { + if let selectionView { + return selectionView } - let badgeView = SiteMediaCollectionCellBadgeView() - contentView.addSubview(badgeView) - badgeView.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - badgeView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -4), - badgeView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -4) - ]) - self.badgeView = badgeView - return badgeView + let selectionView = SiteMediaCollectionCellSelectionOverlayView() + imageContainerView.addSubview(selectionView) + selectionView.translatesAutoresizingMaskIntoConstraints = false + imageContainerView.pinSubviewToAllEdges(selectionView) + self.selectionView = selectionView + return selectionView } } diff --git a/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCellBadgeView.swift b/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCellSelectionOverlayView.swift similarity index 60% rename from WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCellBadgeView.swift rename to WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCellSelectionOverlayView.swift index de914d899fb6..c8e5c724e5e9 100644 --- a/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCellBadgeView.swift +++ b/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCellSelectionOverlayView.swift @@ -1,6 +1,47 @@ import UIKit -final class SiteMediaCollectionCellBadgeView: UIView { +final class SiteMediaCollectionCellSelectionOverlayView: UIView { + private let overlayView = UIView() + private let badgeView = SiteMediaCollectionCellSelectionOverlayBadgeView() + + override init(frame: CGRect) { + super.init(frame: frame) + + addSubview(overlayView) + overlayView.backgroundColor = UIColor.white.withAlphaComponent(0.15) + overlayView.translatesAutoresizingMaskIntoConstraints = false + pinSubviewToAllEdges(overlayView) + + addSubview(badgeView) + badgeView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + badgeView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -4), + badgeView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -4) + ]) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setBadge(_ badge: SiteMediaCollectionCellViewModel.BadgeType) { + switch badge { + case .unordered: + badgeView.textLabel.attributedText = NSAttributedString(attachment: { + let attachment = NSTextAttachment() + let configuration = UIImage.SymbolConfiguration(font: UIFont.systemFont(ofSize: 11, weight: .semibold)) + attachment.image = UIImage(systemName: "checkmark", withConfiguration: configuration)?.withTintColor(.white, renderingMode: .alwaysTemplate) + return attachment + }(), attributes: [ + NSAttributedString.Key.baselineOffset: 1 // It doesn't appear visually centered othwerwise + ]) + case .ordered(let index): + badgeView.textLabel.text = (index + 1).description + } + } +} + +private final class SiteMediaCollectionCellSelectionOverlayBadgeView: UIView { let textLabel = UILabel() override init(frame: CGRect) { @@ -35,20 +76,4 @@ final class SiteMediaCollectionCellBadgeView: UIView { layer.cornerRadius = bounds.height / 2 } - - func setBadge(_ badge: SiteMediaCollectionCellViewModel.BadgeType) { - switch badge { - case .unordered: - textLabel.attributedText = NSAttributedString(attachment: { - let attachment = NSTextAttachment() - let configuration = UIImage.SymbolConfiguration(font: UIFont.systemFont(ofSize: 11, weight: .semibold)) - attachment.image = UIImage(systemName: "checkmark", withConfiguration: configuration)?.withTintColor(.white, renderingMode: .alwaysTemplate) - return attachment - }(), attributes: [ - NSAttributedString.Key.baselineOffset: 1 // It doesn't appear visually centered othwerwise - ]) - case .ordered(let index): - textLabel.text = (index + 1).description - } - } } diff --git a/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCellViewModel.swift b/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCellViewModel.swift index a72848f73ac3..5c32bf76fe12 100644 --- a/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCellViewModel.swift +++ b/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCellViewModel.swift @@ -15,7 +15,6 @@ final class SiteMediaCollectionCellViewModel { private let mediaType: MediaType private let service: MediaImageService private let coordinator: MediaCoordinator - private let cache: MemoryCache private var isVisible = false private var isPrefetchingNeeded = false @@ -23,6 +22,14 @@ final class SiteMediaCollectionCellViewModel { private var progressObserver: NSKeyValueObservation? private var observations: [NSKeyValueObservation] = [] + var aspectRatio: CGFloat? { + guard let width = media.width?.floatValue, width > 0, + let height = media.height?.floatValue, height > 0 else { + return nil + } + return CGFloat(width / height) + } + enum BadgeType { case unordered case ordered(index: Int) @@ -34,14 +41,12 @@ final class SiteMediaCollectionCellViewModel { init(media: Media, service: MediaImageService = .shared, - coordinator: MediaCoordinator = .shared, - cache: MemoryCache = .shared) { + coordinator: MediaCoordinator = .shared) { self.mediaID = TaggedManagedObjectID(media) self.media = media self.mediaType = media.mediaType self.service = service self.coordinator = coordinator - self.cache = cache observations.append(media.observe(\.remoteStatusNumber, options: [.initial, .new]) { [weak self] _, _ in self?.updateOverlayState() @@ -114,7 +119,7 @@ final class SiteMediaCollectionCellViewModel { } imageTask = Task { @MainActor [service, media, weak self] in do { - let image = try await service.thumbnail(for: media) + let image = try await service.image(for: media, size: .small) self?.didFinishLoading(with: image) } catch { self?.didFinishLoading(with: nil) @@ -129,9 +134,6 @@ final class SiteMediaCollectionCellViewModel { } private func didFinishLoading(with image: UIImage?) { - if let image { - cache.setImage(image, forKey: makeCacheKey(for: media)) - } if !Task.isCancelled { if let image { onImageLoaded?(image) @@ -145,12 +147,9 @@ final class SiteMediaCollectionCellViewModel { /// Returns the image from the memory cache. func getCachedThubmnail() -> UIImage? { - guard supportsThumbnails else { return nil} - return cache.getImage(forKey: makeCacheKey(for: media)) - } - - private func makeCacheKey(for media: Media) -> String { - "thumbnail-\(media.objectID)" + guard supportsThumbnails else { return nil } + let mediaID = TaggedManagedObjectID(media) + return service.getCachedThumbnail(for: mediaID, size: .small) } // Monitors thumbnails generated by `MediaImportService`. diff --git a/WordPress/Classes/ViewRelated/Media/SolidColorActivityIndicator.swift b/WordPress/Classes/ViewRelated/Media/SolidColorActivityIndicator.swift new file mode 100644 index 000000000000..0a8a87876954 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Media/SolidColorActivityIndicator.swift @@ -0,0 +1,20 @@ +import Foundation + +final class SolidColorActivityIndicator: UIView, ActivityIndicatorType { + init(color: UIColor = .secondarySystemBackground) { + super.init(frame: .zero) + backgroundColor = color + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func startAnimating() { + isHidden = false + } + + func stopAnimating() { + isHidden = true + } +} diff --git a/WordPress/Classes/ViewRelated/Media/StockPhotos/AztecMediaPickingCoordinator.swift b/WordPress/Classes/ViewRelated/Media/StockPhotos/AztecMediaPickingCoordinator.swift index c9b108a8772b..f152e45c0c24 100644 --- a/WordPress/Classes/ViewRelated/Media/StockPhotos/AztecMediaPickingCoordinator.swift +++ b/WordPress/Classes/ViewRelated/Media/StockPhotos/AztecMediaPickingCoordinator.swift @@ -1,22 +1,16 @@ import MobileCoreServices -import WPMediaPicker /// Prepares the alert controller that will be presented when tapping the "more" button in Aztec's Format Bar final class AztecMediaPickingCoordinator { - typealias PickersDelegate = StockPhotosPickerDelegate & TenorPickerDelegate - private weak var delegate: PickersDelegate? - private var tenor: TenorPicker? - private let stockPhotos = StockPhotosPicker() + private weak var delegate: ExternalMediaPickerViewDelegate? - init(delegate: PickersDelegate) { + init(delegate: ExternalMediaPickerViewDelegate) { self.delegate = delegate - stockPhotos.delegate = delegate } - func present(context: MediaPickingContext) { - let origin = context.origin - let blog = context.blog - let fromView = context.view + func present(in origin: UIViewController & UIDocumentPickerDelegate, + blog: Blog) { + let fromView = origin.view ?? UIView() let alertController = UIAlertController(title: nil, message: nil, @@ -62,14 +56,13 @@ final class AztecMediaPickingCoordinator { } private func showStockPhotos(origin: UIViewController, blog: Blog) { - stockPhotos.presentPicker(origin: origin, blog: blog) + MediaPickerMenu(viewController: origin, isMultipleSelectionEnabled: true) + .showStockPhotosPicker(blog: blog, delegate: self) } private func showTenor(origin: UIViewController, blog: Blog) { - let picker = TenorPicker() - picker.delegate = self - picker.presentPicker(origin: origin, blog: blog) - tenor = picker + MediaPickerMenu(viewController: origin, isMultipleSelectionEnabled: true) + .showFreeGIFPicker(blog: blog, delegate: self) } private func showDocumentPicker(origin: UIViewController & UIDocumentPickerDelegate, blog: Blog) { @@ -81,9 +74,8 @@ final class AztecMediaPickingCoordinator { } } -extension AztecMediaPickingCoordinator: TenorPickerDelegate { - func tenorPicker(_ picker: TenorPicker, didFinishPicking assets: [TenorMedia]) { - delegate?.tenorPicker(picker, didFinishPicking: assets) - tenor = nil +extension AztecMediaPickingCoordinator: ExternalMediaPickerViewDelegate { + func externalMediaPickerViewController(_ viewController: ExternalMediaPickerViewController, didFinishWithSelection selection: [ExternalMediaAsset]) { + delegate?.externalMediaPickerViewController(viewController, didFinishWithSelection: selection) } } diff --git a/WordPress/Classes/ViewRelated/Media/StockPhotos/MediaPickingContext.swift b/WordPress/Classes/ViewRelated/Media/StockPhotos/MediaPickingContext.swift deleted file mode 100644 index 8aea87d06c5c..000000000000 --- a/WordPress/Classes/ViewRelated/Media/StockPhotos/MediaPickingContext.swift +++ /dev/null @@ -1,14 +0,0 @@ -/// Encapsulates context parameters to initiate a flow to pick media from several sources -struct MediaPickingContext { - let origin: UIViewController & UIDocumentPickerDelegate - let view: UIView - let barButtonItem: UIBarButtonItem? - let blog: Blog - - init(origin: UIViewController & UIDocumentPickerDelegate, view: UIView, barButtonItem: UIBarButtonItem? = nil, blog: Blog) { - self.origin = origin - self.view = view - self.barButtonItem = barButtonItem - self.blog = blog - } -} diff --git a/WordPress/Classes/ViewRelated/Media/StockPhotos/NoResultsStockPhotosConfiguration.swift b/WordPress/Classes/ViewRelated/Media/StockPhotos/NoResultsStockPhotosConfiguration.swift deleted file mode 100644 index 87cc912267a6..000000000000 --- a/WordPress/Classes/ViewRelated/Media/StockPhotos/NoResultsStockPhotosConfiguration.swift +++ /dev/null @@ -1,44 +0,0 @@ -// Empty state for Stock Photos - -struct NoResultsStockPhotosConfiguration { - - static func configureAsIntro(_ viewController: NoResultsViewController) { - viewController.configure(title: .freePhotosPlaceholderTitle, - buttonTitle: nil, - subtitle: nil, - attributedSubtitle: attributedSubtitle(), - image: Constants.imageName, - accessoryView: nil) - - viewController.view.layoutIfNeeded() - } - - static func configureAsLoading(_ viewController: NoResultsViewController) { - viewController.configure(title: .freePhotosSearchLoading, - buttonTitle: nil, - subtitle: nil, - attributedSubtitle: nil, - image: Constants.imageName, - accessoryView: nil) - - viewController.view.layoutIfNeeded() - } - - static func configure(_ viewController: NoResultsViewController) { - viewController.configureForNoSearchResults(title: .freePhotosSearchNoResult) - viewController.view.layoutIfNeeded() - } - - private enum Constants { - static let companyUrl = "https://www.pexels.com" - static let companyName = "Pexels" - static let imageName = "media-no-results" - } - - static func attributedSubtitle() -> NSAttributedString { - let subtitle: String = .freePhotosPlaceholderSubtitle - let htmlTaggedLink = "\(Constants.companyName)" - let htmlTaggedText = subtitle.replacingOccurrences(of: Constants.companyName, with: htmlTaggedLink) - return NSAttributedString.attributedStringWithHTML(htmlTaggedText, attributes: nil) - } -} diff --git a/WordPress/Classes/ViewRelated/Media/StockPhotos/NullStockPhotosService.swift b/WordPress/Classes/ViewRelated/Media/StockPhotos/NullStockPhotosService.swift deleted file mode 100644 index 842b7ae67832..000000000000 --- a/WordPress/Classes/ViewRelated/Media/StockPhotos/NullStockPhotosService.swift +++ /dev/null @@ -1,7 +0,0 @@ -/// Null implementation of the Stock Photos Service. This implementation will always return empty results -final class NullStockPhotosService: StockPhotosService { - func search(params: StockPhotosSearchParams, completion: @escaping (StockPhotosResultsPage) -> Void) { - let emptyPage = StockPhotosResultsPage.empty() - completion(emptyPage) - } -} diff --git a/WordPress/Classes/ViewRelated/Media/StockPhotos/StockPhotosDataSource.swift b/WordPress/Classes/ViewRelated/Media/StockPhotos/StockPhotosDataSource.swift index cdd76df97ea0..d62ef1962320 100644 --- a/WordPress/Classes/ViewRelated/Media/StockPhotos/StockPhotosDataSource.swift +++ b/WordPress/Classes/ViewRelated/Media/StockPhotos/StockPhotosDataSource.swift @@ -1,14 +1,12 @@ -import WPMediaPicker - +import Foundation /// Data Source for Stock Photos -final class StockPhotosDataSource: NSObject, WPMediaCollectionDataSource { - fileprivate static let paginationThreshold = 10 +final class StockPhotosDataSource: ExternalMediaDataSource { + private(set) var assets = [ExternalMediaAsset]() - fileprivate var photosMedia = [StockPhotosMedia]() - var observers = [String: WPMediaChangesBlock]() private var dataLoader: StockPhotosDataLoader? + var onUpdatedAssets: (() -> Void)? var onStartLoading: (() -> Void)? var onStopLoading: (() -> Void)? @@ -17,25 +15,13 @@ final class StockPhotosDataSource: NSObject, WPMediaCollectionDataSource { private(set) var searchQuery: String = "" init(service: StockPhotosService) { - super.init() self.dataLoader = StockPhotosDataLoader(service: service, delegate: self) } - func clearSearch(notifyObservers shouldNotify: Bool) { - photosMedia.removeAll() - if shouldNotify { - notifyObservers() - } - } - - func numberOfGroups() -> Int { - return 1 - } - - func search(for searchText: String?) { - searchQuery = searchText ?? "" + func search(for searchText: String) { + searchQuery = searchText - guard searchText?.isEmpty == false else { + guard searchText.count > 1 else { clearSearch(notifyObservers: true) scheduler.cancel() return @@ -52,110 +38,15 @@ final class StockPhotosDataSource: NSObject, WPMediaCollectionDataSource { dataLoader?.search(params) } - func group(at index: Int) -> WPMediaGroup { - return StockPhotosMediaGroup() - } - - func selectedGroup() -> WPMediaGroup? { - return StockPhotosMediaGroup() - } - - func numberOfAssets() -> Int { - return photosMedia.count - } - - func media(at index: Int) -> WPMediaAsset { - fetchMoreContentIfNecessary(index) - return photosMedia[index] - } - - func media(withIdentifier identifier: String) -> WPMediaAsset? { - return photosMedia.filter { $0.identifier() == identifier }.first - } - - func registerChangeObserverBlock(_ callback: @escaping WPMediaChangesBlock) -> NSObjectProtocol { - let blockKey = UUID().uuidString - observers[blockKey] = callback - return blockKey as NSString - } - - func unregisterChangeObserver(_ blockKey: NSObjectProtocol) { - guard let key = blockKey as? String else { - assertionFailure("blockKey must be of type String") - return - } - observers.removeValue(forKey: key) - } - - func registerGroupChangeObserverBlock(_ callback: @escaping WPMediaGroupChangesBlock) -> NSObjectProtocol { - // The group never changes - return NSNull() - } - - func unregisterGroupChangeObserver(_ blockKey: NSObjectProtocol) { - // The group never changes - } - - func loadData(with options: WPMediaLoadOptions, success successBlock: WPMediaSuccessBlock?, failure failureBlock: WPMediaFailureBlock? = nil) { - successBlock?() - } - - func mediaTypeFilter() -> WPMediaType { - return .image - } - - func ascendingOrdering() -> Bool { - return true - } - - func searchCancelled() { - searchQuery = "" - clearSearch(notifyObservers: true) - } - - // MARK: Unnused protocol methods - - func setSelectedGroup(_ group: WPMediaGroup) { - // - } - - func add(_ image: UIImage, metadata: [AnyHashable: Any]?, completionBlock: WPMediaAddedBlock? = nil) { - // - } - - func addVideo(from url: URL, completionBlock: WPMediaAddedBlock? = nil) { - // - } - - func setMediaTypeFilter(_ filter: WPMediaType) { - // - } - - func setAscendingOrdering(_ ascending: Bool) { - // - } -} - -// MARK: - Helpers - -extension StockPhotosDataSource { - private func notifyObservers(incremental: Bool = false, inserted: IndexSet = IndexSet()) { - observers.forEach { - $0.value(incremental, IndexSet(), inserted, IndexSet(), []) - } - } -} - -// MARK: - Pagination -extension StockPhotosDataSource { - fileprivate func fetchMoreContentIfNecessary(_ index: Int) { - if shoudLoadMore(index) { - dataLoader?.loadNextPage() + private func clearSearch(notifyObservers shouldNotify: Bool) { + assets.removeAll() + if shouldNotify { + onUpdatedAssets?() } } - private func shoudLoadMore(_ index: Int) -> Bool { - return index + type(of: self).paginationThreshold >= numberOfAssets() + func loadMore() { + dataLoader?.loadNextPage() } } @@ -170,26 +61,12 @@ extension StockPhotosDataSource: StockPhotosDataLoaderDelegate { return } + assert(Thread.isMainThread) if reset { - overwriteMedia(with: media) + self.assets = media } else { - appendMedia(with: media) + self.assets += media } - } - - private func overwriteMedia(with media: [StockPhotosMedia]) { - photosMedia = media - notifyObservers(incremental: false) - } - - private func appendMedia(with media: [StockPhotosMedia]) { - let currentMaxIndex = photosMedia.count - let newMaxIndex = currentMaxIndex + media.count - 1 - - let isIncremental = currentMaxIndex != 0 - let insertedIndexes = IndexSet(integersIn: currentMaxIndex...newMaxIndex) - - photosMedia.append(contentsOf: media) - notifyObservers(incremental: isIncremental, inserted: insertedIndexes) + onUpdatedAssets?() } } diff --git a/WordPress/Classes/ViewRelated/Media/StockPhotos/StockPhotosMedia.swift b/WordPress/Classes/ViewRelated/Media/StockPhotos/StockPhotosMedia.swift index ca5c36fa8fa5..f5f103effbf0 100644 --- a/WordPress/Classes/ViewRelated/Media/StockPhotos/StockPhotosMedia.swift +++ b/WordPress/Classes/ViewRelated/Media/StockPhotos/StockPhotosMedia.swift @@ -1,12 +1,4 @@ import Foundation -import WPMediaPicker - -struct ThumbnailCollection { - private(set) var largeURL: URL - private(set) var mediumURL: URL - private(set) var postThumbnailURL: URL - private(set) var thumbnailURL: URL -} /// Models a Stock Photo /// @@ -19,6 +11,13 @@ final class StockPhotosMedia: NSObject { private(set) var size: CGSize private(set) var thumbnails: ThumbnailCollection + struct ThumbnailCollection { + private(set) var largeURL: URL + private(set) var mediumURL: URL + private(set) var postThumbnailURL: URL + private(set) var thumbnailURL: URL + } + init(id: String, URL: URL, title: String, name: String, caption: String, size: CGSize, thumbnails: ThumbnailCollection) { self.id = id self.URL = URL @@ -30,81 +29,15 @@ final class StockPhotosMedia: NSObject { } } -// MARK: - WPMediaAsset conformance - -extension StockPhotosMedia: WPMediaAsset { - func image(with size: CGSize, completionHandler: @escaping WPMediaImageBlock) -> WPMediaRequestID { - let thumb = thumbURL(with: size) - - DispatchQueue.global().async { - do { - let data = try Data(contentsOf: thumb) - let image = UIImage(data: data) - completionHandler(image, nil) - } catch { - completionHandler(nil, error) - } - } - - let number = Int32(id) ?? 0 - return number as WPMediaRequestID - } - - // We assume that if the size passed is .zero, we want the largest possible image - private func thumbURL(with size: CGSize) -> URL { - return size == .zero ? thumbnails.largeURL : thumbnails.postThumbnailURL - } - - func cancelImageRequest(_ requestID: WPMediaRequestID) { - // - } - - func videoAsset(completionHandler: @escaping WPMediaAssetBlock) -> WPMediaRequestID { - return 0 - } - - func assetType() -> WPMediaType { - return .image - } - - func duration() -> TimeInterval { - return 0 - } - - func baseAsset() -> Any { - return self - } - - func identifier() -> String { - return id - } - - func date() -> Date { - return Date() - } - - func pixelSize() -> CGSize { - return size - } -} - -// MARK: - ExportableAsset conformance - -extension StockPhotosMedia: ExportableAsset { - var assetMediaType: MediaType { - return .image - } -} - -// MARK: - MediaExternalAsset conformance - -extension StockPhotosMedia: MediaExternalAsset { - +extension StockPhotosMedia: ExternalMediaAsset { + var assetMediaType: MediaType { .image } + var thumbnailURL: URL { thumbnails.thumbnailURL } + var largeURL: URL { thumbnails.largeURL } } // MARK: - Decodable conformance -extension ThumbnailCollection: Decodable { +extension StockPhotosMedia.ThumbnailCollection: Decodable { enum CodingKeys: String, CodingKey { case large case medium diff --git a/WordPress/Classes/ViewRelated/Media/StockPhotos/StockPhotosMediaGroup.swift b/WordPress/Classes/ViewRelated/Media/StockPhotos/StockPhotosMediaGroup.swift deleted file mode 100644 index c24c91a11dfb..000000000000 --- a/WordPress/Classes/ViewRelated/Media/StockPhotos/StockPhotosMediaGroup.swift +++ /dev/null @@ -1,27 +0,0 @@ -import WPMediaPicker - -final class StockPhotosMediaGroup: NSObject, WPMediaGroup { - func name() -> String { - return String.freePhotosLibrary - } - - func image(with size: CGSize, completionHandler: @escaping WPMediaImageBlock) -> WPMediaRequestID { - return 0 - } - - func cancelImageRequest(_ requestID: WPMediaRequestID) { - // - } - - func baseGroup() -> Any { - return "" - } - - func identifier() -> String { - return "group id" - } - - func numberOfAssets(of mediaType: WPMediaType, completionHandler: WPMediaCountBlock? = nil) -> Int { - return 10 - } -} diff --git a/WordPress/Classes/ViewRelated/Media/StockPhotos/StockPhotosPicker.swift b/WordPress/Classes/ViewRelated/Media/StockPhotos/StockPhotosPicker.swift deleted file mode 100644 index 15bb736973b2..000000000000 --- a/WordPress/Classes/ViewRelated/Media/StockPhotos/StockPhotosPicker.swift +++ /dev/null @@ -1,156 +0,0 @@ -import WPMediaPicker - -protocol StockPhotosPickerDelegate: AnyObject { - func stockPhotosPicker(_ picker: StockPhotosPicker, didFinishPicking assets: [StockPhotosMedia]) -} - -/// Presents the Stock Photos main interface -final class StockPhotosPicker: NSObject { - var allowMultipleSelection = true { - didSet { - pickerOptions.allowMultipleSelection = allowMultipleSelection - } - } - - private lazy var dataSource: StockPhotosDataSource = { - return StockPhotosDataSource(service: stockPhotosService) - }() - - private lazy var stockPhotosService: StockPhotosService = { - guard let api = self.blog?.wordPressComRestApi() else { - //TO DO. Shall we present a user facing error (although in theory we should never reach this case if we limit Stock Photos to Jetpack blogs only) - // At this moment, what we do is return a null implementation of the StockPhotosService. The user-facing effect will be that there are no results - return NullStockPhotosService() - } - - return DefaultStockPhotosService(api: api) - }() - - weak var delegate: StockPhotosPickerDelegate? - private var blog: Blog? - private var observerToken: NSObjectProtocol? - - private let searchHint = NoResultsViewController.controller() - - private lazy var pickerOptions: WPMediaPickerOptions = { - let options = WPMediaPickerOptions() - options.showMostRecentFirst = true - options.filter = [.all] - options.allowCaptureOfMedia = false - options.showSearchBar = true - options.preferredStatusBarStyle = WPStyleGuide.preferredStatusBarStyle - options.allowMultipleSelection = allowMultipleSelection - return options - }() - - @discardableResult - func presentPicker(origin: UIViewController, blog: Blog) -> UIViewController { - NoResultsStockPhotosConfiguration.configureAsIntro(searchHint) - self.blog = blog - - let picker: WPNavigationMediaPickerViewController = { - let picker = WPNavigationMediaPickerViewController(options: pickerOptions) - picker.delegate = self - picker.startOnGroupSelector = false - picker.showGroupSelector = false - picker.dataSource = dataSource - picker.cancelButtonTitle = .closePicker - return picker - }() - - origin.present(picker, animated: true) { - picker.mediaPicker.searchBar?.becomeFirstResponder() - } - - observeDataSource() - trackAccess() - - return picker - } - - private func observeDataSource() { - observerToken = dataSource.registerChangeObserverBlock { [weak self] (_, _, _, _, assets) in - self?.updateHintView() - } - dataSource.onStartLoading = { [weak self] in - if let searchHint = self?.searchHint { - NoResultsStockPhotosConfiguration.configureAsLoading(searchHint) - } - } - dataSource.onStopLoading = { [weak self] in - self?.updateHintView() - } - } - - private func shouldShowNoResults() -> Bool { - return dataSource.searchQuery.count > 0 && dataSource.numberOfAssets() == 0 - } - - private func updateHintView() { - searchHint.removeFromView() - if shouldShowNoResults() { - NoResultsStockPhotosConfiguration.configure(searchHint) - } else { - NoResultsStockPhotosConfiguration.configureAsIntro(searchHint) - } - } - - deinit { - if let token = observerToken { - dataSource.unregisterChangeObserver(token) - } - } -} - -extension StockPhotosPicker: WPMediaPickerViewControllerDelegate { - func mediaPickerController(_ picker: WPMediaPickerViewController, didFinishPicking assets: [WPMediaAsset]) { - guard let stockPhotosMedia = assets as? [StockPhotosMedia] else { - assertionFailure("assets should be of type `[StockPhotosMedia]`") - return - } - delegate?.stockPhotosPicker(self, didFinishPicking: stockPhotosMedia) - picker.dismiss(animated: true) - dataSource.clearSearch(notifyObservers: false) - hideKeyboard(from: picker.searchBar) - } - - func emptyViewController(forMediaPickerController picker: WPMediaPickerViewController) -> UIViewController? { - return searchHint - } - - func mediaPickerControllerDidEndLoadingData(_ picker: WPMediaPickerViewController) { - if let searchBar = picker.searchBar { - WPStyleGuide.configureSearchBar(searchBar) - } - } - - func mediaPickerControllerDidCancel(_ picker: WPMediaPickerViewController) { - picker.dismiss(animated: true) - dataSource.clearSearch(notifyObservers: false) - hideKeyboard(from: picker.searchBar) - } - - func mediaPickerController(_ picker: WPMediaPickerViewController, didSelect asset: WPMediaAsset) { - hideKeyboard(from: picker.searchBar) - } - - func mediaPickerController(_ picker: WPMediaPickerViewController, didDeselect asset: WPMediaAsset) { - hideKeyboard(from: picker.searchBar) - } - - private func hideKeyboard(from view: UIView?) { - if let view = view, view.isFirstResponder { - //Fix animation conflict between dismissing the keyboard and showing the accessory input view - DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { - view.resignFirstResponder() - } - } - } -} - -// MARK: - Tracks -extension StockPhotosPicker { - fileprivate func trackAccess() { - WPAnalytics.track(.stockMediaAccessed) - } -} diff --git a/WordPress/Classes/ViewRelated/Media/StockPhotos/StockPhotosStrings.swift b/WordPress/Classes/ViewRelated/Media/StockPhotos/StockPhotosStrings.swift index 9ccb0d1a9aca..d159acef3da8 100644 --- a/WordPress/Classes/ViewRelated/Media/StockPhotos/StockPhotosStrings.swift +++ b/WordPress/Classes/ViewRelated/Media/StockPhotos/StockPhotosStrings.swift @@ -9,10 +9,6 @@ extension String { return NSLocalizedString("Other Apps", comment: "Menu option used for adding media from other applications.") } - static var closePicker: String { - return NSLocalizedString("Close", comment: "Dismiss the media picker for Stock Photos") - } - static var cancelMoreOptions: String { return NSLocalizedString( "stockPhotos.strings.dismiss", @@ -20,21 +16,4 @@ extension String { comment: "Dismiss the AlertView" ) } - - // MARK: - Placeholder - static var freePhotosPlaceholderTitle: String { - return NSLocalizedString("Search to find free photos to add to your Media Library!", comment: "Title for placeholder in Free Photos") - } - - static var freePhotosPlaceholderSubtitle: String { - return NSLocalizedString("Photos provided by Pexels", comment: "Subtitle for placeholder in Free Photos. The company name 'Pexels' should always be written as it is.") - } - - static var freePhotosSearchNoResult: String { - return NSLocalizedString("No media matching your search", comment: "Phrase to show when the user search for images but there are no result to show.") - } - - static var freePhotosSearchLoading: String { - return NSLocalizedString("Loading Photos...", comment: "Phrase to show when the user has searched for images and they are being loaded.") - } } diff --git a/WordPress/Classes/ViewRelated/Media/StockPhotos/StockPhotosWelcomeView.swift b/WordPress/Classes/ViewRelated/Media/StockPhotos/StockPhotosWelcomeView.swift new file mode 100644 index 000000000000..e400455f3742 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Media/StockPhotos/StockPhotosWelcomeView.swift @@ -0,0 +1,62 @@ +import UIKit + +final class StockPhotosWelcomeView: UIView { + override init(frame: CGRect) { + super.init(frame: frame) + + let textLabel = UILabel() + textLabel.text = Strings.title + textLabel.font = WPStyleGuide.fontForTextStyle(.title2) + textLabel.adjustsFontForContentSizeCategory = true + textLabel.textAlignment = .center + textLabel.numberOfLines = 0 + + let subtitleLabel = UILabel() + subtitleLabel.attributedText = makeAttributedSubtitle() + subtitleLabel.font = WPStyleGuide.fontForTextStyle(.body) + subtitleLabel.textColor = .secondaryLabel + subtitleLabel.adjustsFontForContentSizeCategory = true + subtitleLabel.textAlignment = .center + subtitleLabel.numberOfLines = 0 + + let stack = UIStackView(arrangedSubviews: [ + UIImageView(image: UIImage(named: "media-no-results")), + textLabel, + subtitleLabel + ]) + stack.axis = .vertical + stack.alignment = .center + stack.spacing = 24 + stack.isLayoutMarginsRelativeArrangement = true + stack.layoutMargins = UIEdgeInsets(top: 16, left: 32, bottom: 16, right: 32) + + let wrapper = UIStackView(arrangedSubviews: [stack]) + wrapper.alignment = .center + addSubview(wrapper) + wrapper.translatesAutoresizingMaskIntoConstraints = false + pinSubviewToAllEdges(wrapper) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +private enum Constants { + static let companyUrl = "https://www.pexels.com" + static let companyName = "Pexels" +} + +private func makeAttributedSubtitle() -> NSAttributedString { + let subtitle: String = Strings.freePhotosPlaceholderSubtitle + let htmlTaggedLink = "\(Constants.companyName)" + let htmlTaggedText = subtitle.replacingOccurrences(of: Constants.companyName, with: htmlTaggedLink) + return NSAttributedString.attributedStringWithHTML(htmlTaggedText, attributes: nil) +} + +private enum Strings { + static let title = NSLocalizedString("stockPhotos.title", value: "Search to find free photos to add to your Media Library!", comment: "Title for placeholder in Free Photos") + static var freePhotosPlaceholderSubtitle: String { + return NSLocalizedString("stockPhotos.subtitle", value: "Photos provided by Pexels", comment: "Subtitle for placeholder in Free Photos. The company name 'Pexels' should always be written as it is.") + } +} diff --git a/WordPress/Classes/ViewRelated/Media/Tenor/NoResultsTenorConfiguration.swift b/WordPress/Classes/ViewRelated/Media/Tenor/NoResultsTenorConfiguration.swift deleted file mode 100644 index f0f775c0df49..000000000000 --- a/WordPress/Classes/ViewRelated/Media/Tenor/NoResultsTenorConfiguration.swift +++ /dev/null @@ -1,28 +0,0 @@ -// Empty state for Tenor - -struct NoResultsTenorConfiguration { - static func configureAsIntro(_ viewController: NoResultsViewController) { - viewController.configure(title: .tenorPlaceholderTitle, - image: Constants.imageName, - subtitleImage: Constants.subtitleImageName) - - viewController.view.layoutIfNeeded() - } - - static func configureAsLoading(_ viewController: NoResultsViewController) { - viewController.configure(title: .tenorSearchLoading, - image: Constants.imageName) - - viewController.view.layoutIfNeeded() - } - - static func configure(_ viewController: NoResultsViewController) { - viewController.configureForNoSearchResults(title: .tenorSearchNoResult) - viewController.view.layoutIfNeeded() - } - - private enum Constants { - static let imageName = "media-no-results" - static let subtitleImageName = "tenor-attribution" - } -} diff --git a/WordPress/Classes/ViewRelated/Media/Tenor/TenorDataSource.swift b/WordPress/Classes/ViewRelated/Media/Tenor/TenorDataSource.swift index 35861882e252..e43a0812cff2 100644 --- a/WordPress/Classes/ViewRelated/Media/Tenor/TenorDataSource.swift +++ b/WordPress/Classes/ViewRelated/Media/Tenor/TenorDataSource.swift @@ -1,13 +1,12 @@ -import WPMediaPicker +import Foundation /// Data Source for Tenor -final class TenorDataSource: NSObject, WPMediaCollectionDataSource { - fileprivate static let paginationThreshold = 10 +final class TenorDataSource: ExternalMediaDataSource { + private(set) var assets: [ExternalMediaAsset] = [] - fileprivate var tenorMedia = [TenorMedia]() - var observers = [String: WPMediaChangesBlock]() private var dataLoader: TenorDataLoader? + var onUpdatedAssets: (() -> Void)? var onStartLoading: (() -> Void)? var onStopLoading: (() -> Void)? @@ -16,21 +15,13 @@ final class TenorDataSource: NSObject, WPMediaCollectionDataSource { private(set) var searchQuery: String = "" init(service: TenorService) { - super.init() self.dataLoader = TenorDataLoader(service: service, delegate: self) } - func clearSearch(notifyObservers shouldNotify: Bool) { - tenorMedia.removeAll() - if shouldNotify { - notifyObservers() - } - } - - func search(for searchText: String?) { - searchQuery = searchText ?? "" + func search(for searchText: String) { + searchQuery = searchText - guard searchText?.isEmpty == false else { + guard searchQuery.count > 1 else { clearSearch(notifyObservers: true) scheduler.cancel() return @@ -47,120 +38,20 @@ final class TenorDataSource: NSObject, WPMediaCollectionDataSource { dataLoader?.search(params) } - func numberOfGroups() -> Int { - return 1 - } - - func group(at index: Int) -> WPMediaGroup { - return TenorMediaGroup() - } - - func selectedGroup() -> WPMediaGroup? { - return TenorMediaGroup() - } - - func numberOfAssets() -> Int { - return tenorMedia.count - } - - func media(at index: Int) -> WPMediaAsset { - fetchMoreContentIfNecessary(index) - return tenorMedia[index] - } - - func media(withIdentifier identifier: String) -> WPMediaAsset? { - return tenorMedia.filter { $0.identifier() == identifier }.first - } - - func registerChangeObserverBlock(_ callback: @escaping WPMediaChangesBlock) -> NSObjectProtocol { - let blockKey = UUID().uuidString - observers[blockKey] = callback - return blockKey as NSString - } - - func unregisterChangeObserver(_ blockKey: NSObjectProtocol) { - guard let key = blockKey as? String else { - assertionFailure("blockKey must be of type String") - return + private func clearSearch(notifyObservers shouldNotify: Bool) { + assets.removeAll() + if shouldNotify { + onUpdatedAssets?() } - observers.removeValue(forKey: key) - } - - func registerGroupChangeObserverBlock(_ callback: @escaping WPMediaGroupChangesBlock) -> NSObjectProtocol { - // The group never changes - return NSNull() - } - - func unregisterGroupChangeObserver(_ blockKey: NSObjectProtocol) { - // The group never changes - } - - func loadData(with options: WPMediaLoadOptions, success successBlock: WPMediaSuccessBlock?, failure failureBlock: WPMediaFailureBlock? = nil) { - successBlock?() - } - - func mediaTypeFilter() -> WPMediaType { - return .image - } - - func ascendingOrdering() -> Bool { - return true - } - - func searchCancelled() { - searchQuery = "" - clearSearch(notifyObservers: true) - } - - // MARK: Unused protocol methods - - func setSelectedGroup(_ group: WPMediaGroup) { - // - } - - func add(_ image: UIImage, metadata: [AnyHashable: Any]?, completionBlock: WPMediaAddedBlock? = nil) { - // } - func addVideo(from url: URL, completionBlock: WPMediaAddedBlock? = nil) { - // - } - - func setMediaTypeFilter(_ filter: WPMediaType) { - // - } - - func setAscendingOrdering(_ ascending: Bool) { - // - } -} - -// MARK: - Helpers - -extension TenorDataSource { - private func notifyObservers(incremental: Bool = false, inserted: IndexSet = IndexSet()) { - DispatchQueue.main.async { - self.observers.forEach { - $0.value(incremental, IndexSet(), inserted, IndexSet(), []) - } - } + func loadMore() { + dataLoader?.loadNextPage() } } // MARK: - Pagination -extension TenorDataSource { - fileprivate func fetchMoreContentIfNecessary(_ index: Int) { - if shouldLoadMore(index) { - dataLoader?.loadNextPage() - } - } - - private func shouldLoadMore(_ index: Int) -> Bool { - return index + type(of: self).paginationThreshold >= numberOfAssets() - } -} - extension TenorDataSource: TenorDataLoaderDelegate { func didLoad(media: [TenorMedia], reset: Bool) { defer { @@ -172,26 +63,12 @@ extension TenorDataSource: TenorDataLoaderDelegate { return } + assert(Thread.isMainThread) if reset { - overwriteMedia(with: media) + self.assets = media } else { - appendMedia(with: media) + self.assets += media } - } - - private func overwriteMedia(with media: [TenorMedia]) { - tenorMedia = media - notifyObservers(incremental: false) - } - - private func appendMedia(with media: [TenorMedia]) { - let currentMaxIndex = tenorMedia.count - let newMaxIndex = currentMaxIndex + media.count - 1 - - let isIncremental = currentMaxIndex != 0 - let insertedIndexes = IndexSet(integersIn: currentMaxIndex...newMaxIndex) - - tenorMedia.append(contentsOf: media) - notifyObservers(incremental: isIncremental, inserted: insertedIndexes) + onUpdatedAssets?() } } diff --git a/WordPress/Classes/ViewRelated/Media/Tenor/TenorMedia.swift b/WordPress/Classes/ViewRelated/Media/Tenor/TenorMedia.swift index 5553e0c2e850..95552d272e43 100644 --- a/WordPress/Classes/ViewRelated/Media/Tenor/TenorMedia.swift +++ b/WordPress/Classes/ViewRelated/Media/Tenor/TenorMedia.swift @@ -1,5 +1,4 @@ import MobileCoreServices -import WPMediaPicker import UniformTypeIdentifiers struct TenorImageCollection { @@ -25,6 +24,13 @@ final class TenorMedia: NSObject { } } +extension TenorMedia: ExternalMediaAsset { + var thumbnailURL: URL { images.staticThumbnailURL } + var largeURL: URL { images.largeURL } + var caption: String { "" } + var assetMediaType: MediaType { .image } +} + // MARK: - Create Tenor media from API GIF Entity extension TenorMedia { @@ -48,77 +54,3 @@ extension TenorMedia { self.init(id: gif.id, name: gif.title ?? "", images: images, date: gif.created) } } - -// MARK: - WPMediaAsset - -extension TenorMedia: WPMediaAsset { - func image(with size: CGSize, completionHandler: @escaping WPMediaImageBlock) -> WPMediaRequestID { - // We don't need to download any image here, leave it for the overlay to handle - return 0 - } - - func cancelImageRequest(_ requestID: WPMediaRequestID) { - // Nothing to do - } - - func videoAsset(completionHandler: @escaping WPMediaAssetBlock) -> WPMediaRequestID { - return 0 - } - - func assetType() -> WPMediaType { - return .image - } - - func duration() -> TimeInterval { - return 0 - } - - func baseAsset() -> Any { - return self - } - - func identifier() -> String { - return id - } - - func date() -> Date { - return updatedDate - } - - func pixelSize() -> CGSize { - return images.largeSize - } - - func utTypeIdentifier() -> String? { - return UTType.gif.identifier - } -} - -// MARK: - ExportableAsset conformance - -extension TenorMedia: ExportableAsset { - var assetMediaType: MediaType { - return .image - } -} - -// MARK: - MediaExternalAsset conformance - -extension TenorMedia: MediaExternalAsset { - // The URL source for saving into user's media library as well as GIF preview - var URL: URL { - return images.largeURL - } - - var caption: String { - return "" - } -} - -// Overlay -extension TenorMedia { - // Return the smallest GIF size for previewing - var previewURL: URL { - return images.staticThumbnailURL - } -} diff --git a/WordPress/Classes/ViewRelated/Media/Tenor/TenorMediaGroup.swift b/WordPress/Classes/ViewRelated/Media/Tenor/TenorMediaGroup.swift deleted file mode 100644 index 8257a220078b..000000000000 --- a/WordPress/Classes/ViewRelated/Media/Tenor/TenorMediaGroup.swift +++ /dev/null @@ -1,27 +0,0 @@ -import WPMediaPicker - -final class TenorMediaGroup: NSObject, WPMediaGroup { - func name() -> String { - return String.tenor - } - - func image(with size: CGSize, completionHandler: @escaping WPMediaImageBlock) -> WPMediaRequestID { - return 0 - } - - func cancelImageRequest(_ requestID: WPMediaRequestID) { - // - } - - func baseGroup() -> Any { - return "" - } - - func identifier() -> String { - return "group id" - } - - func numberOfAssets(of mediaType: WPMediaType, completionHandler: WPMediaCountBlock? = nil) -> Int { - return 10 - } -} diff --git a/WordPress/Classes/ViewRelated/Media/Tenor/TenorPicker.swift b/WordPress/Classes/ViewRelated/Media/Tenor/TenorPicker.swift deleted file mode 100644 index 3706ff1e41f0..000000000000 --- a/WordPress/Classes/ViewRelated/Media/Tenor/TenorPicker.swift +++ /dev/null @@ -1,187 +0,0 @@ -import MobileCoreServices -import WPMediaPicker -import UniformTypeIdentifiers - -protocol TenorPickerDelegate: AnyObject { - func tenorPicker(_ picker: TenorPicker, didFinishPicking assets: [TenorMedia]) -} - -/// Presents the Tenor main interface -final class TenorPicker: NSObject { - // MARK: - Public properties - - var allowMultipleSelection = true { - didSet { - pickerOptions.allowMultipleSelection = allowMultipleSelection - } - } - - // MARK: - Private properties - - private lazy var dataSource: TenorDataSource = { - TenorDataSource(service: tenorService) - }() - - private lazy var tenorService: TenorService = { - TenorService() - }() - - /// Helps choosing the correct view controller for previewing a media asset - /// - private var mediaPreviewHelper: MediaPreviewHelper! - - weak var delegate: TenorPickerDelegate? - private var blog: Blog? - private var observerToken: NSObjectProtocol? - - private let searchHint = NoResultsViewController.controller() - - private lazy var pickerOptions: WPMediaPickerOptions = { - let options = WPMediaPickerOptions() - options.showMostRecentFirst = true - options.filter = [.all] - options.allowCaptureOfMedia = false - options.showSearchBar = true - options.badgedUTTypes = [UTType.gif.identifier] - options.preferredStatusBarStyle = WPStyleGuide.preferredStatusBarStyle - options.allowMultipleSelection = allowMultipleSelection - return options - }() - - @discardableResult - func presentPicker(origin: UIViewController, blog: Blog) -> UIViewController { - NoResultsTenorConfiguration.configureAsIntro(searchHint) - self.blog = blog - - let picker: WPNavigationMediaPickerViewController = { - let picker = WPNavigationMediaPickerViewController(options: pickerOptions) - picker.delegate = self - picker.startOnGroupSelector = false - picker.showGroupSelector = false - picker.dataSource = dataSource - picker.cancelButtonTitle = .closePicker - picker.mediaPicker.registerClass(forReusableCellOverlayViews: CachedAnimatedImageView.self) - return picker - }() - - origin.present(picker, animated: true) { - picker.mediaPicker.searchBar?.becomeFirstResponder() - } - - observeDataSource() - WPAnalytics.track(.tenorAccessed) - - return picker - } - - private func observeDataSource() { - observerToken = dataSource.registerChangeObserverBlock { [weak self] _, _, _, _, _ in - self?.updateHintView() - } - dataSource.onStartLoading = { [weak self] in - guard let strongSelf = self else { - return - } - NoResultsTenorConfiguration.configureAsLoading(strongSelf.searchHint) - } - dataSource.onStopLoading = { [weak self] in - self?.updateHintView() - } - } - - private func shouldShowNoResults() -> Bool { - return dataSource.searchQuery.count > 0 && dataSource.numberOfAssets() == 0 - } - - private func updateHintView() { - searchHint.removeFromView() - if shouldShowNoResults() { - NoResultsTenorConfiguration.configure(searchHint) - } else { - NoResultsTenorConfiguration.configureAsIntro(searchHint) - } - } - - deinit { - if let token = observerToken { - dataSource.unregisterChangeObserver(token) - } - } -} - -extension TenorPicker: WPMediaPickerViewControllerDelegate { - func mediaPickerController(_ picker: WPMediaPickerViewController, didFinishPicking assets: [WPMediaAsset]) { - guard let assets = assets as? [TenorMedia] else { - assertionFailure("assets should be of type `[TenorMedia]`") - return - } - delegate?.tenorPicker(self, didFinishPicking: assets) - picker.dismiss(animated: true) - dataSource.clearSearch(notifyObservers: false) - hideKeyboard(from: picker.searchBar) - } - - func emptyViewController(forMediaPickerController picker: WPMediaPickerViewController) -> UIViewController? { - return searchHint - } - - func mediaPickerControllerDidEndLoadingData(_ picker: WPMediaPickerViewController) { - if let searchBar = picker.searchBar { - WPStyleGuide.configureSearchBar(searchBar) - } - } - - func mediaPickerControllerDidCancel(_ picker: WPMediaPickerViewController) { - picker.dismiss(animated: true) - dataSource.clearSearch(notifyObservers: false) - hideKeyboard(from: picker.searchBar) - } - - func mediaPickerController(_ picker: WPMediaPickerViewController, didSelect asset: WPMediaAsset) { - hideKeyboard(from: picker.searchBar) - } - - func mediaPickerController(_ picker: WPMediaPickerViewController, didDeselect asset: WPMediaAsset) { - hideKeyboard(from: picker.searchBar) - } - - func mediaPickerController(_ picker: WPMediaPickerViewController, shouldShowOverlayViewForCellFor asset: WPMediaAsset) -> Bool { - return true - } - - func mediaPickerController(_ picker: WPMediaPickerViewController, - willShowOverlayView overlayView: UIView, - forCellFor asset: WPMediaAsset) { - guard let animatedImageView = overlayView as? CachedAnimatedImageView else { - return - } - - guard let tenorMedia = asset as? TenorMedia else { - assertionFailure("asset should be of type `TenorMedia`") - return - } - - animatedImageView.prepForReuse() - animatedImageView.gifStrategy = .tinyGIFs - animatedImageView.contentMode = .scaleAspectFill - animatedImageView.clipsToBounds = true - animatedImageView.setAnimatedImage(URLRequest(url: tenorMedia.previewURL), - placeholderImage: nil, - success: nil, - failure: nil) - } - - func mediaPickerController(_ picker: WPMediaPickerViewController, previewViewControllerFor assets: [WPMediaAsset], selectedIndex selected: Int) -> UIViewController? { - mediaPreviewHelper = MediaPreviewHelper(assets: assets) - return mediaPreviewHelper.previewViewController(selectedIndex: selected) - } - - private func hideKeyboard(from view: UIView?) { - if let view = view, view.isFirstResponder { - // Fix animation conflict between dismissing the keyboard and showing the accessory input view - DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { - view.resignFirstResponder() - } - } - } -} diff --git a/WordPress/Classes/ViewRelated/Media/Tenor/TenorStrings.swift b/WordPress/Classes/ViewRelated/Media/Tenor/TenorStrings.swift index 4f82400180fd..b756ce113de7 100644 --- a/WordPress/Classes/ViewRelated/Media/Tenor/TenorStrings.swift +++ b/WordPress/Classes/ViewRelated/Media/Tenor/TenorStrings.swift @@ -1,29 +1,12 @@ -/// Extension on String containing the literals for the Tenor feature +import Foundation + +// Extension on String containing the literals for the Tenor feature extension String { // MARK: - Entry point: alert controller static var tenor: String { return NSLocalizedString("Free GIF Library", comment: "One of the options when selecting More in the Post Editor's format bar") } - - // MARK: - Placeholder - - static var tenorPlaceholderTitle: String { - return NSLocalizedString("Search to find GIFs to add to your Media Library!", comment: "Title for placeholder in Tenor picker") - } - - static var tenorPlaceholderSubtitle: String { - return NSLocalizedString("Powered by Tenor", comment: "Subtitle for placeholder in Tenor picker. `The company name 'Tenor' should always be written as it is.") - } - - static var tenorSearchNoResult: String { - return NSLocalizedString("No media matching your search", comment: "Phrase to show when the user searches for GIFs but there are no result to show.") - } - - static var tenorSearchLoading: String { - return NSLocalizedString("Loading GIFs...", comment: "Phrase to show when the user has searched for GIFs and they are being loaded.") - } - } enum GIFAlertStrings { diff --git a/WordPress/Classes/ViewRelated/Media/Tenor/TenorWelcomeView.swift b/WordPress/Classes/ViewRelated/Media/Tenor/TenorWelcomeView.swift new file mode 100644 index 000000000000..410522254cd6 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Media/Tenor/TenorWelcomeView.swift @@ -0,0 +1,39 @@ +import UIKit + +final class TenorWelcomeView: UIView { + override init(frame: CGRect) { + super.init(frame: frame) + + let textLabel = UILabel() + textLabel.font = WPStyleGuide.fontForTextStyle(.title2) + textLabel.adjustsFontForContentSizeCategory = true + textLabel.text = Strings.title + textLabel.textAlignment = .center + textLabel.numberOfLines = 0 + + let stack = UIStackView(arrangedSubviews: [ + UIImageView(image: UIImage(named: "media-no-results")), + textLabel, + UIImageView(image: UIImage(named: "tenor-attribution")) + ]) + stack.axis = .vertical + stack.alignment = .center + stack.spacing = 24 + stack.isLayoutMarginsRelativeArrangement = true + stack.layoutMargins = UIEdgeInsets(top: 16, left: 32, bottom: 16, right: 32) + + let wrapper = UIStackView(arrangedSubviews: [stack]) + wrapper.alignment = .center + addSubview(wrapper) + wrapper.translatesAutoresizingMaskIntoConstraints = false + pinSubviewToAllEdges(wrapper) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +private enum Strings { + static let title = NSLocalizedString("tenor.welcomeMessage", value: "Search to find GIFs to add to your Media Library!", comment: "Title for placeholder in Tenor picker") +} diff --git a/WordPress/Classes/ViewRelated/Media/WPStyleGuide+Loader.swift b/WordPress/Classes/ViewRelated/Media/WPStyleGuide+Loader.swift deleted file mode 100644 index 207860c2d021..000000000000 --- a/WordPress/Classes/ViewRelated/Media/WPStyleGuide+Loader.swift +++ /dev/null @@ -1,12 +0,0 @@ - -import UIKit - -extension WPStyleGuide { - static func styleProgressViewWhite(_ progressView: CircularProgressView) { - progressView.backgroundColor = .clear - } - - static func styleProgressViewForMediaCell(_ progressView: CircularProgressView) { - progressView.backgroundColor = .neutral(.shade70) - } -} diff --git a/WordPress/Classes/ViewRelated/Menus/MenusViewController.h b/WordPress/Classes/ViewRelated/Menus/MenusViewController.h index 37f2e35f93f8..c2778e3ad0b9 100644 --- a/WordPress/Classes/ViewRelated/Menus/MenusViewController.h +++ b/WordPress/Classes/ViewRelated/Menus/MenusViewController.h @@ -2,12 +2,15 @@ NS_ASSUME_NONNULL_BEGIN -@class Blog; +@class Blog, MenusService; @interface MenusViewController : UIViewController + (MenusViewController *)controllerWithBlog:(Blog *)blog; +@property (nonatomic, strong, readonly) Blog *blog; +@property (nonatomic, strong, readonly) MenusService *menusService; + @end NS_ASSUME_NONNULL_END diff --git a/WordPress/Classes/ViewRelated/Menus/MenusViewController.m b/WordPress/Classes/ViewRelated/Menus/MenusViewController.m index d90a3c7ad4d3..d0ea15f24916 100644 --- a/WordPress/Classes/ViewRelated/Menus/MenusViewController.m +++ b/WordPress/Classes/ViewRelated/Menus/MenusViewController.m @@ -15,8 +15,6 @@ #import #import - - static CGFloat const ScrollViewOffsetAdjustmentPadding = 10.0; @interface MenusViewController () @@ -32,9 +30,6 @@ @interface MenusViewController () Void, failure: @escaping (Error) -> Void) -> () -> Void { + let coreDataStack = ContextManager.shared + let repository = PostRepository(coreDataStack: coreDataStack) + let fetchAllPagesTask = repository.fetchAllPages(statuses: [.publish], in: TaggedManagedObjectID(blog)) + + // Wait for the fetching all pages task to complete and pass its result to the success or failure block. + Task { [weak self] in + let allPages: [TaggedManagedObjectID] + do { + allPages = try await fetchAllPagesTask.value + } catch { + failure(error) + return + } + + guard let menusService = self?.menusService else { return } + + await menusService.managedObjectContext.perform { + let items = allPages.compactMap { + menusService.createItem(withPageID: $0.objectID, in: menusService.managedObjectContext) + } + success(items) + } + } + + return fetchAllPagesTask.cancel + } + +} diff --git a/WordPress/Classes/ViewRelated/NUX/LoginEpilogueTableViewController.swift b/WordPress/Classes/ViewRelated/NUX/LoginEpilogueTableViewController.swift index fc84ba86ea00..0106d99946e4 100644 --- a/WordPress/Classes/ViewRelated/NUX/LoginEpilogueTableViewController.swift +++ b/WordPress/Classes/ViewRelated/NUX/LoginEpilogueTableViewController.swift @@ -233,13 +233,11 @@ private extension LoginEpilogueTableViewController { } enum Settings { - static let headerReuseIdentifier = "SectionHeader" static let userCellReuseIdentifier = "userInfo" static let chooseSiteReuseIdentifier = "chooseSite" static let createNewSiteReuseIdentifier = "createNewSite" static let profileRowHeight = CGFloat(180) static let blogRowHeight = CGFloat(60) - static let headerHeight = CGFloat(50) } } diff --git a/WordPress/Classes/ViewRelated/NUX/SignupEpilogueTableViewController.swift b/WordPress/Classes/ViewRelated/NUX/SignupEpilogueTableViewController.swift index 871c2bdb12ca..1b5b0e9cf117 100644 --- a/WordPress/Classes/ViewRelated/NUX/SignupEpilogueTableViewController.swift +++ b/WordPress/Classes/ViewRelated/NUX/SignupEpilogueTableViewController.swift @@ -220,7 +220,6 @@ private extension SignupEpilogueTableViewController { static let noPasswordRows = 2 static let allAccountRows = 3 static let headerFooterHeight: CGFloat = 50 - static let footerTrailingMargin: CGFloat = 16 static let footerTopMargin: CGFloat = 8 } diff --git a/WordPress/Classes/ViewRelated/NUX/SignupEpilogueViewController.swift b/WordPress/Classes/ViewRelated/NUX/SignupEpilogueViewController.swift index 47e597637d1c..f7db1515d541 100644 --- a/WordPress/Classes/ViewRelated/NUX/SignupEpilogueViewController.swift +++ b/WordPress/Classes/ViewRelated/NUX/SignupEpilogueViewController.swift @@ -340,16 +340,3 @@ extension SignupEpilogueViewController: SignupUsernameViewControllerDelegate { } } } - -// MARK: - User Defaults - -extension UserDefaults { - var quickStartWasDismissedPermanently: Bool { - get { - return bool(forKey: #function) - } - set { - set(newValue, forKey: #function) - } - } -} diff --git a/WordPress/Classes/ViewRelated/NUX/UnifiedProloguePages.swift b/WordPress/Classes/ViewRelated/NUX/UnifiedProloguePages.swift index 0cdf6dc0a11c..40fff9d454e0 100644 --- a/WordPress/Classes/ViewRelated/NUX/UnifiedProloguePages.swift +++ b/WordPress/Classes/ViewRelated/NUX/UnifiedProloguePages.swift @@ -239,11 +239,4 @@ class UnifiedProloguePageViewController: UIViewController { return UIView() } } - - enum Metrics { - static let topInset: CGFloat = 96.0 - static let horizontalInset: CGFloat = 24.0 - static let titleToContentSpacing: CGFloat = 48.0 - static let heightRatio: CGFloat = WPDeviceIdentification.isiPad() ? 0.5 : 0.4 - } } diff --git a/WordPress/Classes/ViewRelated/NUX/WordPressAuthenticationManager.swift b/WordPress/Classes/ViewRelated/NUX/WordPressAuthenticationManager.swift index 9a0c3a11f091..cd10aafcc1e5 100644 --- a/WordPress/Classes/ViewRelated/NUX/WordPressAuthenticationManager.swift +++ b/WordPress/Classes/ViewRelated/NUX/WordPressAuthenticationManager.swift @@ -79,6 +79,7 @@ extension WordPressAuthenticationManager { enableSignupWithGoogle: AppConfiguration.allowSignUp, enableUnifiedAuth: true, enableUnifiedCarousel: true, + enablePasskeys: true, enableSocialLogin: true) } @@ -175,7 +176,7 @@ extension WordPressAuthenticationManager { prologueButtonsBackgroundColor: prologueButtonsBackgroundColor, prologueViewBackgroundColor: prologueViewBackgroundColor, prologueBackgroundImage: authenticationHandler?.prologueBackgroundImage, - prologueButtonsBlurEffect: authenticationHandler?.prologueButtonsBlurEffect, + prologueButtonsBlurEffect: nil, navBarBackgroundColor: .appBarBackground, navButtonTextColor: .appBarTint, navTitleTextColor: .appBarText) @@ -584,26 +585,6 @@ private extension WordPressAuthenticationManager { typealias QuickStartOnDismissHandler = (Blog, Bool) -> Void - func presentQuickStartPrompt(for blog: Blog, in navigationController: UINavigationController, onDismiss: QuickStartOnDismissHandler?) { - // If the quick start prompt has already been dismissed, - // then show the My Site screen for the specified blog - guard !quickStartSettings.promptWasDismissed(for: blog) else { - - if self.windowManager.isShowingFullscreenSignIn { - self.windowManager.dismissFullscreenSignIn(blogToShow: blog) - } else { - navigationController.dismiss(animated: true) - } - - return - } - - // Otherwise, show the Quick Start prompt - let quickstartPrompt = QuickStartPromptViewController(blog: blog) - quickstartPrompt.onDismiss = onDismiss - navigationController.pushViewController(quickstartPrompt, animated: true) - } - func onDismissQuickStartPromptHandler(type: QuickStartType, onDismiss: @escaping () -> Void) -> QuickStartOnDismissHandler { return { [weak self] blog, _ in guard let self = self else { diff --git a/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationDetailsViewController.swift b/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationDetailsViewController.swift index 23b52cf39207..556e3a64ea23 100644 --- a/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationDetailsViewController.swift +++ b/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationDetailsViewController.swift @@ -69,10 +69,6 @@ class NotificationDetailsViewController: UIViewController, NoResultsViewHost { /// fileprivate let estimatedRowHeightsCache = NSCache() - /// A Reader Detail VC to display post content if needed - /// - private var readerDetailViewController: ReaderDetailViewController? - /// Previous NavBar Navigation Button /// var previousNavigationButton: UIButton! @@ -549,33 +545,6 @@ extension NotificationDetailsViewController { } } - - -// MARK: - Reader Helpers -// -private extension NotificationDetailsViewController { - func attachReaderViewIfNeeded() { - guard shouldAttachReaderView, - let postID = note.metaPostID, - let siteID = note.metaSiteID else { - readerDetailViewController?.remove() - return - } - - readerDetailViewController?.remove() - let readerDetailViewController = ReaderDetailViewController.controllerWithPostID(postID, siteID: siteID) - add(readerDetailViewController) - readerDetailViewController.view.translatesAutoresizingMaskIntoConstraints = false - view.pinSubviewToSafeArea(readerDetailViewController.view) - self.readerDetailViewController = readerDetailViewController - } - - var shouldAttachReaderView: Bool { - return note.kind == .newPost - } -} - - // MARK: - Suggestions View Helpers // private extension NotificationDetailsViewController { @@ -1456,12 +1425,6 @@ private extension NotificationDetailsViewController { return NotificationActionsService(coreDataStack: ContextManager.shared) } - enum DisplayError: Error { - case missingParameter - case unsupportedFeature - case unsupportedType - } - enum ContentMedia { static let richBlockTypes = Set(arrayLiteral: FormattableContentKind.text, FormattableContentKind.comment) static let duration = TimeInterval(0.25) @@ -1477,7 +1440,6 @@ private extension NotificationDetailsViewController { enum Settings { static let numberOfSections = 1 static let estimatedRowHeight = CGFloat(44) - static let expirationFiveMinutes = TimeInterval(60 * 5) } enum Assets { diff --git a/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationSettingsViewController.swift b/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationSettingsViewController.swift index bd6eb02b15d8..c333360da349 100644 --- a/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationSettingsViewController.swift +++ b/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationSettingsViewController.swift @@ -35,7 +35,6 @@ class NotificationSettingsViewController: UIViewController { // MARK: - Private Constants fileprivate let blogReuseIdentifier = WPBlogTableViewCell.classNameWithoutNamespaces() - fileprivate let blogRowHeight = CGFloat(54.0) fileprivate let defaultReuseIdentifier = WPTableViewCell.classNameWithoutNamespaces() fileprivate let switchReuseIdentifier = SwitchTableViewCell.classNameWithoutNamespaces() diff --git a/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationsViewController.swift b/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationsViewController.swift index 7ae11be9de35..28e03dffad54 100644 --- a/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationsViewController.swift +++ b/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationsViewController.swift @@ -439,7 +439,7 @@ class NotificationsViewController: UIViewController, UIViewControllerRestoration func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { // skip when the notification is marked for deletion. - guard let note = tableViewHandler.resultsController?.object(at: indexPath) as? Notification, + guard let note = tableViewHandler.resultsController?.managedObject(atUnsafe: indexPath) as? Notification, deletionRequestForNoteWithID(note.objectID) == nil else { return nil } @@ -466,7 +466,7 @@ class NotificationsViewController: UIViewController, UIViewControllerRestoration func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { // skip when the notification is marked for deletion. - guard let note = tableViewHandler.resultsController?.object(at: indexPath) as? Notification, + guard let note = tableViewHandler.resultsController?.managedObject(atUnsafe: indexPath) as? Notification, let block: FormattableCommentContent = note.contentGroup(ofKind: .comment)?.blockOfKind(.comment), deletionRequestForNoteWithID(note.objectID) == nil else { return nil @@ -890,7 +890,7 @@ extension NotificationsViewController { } let noteIndexPath = tableView.indexPathsForVisibleRows?.first { indexPath in - return note == tableViewHandler.resultsController?.object(at: indexPath) as? Notification + return note == tableViewHandler.resultsController?.managedObject(atUnsafe: indexPath) as? Notification } guard noteIndexPath == nil else { @@ -1408,7 +1408,7 @@ extension NotificationsViewController: WPTableViewHandlerDelegate { } func configureCell(_ cell: UITableViewCell, at indexPath: IndexPath) { - guard let note = tableViewHandler.resultsController?.object(at: indexPath) as? Notification, + guard let note = tableViewHandler.resultsController?.managedObject(atUnsafe: indexPath) as? Notification, let cell = cell as? ListTableViewCell else { return } @@ -1821,20 +1821,6 @@ extension NotificationsViewController: WPSplitViewControllerDetailProvider { controller.view.backgroundColor = .basicBackground return controller } - - private func fetchFirstNotification() -> Notification? { - let context = managedObjectContext() - guard let fetchRequest = self.fetchRequest() else { - return nil - } - fetchRequest.fetchLimit = 1 - - if let results = try? context.fetch(fetchRequest) as? [Notification] { - return results.first - } - - return nil - } } // MARK: - Details Navigation Datasource @@ -1881,10 +1867,6 @@ private extension NotificationsViewController { return ContextManager.sharedInstance().mainContext } - var actionsService: NotificationActionsService { - return NotificationActionsService(coreDataStack: ContextManager.shared) - } - var userDefaults: UserPersistentRepository { return UserPersistentStoreFactory.instance() } @@ -2008,7 +1990,6 @@ private extension NotificationsViewController { enum Syncing { static let minimumPullToRefreshDelay = TimeInterval(1.5) static let pushMaxWait = TimeInterval(1.5) - static let syncTimeout = TimeInterval(10) static let undoTimeout = TimeInterval(4) } diff --git a/WordPress/Classes/ViewRelated/Notifications/FormattableContent/Ranges/NotificationContentRangeFactory.swift b/WordPress/Classes/ViewRelated/Notifications/FormattableContent/Ranges/NotificationContentRangeFactory.swift index 98e35c6ea947..e8239a18aa2f 100644 --- a/WordPress/Classes/ViewRelated/Notifications/FormattableContent/Ranges/NotificationContentRangeFactory.swift +++ b/WordPress/Classes/ViewRelated/Notifications/FormattableContent/Ranges/NotificationContentRangeFactory.swift @@ -74,7 +74,6 @@ struct NotificationContentRangeFactory: FormattableRangesFactory { enum RangeKeys { static let rawType = "type" static let url = "url" - static let indices = "indices" static let id = "id" static let value = "value" static let siteId = "site_id" diff --git a/WordPress/Classes/ViewRelated/Notifications/Style/WPStyleGuide+Notifications.swift b/WordPress/Classes/ViewRelated/Notifications/Style/WPStyleGuide+Notifications.swift index 1d7f9cb6ae1f..8541d573ab2e 100644 --- a/WordPress/Classes/ViewRelated/Notifications/Style/WPStyleGuide+Notifications.swift +++ b/WordPress/Classes/ViewRelated/Notifications/Style/WPStyleGuide+Notifications.swift @@ -12,26 +12,15 @@ extension WPStyleGuide { // NoteTableViewHeader public static let sectionHeaderBackgroundColor = UIColor.ungroupedListBackground - public static var sectionHeaderRegularStyle: [NSAttributedString.Key: Any] { - return [.paragraphStyle: sectionHeaderParagraph, - .font: sectionHeaderFont, - .foregroundColor: sectionHeaderTextColor] - } - // ListTableViewCell public static let unreadIndicatorColor = UIColor.primaryLight // Notification cells public static let noticonFont = UIFont(name: "Noticons", size: 16) - public static let noticonTextColor = UIColor.textInverted public static let noticonReadColor = UIColor.listSmallIcon public static let noticonUnreadColor = UIColor.primary - public static let noticonUnmoderatedColor = UIColor.warning public static let noteBackgroundReadColor = UIColor.ungroupedListBackground - public static let noteBackgroundUnreadColor = UIColor.ungroupedListUnread - - public static let noteSeparatorColor = blockSeparatorColor // Notification undo overlay public static let noteUndoBackgroundColor = UIColor.error @@ -268,12 +257,6 @@ extension WPStyleGuide { return (approved ? blockLinkColor : blockUnapprovedLinkColor) } - // Filters Helpers - public static func configureSegmentedControl(_ segmentedControl: UISegmentedControl) { - let style = [ NSAttributedString.Key.font: WPFontManager.systemRegularFont(ofSize: 12) ] - segmentedControl.setTitleTextAttributes(style, for: UIControl.State()) - } - // User Cell Helpers public static func configureFollowButton(_ button: UIButton) { // General @@ -321,7 +304,6 @@ extension WPStyleGuide { public static let headerFontSize = CGFloat(12) public static let headerLineSize = CGFloat(16) - public static let subjectFontSize = UIDevice.isPad() ? CGFloat(16) : CGFloat(14) public static let subjectNoticonSize = UIDevice.isPad() ? CGFloat(15) : CGFloat(14) public static let subjectLineSize = UIDevice.isPad() ? CGFloat(24) : CGFloat(18) public static let snippetLineSize = subjectLineSize @@ -335,9 +317,6 @@ extension WPStyleGuide { // // ParagraphStyle's - fileprivate static let sectionHeaderParagraph = NSMutableParagraphStyle( - minLineHeight: headerLineSize, lineBreakMode: .byWordWrapping, alignment: .natural - ) fileprivate static let subjectParagraph = NSMutableParagraphStyle( minLineHeight: subjectLineSize, lineBreakMode: .byWordWrapping, alignment: .natural ) @@ -358,7 +337,6 @@ extension WPStyleGuide { ) // Colors - fileprivate static let sectionHeaderTextColor = UIColor.textSubtle fileprivate static let subjectTextColor = UIColor.text fileprivate static let subjectNoticonColor = noticonReadColor fileprivate static let footerTextColor = UIColor.textSubtle @@ -367,9 +345,6 @@ extension WPStyleGuide { fileprivate static let headerTitleContextColor = UIColor.primary // Fonts - fileprivate static var sectionHeaderFont: UIFont { - return WPStyleGuide.fontForTextStyle(.caption1, fontWeight: .semibold) - } fileprivate static var subjectRegularFont: UIFont { return WPStyleGuide.fontForTextStyle(.subheadline) } diff --git a/WordPress/Classes/ViewRelated/Notifications/Tools/NotificationMediaDownloader.swift b/WordPress/Classes/ViewRelated/Notifications/Tools/NotificationMediaDownloader.swift index bb116d0c8e43..ce5b8928d0b9 100644 --- a/WordPress/Classes/ViewRelated/Notifications/Tools/NotificationMediaDownloader.swift +++ b/WordPress/Classes/ViewRelated/Notifications/Tools/NotificationMediaDownloader.swift @@ -98,7 +98,7 @@ class NotificationMediaDownloader: NSObject { let targetSize = cappedImageSize(originalImage.size, maximumWidth: maximumWidth) let resizedImage = resizedImagesMap[url] - if resizedImage == nil || resizedImage?.size == targetSize || resizedImage as? AnimatedImageWrapper != nil { + if resizedImage == nil || resizedImage?.size == targetSize || resizedImage as? AnimatedImage != nil { continue } @@ -193,7 +193,7 @@ class NotificationMediaDownloader: NSObject { /// private func resizeImageIfNeeded(_ image: UIImage, maximumWidth: CGFloat, callback: @escaping (UIImage) -> Void) { // Animated images aren't actually resized, so return the image itself if we've already recorded the target size - if let animatedImage = image as? AnimatedImageWrapper, animatedImage.targetSize != nil { + if let animatedImage = image as? AnimatedImage, animatedImage.targetSize != nil { callback(animatedImage) return } @@ -209,7 +209,7 @@ class NotificationMediaDownloader: NSObject { // If we try to resize the animate image it will lose all of its frames // Instead record the target size so we can properly set the bounds of the view later - if let animatedImage = image as? AnimatedImageWrapper, animatedImage.gifData != nil { + if let animatedImage = image as? AnimatedImage, animatedImage.gifData != nil { animatedImage.targetSize = targetSize resizedImage = animatedImage } else { diff --git a/WordPress/Classes/ViewRelated/Notifications/Views/NoteBlockActionsTableViewCell.swift b/WordPress/Classes/ViewRelated/Notifications/Views/NoteBlockActionsTableViewCell.swift index 95878a73e2ef..57dfbd092d48 100644 --- a/WordPress/Classes/ViewRelated/Notifications/Views/NoteBlockActionsTableViewCell.swift +++ b/WordPress/Classes/ViewRelated/Notifications/Views/NoteBlockActionsTableViewCell.swift @@ -366,11 +366,6 @@ private extension NoteBlockActionsTableViewCell { // MARK: - Private Constants // private extension NoteBlockActionsTableViewCell { - struct Edit { - static let normalTitle = NSLocalizedString("Edit", comment: "Verb, edit a comment") - static let normalHint = NSLocalizedString("Edits a comment", comment: "Edit Action Spoken hint.") - } - struct Constants { static let buttonSpacing = CGFloat(20) static let buttonSpacingCompact = CGFloat(2) diff --git a/WordPress/Classes/ViewRelated/Pages/BasePageListCell.h b/WordPress/Classes/ViewRelated/Pages/BasePageListCell.h deleted file mode 100644 index 6c33eb6a56b7..000000000000 --- a/WordPress/Classes/ViewRelated/Pages/BasePageListCell.h +++ /dev/null @@ -1,36 +0,0 @@ -#import - -@class AbstractPost; -@class BasePageListCell; - -NS_ASSUME_NONNULL_BEGIN - -/// A block that represents an action triggered by tapping on a button in a cell. -/// -/// @param cell The cell that contains the button that was tapped. -/// @param button The button that was tapped. -/// @param post The post represented by the cell that was tapped. -/// -typedef void(^BasePageListCellActionBlock)(BasePageListCell* cell, - UIButton* button, - AbstractPost* post); - -/// A base cell to represent a page object. -/// -@interface BasePageListCell : UITableViewCell - -/// The post represented by this cell. -/// -@property (nonatomic, strong, readwrite, nullable) AbstractPost *post; - -/// The block that will be executed when the main button inside this cell is tapped. -/// -@property (nonatomic, copy, readwrite, nullable) BasePageListCellActionBlock onAction; - -/// Configure the cell to represent the specified post. -/// -- (void)configureCell:(AbstractPost *)post; - -@end - -NS_ASSUME_NONNULL_END diff --git a/WordPress/Classes/ViewRelated/Pages/BasePageListCell.m b/WordPress/Classes/ViewRelated/Pages/BasePageListCell.m deleted file mode 100644 index 5ccee46b4b92..000000000000 --- a/WordPress/Classes/ViewRelated/Pages/BasePageListCell.m +++ /dev/null @@ -1,22 +0,0 @@ -#import "BasePageListCell.h" -#import "AbstractPost.h" - -@implementation BasePageListCell - -- (void)configureCell:(AbstractPost *)post -{ - self.post = post; -} - - -#pragma mark - Action - -- (IBAction)onAction:(UIButton *)sender -{ - if (self.onAction) { - NSAssert(self.post != nil, @"Expected the post to be set here."); - self.onAction(self, sender, self.post); - } -} - -@end diff --git a/WordPress/Classes/ViewRelated/Pages/PageListCell.swift b/WordPress/Classes/ViewRelated/Pages/PageListCell.swift new file mode 100644 index 000000000000..27f7c3af8678 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Pages/PageListCell.swift @@ -0,0 +1,156 @@ +import Foundation +import UIKit +import Combine + +final class PageListCell: UITableViewCell, PostSearchResultCell, Reusable { + + // MARK: - Views + + private let titleLabel = UILabel() + private let badgeIconView = UIImageView() + private let badgesLabel = UILabel() + private let featuredImageView = CachedAnimatedImageView() + private let ellipsisButton = UIButton(type: .custom) + private let contentStackView = UIStackView() + private var indentationIconView = UIImageView() + + // MARK: - Properties + + private lazy var imageLoader = ImageLoader(imageView: featuredImageView, loadingIndicator: SolidColorActivityIndicator()) + + // MARK: - PostSearchResultCell + + var attributedText: NSAttributedString? { + get { titleLabel.attributedText } + set { titleLabel.attributedText = newValue } + } + + // MARK: - Initializers + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setupViews() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Public + + override func prepareForReuse() { + super.prepareForReuse() + + imageLoader.prepareForReuse() + } + + func configure(with viewModel: PageListItemViewModel, indentation: Int = 0, isFirstSubdirectory: Bool = false, delegate: InteractivePostViewDelegate? = nil) { + if let delegate { + configureEllipsisButton(with: viewModel.page, delegate: delegate) + } + + titleLabel.attributedText = viewModel.title + + badgeIconView.image = viewModel.badgeIcon + badgeIconView.isHidden = viewModel.badgeIcon == nil + badgesLabel.attributedText = viewModel.badges + + featuredImageView.isHidden = viewModel.imageURL == nil + if let imageURL = viewModel.imageURL { + let host = MediaHost(with: viewModel.page) { error in + WordPressAppDelegate.crashLogging?.logError(error) + } + imageLoader.loadImage(with: imageURL, from: host, preferredSize: Constants.imageSize) + } + + separatorInset = UIEdgeInsets(top: 0, left: 16 + CGFloat(indentation) * 32, bottom: 0, right: 0) + contentStackView.directionalLayoutMargins = NSDirectionalEdgeInsets( + top: 12, + leading: 16 + CGFloat(max(0, indentation - 1)) * 32, + bottom: 12, + trailing: 16 + ) + indentationIconView.isHidden = indentation == 0 + indentationIconView.alpha = isFirstSubdirectory ? 1 : 0 // Still contribute to layout + } + + private func configureEllipsisButton(with page: Page, delegate: InteractivePostViewDelegate) { + ellipsisButton.showsMenuAsPrimaryAction = true + ellipsisButton.menu = AbstractPostMenuHelper(page).makeMenu(presentingView: ellipsisButton, delegate: delegate) + } + + // MARK: - Setup + + private func setupViews() { + setupLabels() + setupFeaturedImageView() + setupEllipsisButton() + + indentationIconView.tintColor = .secondaryLabel + indentationIconView.image = UIImage(named: "subdirectory") + indentationIconView.setContentHuggingPriority(.required, for: .horizontal) + indentationIconView.setContentCompressionResistancePriority(.required, for: .horizontal) + + let badgesStackView = UIStackView(arrangedSubviews: [ + badgeIconView, badgesLabel, UIView() + ]) + badgesStackView.alignment = .bottom + badgesStackView.spacing = 2 + + let labelsStackView = UIStackView(arrangedSubviews: [ + titleLabel, badgesStackView + ]) + labelsStackView.spacing = 4 + labelsStackView.axis = .vertical + + contentStackView.addArrangedSubviews([ + indentationIconView, labelsStackView, featuredImageView, ellipsisButton + ]) + contentStackView.spacing = 8 + contentStackView.alignment = .center + contentStackView.isLayoutMarginsRelativeArrangement = true + + NSLayoutConstraint.activate([ + badgeIconView.heightAnchor.constraint(equalToConstant: 18), + badgeIconView.heightAnchor.constraint(equalTo: badgeIconView.widthAnchor, multiplier: 1) + ]) + + contentView.addSubview(contentStackView) + contentStackView.translatesAutoresizingMaskIntoConstraints = false + contentView.pinSubviewToAllEdges(contentStackView) + } + + private func setupLabels() { + titleLabel.translatesAutoresizingMaskIntoConstraints = false + titleLabel.adjustsFontForContentSizeCategory = true + titleLabel.numberOfLines = 1 + + badgeIconView.tintColor = UIColor.secondaryLabel + } + + private func setupFeaturedImageView() { + featuredImageView.translatesAutoresizingMaskIntoConstraints = false + featuredImageView.contentMode = .scaleAspectFill + featuredImageView.layer.masksToBounds = true + featuredImageView.layer.cornerRadius = 5 + + NSLayoutConstraint.activate([ + featuredImageView.widthAnchor.constraint(equalToConstant: Constants.imageSize.width), + featuredImageView.heightAnchor.constraint(equalToConstant: Constants.imageSize.height), + ]) + } + + private func setupEllipsisButton() { + ellipsisButton.translatesAutoresizingMaskIntoConstraints = false + ellipsisButton.setImage(UIImage(named: "more-horizontal-mobile"), for: .normal) + ellipsisButton.tintColor = .listIcon + + NSLayoutConstraint.activate([ + ellipsisButton.widthAnchor.constraint(equalToConstant: 24) + ]) + } +} + +private enum Constants { + static let imageSize = CGSize(width: 44, height: 44) +} diff --git a/WordPress/Classes/ViewRelated/Pages/PageListItemViewModel.swift b/WordPress/Classes/ViewRelated/Pages/PageListItemViewModel.swift new file mode 100644 index 000000000000..2f58e0e70836 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Pages/PageListItemViewModel.swift @@ -0,0 +1,75 @@ +import Foundation + +final class PageListItemViewModel { + let page: Page + let title: NSAttributedString + let badgeIcon: UIImage? + let badges: NSAttributedString + let imageURL: URL? + let accessibilityIdentifier: String? + + init(page: Page) { + self.page = page + self.title = makeContentAttributedString(for: page) + self.badgeIcon = makeBadgeIcon(for: page) + self.badges = makeBadgesString(for: page) + self.imageURL = page.featuredImageURL + self.accessibilityIdentifier = page.slugForDisplay() + } +} + +private func makeContentAttributedString(for page: Page) -> NSAttributedString { + let page = page.hasRevision() ? page.revision : page + let title = page?.titleForDisplay() ?? "" + return NSAttributedString(string: title, attributes: [ + .font: WPStyleGuide.fontForTextStyle(.callout, fontWeight: .semibold), + .foregroundColor: UIColor.text + ]) +} + +private func makeBadgeIcon(for page: Page) -> UIImage? { + if page.isSiteHomepage { + return UIImage(named: "home") + } + if page.isSitePostsPage { + return UIImage(named: "posts") + } + return nil +} + +private func makeBadgesString(for page: Page) -> NSAttributedString { + var badges: [String] = [] + var colors: [Int: UIColor] = [:] + if page.isSiteHomepage { + badges.append(Strings.badgeHomepage) + } else if page.isSitePostsPage { + badges.append(Strings.badgePosts) + } + if let date = AbstractPostHelper.getLocalizedStatusWithDate(for: page) { + if page.status == .trash { + colors[badges.endIndex] = .systemRed + } + badges.append(date) + } + if page.hasPrivateState { + badges.append(Strings.badgePrivatePage) + } + if page.hasPendingReviewState { + badges.append(Strings.badgePendingReview) + } + if page.hasLocalChanges() { + badges.append(Strings.badgeLocalChanges) + } + + return AbstractPostHelper.makeBadgesString(with: badges.enumerated().map { index, badge in + (badge, colors[index]) + }) +} + +private enum Strings { + static let badgeHomepage = NSLocalizedString("pageList.badgeHomepage", value: "Homepage", comment: "Badge for page cells") + static let badgePosts = NSLocalizedString("pageList.badgePosts", value: "Posts page", comment: "Badge for page cells") + static let badgePrivatePage = NSLocalizedString("pageList.badgePrivate", value: "Private", comment: "Badge for page cells") + static let badgePendingReview = NSLocalizedString("pageList.badgePendingReview", value: "Pending review", comment: "Badge for page cells") + static let badgeLocalChanges = NSLocalizedString("pageList.badgeLocalChanges", value: "Local changes", comment: "Badge for page cells") +} diff --git a/WordPress/Classes/ViewRelated/Pages/PageListTableViewCell.h b/WordPress/Classes/ViewRelated/Pages/PageListTableViewCell.h deleted file mode 100644 index 964094bb5561..000000000000 --- a/WordPress/Classes/ViewRelated/Pages/PageListTableViewCell.h +++ /dev/null @@ -1,6 +0,0 @@ -#import -#import "BasePageListCell.h" - -@interface PageListTableViewCell : BasePageListCell - -@end diff --git a/WordPress/Classes/ViewRelated/Pages/PageListTableViewCell.m b/WordPress/Classes/ViewRelated/Pages/PageListTableViewCell.m deleted file mode 100644 index 3497f942ba5a..000000000000 --- a/WordPress/Classes/ViewRelated/Pages/PageListTableViewCell.m +++ /dev/null @@ -1,215 +0,0 @@ -#import "PageListTableViewCell.h" -#import "WPStyleGuide+Pages.h" -#import "WordPress-Swift.h" - -@import Gridicons; - - -static CGFloat const PageListTableViewCellTagLabelRadius = 2.0; -static CGFloat const FeaturedImageSize = 120.0; - -@interface PageListTableViewCell() - -@property (nonatomic, strong) IBOutlet UILabel *titleLabel; -@property (nonatomic, strong) IBOutlet UILabel *timestampLabel; -@property (nonatomic, strong) IBOutlet UILabel *badgesLabel; -@property (nonatomic, strong) IBOutlet UILabel *typeLabel; -@property (nonatomic, strong) IBOutlet UIImageView *typeIcon; -@property (strong, nonatomic) IBOutlet CachedAnimatedImageView *featuredImageView; -@property (nonatomic, strong) IBOutlet UIButton *menuButton; -@property (nonatomic, strong) IBOutlet NSLayoutConstraint *labelsContainerTrailing; -@property (nonatomic, strong) IBOutlet NSLayoutConstraint *leadingContentConstraint; - -@property (nonatomic, strong) ImageLoader *featuredImageLoader; -@property (nonatomic, strong) NSDateFormatter *dateFormatter; - -@end - -@implementation PageListTableViewCell { - CGFloat _indentationWidth; - NSInteger _indentationLevel; -} - -- (void)awakeFromNib -{ - [super awakeFromNib]; - self.contentView.autoresizingMask = UIViewAutoresizingFlexibleHeight; - [self applyStyles]; - [self setupAccessibility]; -} - -- (void)prepareForReuse -{ - [super prepareForReuse]; - - [self applyStyles]; - [self.featuredImageLoader prepareForReuse]; - [self setNeedsDisplay]; -} - -- (ImageLoader *)featuredImageLoader -{ - if (_featuredImageLoader == nil) { - _featuredImageLoader = [[ImageLoader alloc] initWithImageView:self.featuredImageView - gifStrategy:GIFStrategyLargeGIFs]; - } - return _featuredImageLoader; -} - -- (NSDateFormatter *)dateFormatter -{ - if (_dateFormatter == nil) { - _dateFormatter = [NSDateFormatter new]; - _dateFormatter.doesRelativeDateFormatting = YES; - _dateFormatter.dateStyle = NSDateFormatterNoStyle; - _dateFormatter.timeStyle = NSDateFormatterShortStyle; - } - return _dateFormatter; -} - -- (CGFloat)indentationWidth -{ - return _indentationWidth; -} - -- (NSInteger)indentationLevel -{ - return _indentationLevel; -} - -- (void)setIndentationWidth:(CGFloat)indentationWidth -{ - _indentationWidth = indentationWidth; - [self updateLeadingContentConstraint]; -} - -- (void)setIndentationLevel:(NSInteger)indentationLevel -{ - _indentationLevel = indentationLevel; - [self updateLeadingContentConstraint]; -} - - -#pragma mark - Accessors - -- (void)setPost:(AbstractPost *)post -{ - [super setPost:post]; - [self configureTitle]; - [self configureForStatus]; - [self configureBadges]; - [self configureFeaturedImage]; - self.accessibilityIdentifier = post.slugForDisplay; -} - -#pragma mark - Configuration - -- (void)applyStyles -{ - [WPStyleGuide configureTableViewCell:self]; - [WPStyleGuide configureLabel:self.timestampLabel textStyle:UIFontTextStyleSubheadline]; - [WPStyleGuide configureLabel:self.badgesLabel textStyle:UIFontTextStyleSubheadline]; - [WPStyleGuide configureLabel:self.typeLabel textStyle:UIFontTextStyleSubheadline]; - - self.titleLabel.font = [WPStyleGuide notoBoldFontForTextStyle:UIFontTextStyleHeadline]; - self.titleLabel.adjustsFontForContentSizeCategory = YES; - - self.titleLabel.textColor = [UIColor murielText]; - self.badgesLabel.textColor = [UIColor murielTextSubtle]; - self.typeLabel.textColor = [UIColor murielTextSubtle]; - self.menuButton.tintColor = [UIColor murielTextSubtle]; - [self.menuButton setImage:[UIImage gridiconOfType:GridiconTypeEllipsis] forState:UIControlStateNormal]; - - self.typeIcon.tintColor = [UIColor murielTextSubtle]; - - self.backgroundColor = [UIColor murielNeutral5]; - self.contentView.backgroundColor = [UIColor murielNeutral5]; - - self.featuredImageView.layer.cornerRadius = PageListTableViewCellTagLabelRadius; -} - -- (void)configureTitle -{ - AbstractPost *post = [self.post hasRevision] ? [self.post revision] : self.post; - self.titleLabel.text = [post titleForDisplay] ?: [NSString string]; -} - -- (void)configureForStatus -{ - if (self.post.isFailed && !self.post.hasLocalChanges) { - self.titleLabel.textColor = [UIColor murielError]; - self.menuButton.tintColor = [UIColor murielError]; - } -} - -- (void)updateLeadingContentConstraint -{ - self.leadingContentConstraint.constant = (CGFloat)_indentationLevel * _indentationWidth; -} - -- (void)configureBadges -{ - Page *page = (Page *)self.post; - - NSMutableArray *badges = [NSMutableArray new]; - - [self.typeLabel setText:@""]; - [self.typeIcon setImage:nil]; - - if (self.post.dateCreated != nil) { - NSString *timestamp = [self.post isScheduled] ? [self.dateFormatter stringFromDate:self.post.dateCreated] : [self.post.dateCreated mediumString]; - [badges addObject:timestamp]; - } - - if (page.isSiteHomepage) { - [badges addObject:@""]; - [self.typeLabel setText:NSLocalizedString(@"Homepage", @"Title of the Homepage Badge")]; - [self.typeIcon setImage:[UIImage gridiconOfType:GridiconTypeHouse]]; - } - - if (page.isSitePostsPage) { - [badges addObject:@""]; - [self.typeLabel setText:NSLocalizedString(@"Posts page", @"Title of the Posts Page Badge")]; - [self.typeIcon setImage:[UIImage gridiconOfType:GridiconTypePosts]]; - } - - if (page.hasPrivateState) { - [badges addObject:NSLocalizedString(@"Private", @"Title of the Private Badge")]; - } else if (page.hasPendingReviewState) { - [badges addObject:NSLocalizedString(@"Pending review", @"Title of the Pending Review Badge")]; - } - - if (page.hasLocalChanges) { - [badges addObject:NSLocalizedString(@"Local changes", @"Title of the Local Changes Badge")]; - } - - self.badgesLabel.text = [badges componentsJoinedByString:@" · "]; -} - -- (void)configureFeaturedImage -{ - Page *page = (Page *)self.post; - - BOOL hideFeaturedImage = page.featuredImage == nil; - self.featuredImageView.hidden = hideFeaturedImage; - self.labelsContainerTrailing.active = !hideFeaturedImage; - BOOL isBlogAtomic = [page.featuredImage.blog isAtomic]; - - if (!hideFeaturedImage) { - [self.featuredImageLoader loadImageFromMedia:page.featuredImage - preferredSize:CGSizeMake(FeaturedImageSize, FeaturedImageSize) - placeholder:nil - isBlogAtomic:isBlogAtomic - success:nil - error:^(NSError *error) { - DDLogError(@"Failed to load the media: %@", error); - }]; - - } -} - -- (void)setupAccessibility { - self.menuButton.accessibilityLabel = NSLocalizedString(@"More", @"Accessibility label for the More button in Page List."); -} - -@end diff --git a/WordPress/Classes/ViewRelated/Pages/PageListTableViewCell.xib b/WordPress/Classes/ViewRelated/Pages/PageListTableViewCell.xib deleted file mode 100644 index e42c25955b09..000000000000 --- a/WordPress/Classes/ViewRelated/Pages/PageListTableViewCell.xib +++ /dev/null @@ -1,138 +0,0 @@ - - - - - - - - - - - NotoSerif-Bold - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/WordPress/Classes/ViewRelated/Pages/PageListTableViewHandler.swift b/WordPress/Classes/ViewRelated/Pages/PageListTableViewHandler.swift deleted file mode 100644 index 80b697a51e1e..000000000000 --- a/WordPress/Classes/ViewRelated/Pages/PageListTableViewHandler.swift +++ /dev/null @@ -1,179 +0,0 @@ -import Foundation - - -final class PageListTableViewHandler: WPTableViewHandler { - var isSearching: Bool = false - var status: PostListFilter.Status = .published - var groupResults: Bool { - if isSearching { - return true - } - - return status == .scheduled - } - - var showEditorHomepage: Bool { - guard RemoteFeatureFlag.siteEditorMVP.enabled() else { - return false - } - - let isFSETheme = blog.blockEditorSettings?.isFSETheme ?? false - return isFSETheme && status == .published && !groupResults - } - - private var pages: [Page] = [] - private let blog: Blog - - private lazy var publishedResultController: NSFetchedResultsController = { - let publishedFilter = PostListFilter.publishedFilter() - let fetchRequest = NSFetchRequest(entityName: Page.entityName()) - let predicate = NSPredicate(format: "\(#keyPath(Page.blog)) = %@ && \(#keyPath(Page.revision)) = nil", blog) - let predicates = [predicate, publishedFilter.predicateForFetchRequest] - fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates) - fetchRequest.sortDescriptors = publishedFilter.sortDescriptors - return resultsController(with: fetchRequest, context: managedObjectContext(), performFetch: false) - }() - - private lazy var searchResultsController: NSFetchedResultsController = { - return resultsController(with: fetchRequest(), context: managedObjectContext(), keyPath: BasePost.statusKeyPath, performFetch: false) - }() - - private lazy var groupedResultsController: NSFetchedResultsController = { - return resultsController(with: fetchRequest(), context: managedObjectContext(), keyPath: sectionNameKeyPath()) - }() - - private lazy var flatResultsController: NSFetchedResultsController = { - return resultsController(with: fetchRequest(), context: managedObjectContext()) - }() - - - init(tableView: UITableView, blog: Blog) { - self.blog = blog - super.init(tableView: tableView) - } - - override var resultsController: NSFetchedResultsController { - if isSearching { - return searchResultsController - } - - return groupResults ? groupedResultsController : flatResultsController - } - - override func refreshTableView() { - refreshTableView(at: nil) - } - - func refreshTableView(at indexPath: IndexPath?) { - super.clearCachedRowHeights() - - do { - try resultsController.performFetch() - pages = setupPages() - } catch { - DDLogError("Error fetching pages after refreshing the table: \(error)") - } - - if let indexPath = indexPath { - tableView.reloadSections(IndexSet(integer: indexPath.section), with: .fade) - } else { - tableView.reloadData() - } - } - - // MARK: - Public methods - - func page(at indexPath: IndexPath) -> Page { - guard groupResults else { - return pages[indexPath.row] - } - - guard let page = resultsController.object(at: indexPath) as? Page else { - // Retrieveing anything other than a post object means we have an app with an invalid - // state. Ignoring this error would be counter productive as we have no idea how this - // can affect the App. This controlled interruption is intentional. - // - // - Diego Rey Mendez, May 18 2016 - // - fatalError("Expected a Page object.") - } - - return page - } - - func index(for page: Page) -> Int? { - return pages.firstIndex(of: page) - } - - func removePage(from index: Int?) -> [Page] { - guard let index = index, status == .published else { - do { - try publishedResultController.performFetch() - if let pages = publishedResultController.fetchedObjects as? [Page] { - return pages.setHomePageFirst().hierarchySort() - } - } catch { - DDLogError("Error fetching pages after refreshing the table: \(error)") - } - - return [] - } - - return pages.remove(from: index) - } - - - // MARK: - Override TableView Datasource - - override func numberOfSections(in tableView: UITableView) -> Int { - return groupResults ? super.numberOfSections(in: tableView) : 1 - } - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - let additionalPage = showEditorHomepage ? 1 : 0 - return groupResults ? super.tableView(tableView, numberOfRowsInSection: section) : pages.count + additionalPage - } - - - // MARK: - Private methods - - private func resultsController(with request: NSFetchRequest?, - context: NSManagedObjectContext?, - keyPath: String? = nil, - performFetch: Bool = true) -> NSFetchedResultsController { - guard let request = request, let context = context else { - fatalError("A request and a context must exist") - } - - let controller = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: keyPath, cacheName: nil) - if performFetch { - do { - try controller.performFetch() - } catch { - DDLogError("Error fetching pages after refreshing the table: \(error)") - } - } - - return controller - } - - private func fetchRequest() -> NSFetchRequest? { - return delegate?.fetchRequest() - } - - private func managedObjectContext() -> NSManagedObjectContext? { - return delegate?.managedObjectContext() - } - - private func sectionNameKeyPath() -> String? { - return delegate?.sectionNameKeyPath!() - } - - private func setupPages() -> [Page] { - guard !groupResults, let pages = resultsController.fetchedObjects as? [Page] else { - return [] - } - - return status == .published ? pages.setHomePageFirst().hierarchySort() : pages - } -} diff --git a/WordPress/Classes/ViewRelated/Pages/PageListViewController+Menu.swift b/WordPress/Classes/ViewRelated/Pages/PageListViewController+Menu.swift new file mode 100644 index 000000000000..82b74b55fedd --- /dev/null +++ b/WordPress/Classes/ViewRelated/Pages/PageListViewController+Menu.swift @@ -0,0 +1,139 @@ +import Foundation + +extension PageListViewController: InteractivePostViewDelegate { + + func edit(_ apost: AbstractPost) { + guard let page = apost as? Page else { return } + + let didOpenEditor = PageEditorPresenter.handle(page: page, in: self, entryPoint: .pagesList) + if didOpenEditor { + WPAppAnalytics.track(.postListEditAction, withProperties: propertiesForAnalytics(), with: page) + } + } + + func view(_ apost: AbstractPost) { + viewPost(apost) + } + + func stats(for apost: AbstractPost) { + // Not available for pages + } + + func duplicate(_ apost: AbstractPost) { + guard let page = apost as? Page else { return } + copyPage(page) + } + + func publish(_ apost: AbstractPost) { + publishPost(apost) + } + + func trash(_ post: AbstractPost, completion: @escaping () -> Void) { + guard let page = post as? Page else { return } + trashPage(page, completion: completion) + } + + func draft(_ apost: AbstractPost) { + moveToDraft(apost) + } + + func retry(_ apost: AbstractPost) { + guard let page = apost as? Page else { return } + PostCoordinator.shared.save(page) + } + + func cancelAutoUpload(_ apost: AbstractPost) { + // Not available for pages + } + + func share(_ apost: AbstractPost, fromView view: UIView) { + // Not available for pages + } + + func blaze(_ apost: AbstractPost) { + BlazeEventsTracker.trackEntryPointTapped(for: .pagesList) + BlazeFlowCoordinator.presentBlaze(in: self, source: .pagesList, blog: blog, post: apost) + } + + func comments(_ apost: AbstractPost) { + // Not available for pages + } + + func showSettings(for post: AbstractPost) { + WPAnalytics.track(.postListSettingsAction, properties: propertiesForAnalytics()) + PostSettingsViewController.showStandaloneEditor(for: post, from: self) + } + + func setParent(for apost: AbstractPost) { + guard let page = apost as? Page else { return } + Task { + await setParentPage(for: page) + } + } + + func setHomepage(for apost: AbstractPost) { + guard let page = apost as? Page else { return } + WPAnalytics.track(.postListSetAsPostsPageAction) + setPageAsHomepage(page) + } + + func setPostsPage(for apost: AbstractPost) { + guard let page = apost as? Page else { return } + WPAnalytics.track(.postListSetHomePageAction) + togglePageAsPostsPage(page) + } + + func setRegularPage(for apost: AbstractPost) { + guard let page = apost as? Page else { return } + WPAnalytics.track(.postListSetAsRegularPageAction) + togglePageAsPostsPage(page) + } + + // MARK: - Helpers + + private func copyPage(_ page: Page) { + // Analytics + WPAnalytics.track(.postListDuplicateAction, withProperties: propertiesForAnalytics()) + // Copy Page + let newPage = page.blog.createDraftPage() + newPage.postTitle = page.postTitle + newPage.content = page.content + // Open Editor + let editorViewController = EditPageViewController(page: newPage) + present(editorViewController, animated: false) + } + + private func trashPage(_ page: Page, completion: @escaping () -> Void) { + let isPageTrashed = page.status == .trash + let actionText = isPageTrashed ? Strings.DeletePermanently.actionText : Strings.Trash.actionText + let titleText = isPageTrashed ? Strings.DeletePermanently.titleText : Strings.Trash.titleText + let messageText = isPageTrashed ? Strings.DeletePermanently.messageText : Strings.Trash.messageText + + let alertController = UIAlertController(title: titleText, message: messageText, preferredStyle: .alert) + alertController.addCancelActionWithTitle(Strings.cancelText) { _ in + completion() + } + alertController.addDestructiveActionWithTitle(actionText) { [weak self] _ in + self?.deletePost(page) + completion() + } + alertController.presentFromRootViewController() + } +} + +private enum Strings { + + static let cancelText = NSLocalizedString("pagesList.trash.cancel", value: "Cancel", comment: "Cancels an Action") + + enum DeletePermanently { + static let actionText = NSLocalizedString("pagesList.deletePermanently.actionTitle", value: "Delete Permanently", comment: "Delete option in the confirmation alert when deleting a page from the trash.") + static let titleText = NSLocalizedString("pagesList.deletePermanently.alertTitle", value: "Delete Permanently?", comment: "Title of the confirmation alert when deleting a page from the trash.") + static let messageText = NSLocalizedString("pagesList.deletePermanently.alertMessage", value: "Are you sure you want to permanently delete this page?", comment: "Message of the confirmation alert when deleting a page from the trash.") + } + + enum Trash { + static let actionText = NSLocalizedString("pagesList.trash.actionTitle", value: "Move to Trash", comment: "Trash option in the trash page confirmation alert.") + static let titleText = NSLocalizedString("pagesList.trash.alertTitle", value: "Trash this page?", comment: "Title of the trash page confirmation alert.") + static let messageText = NSLocalizedString("pagesList.trash.alertMessage", value: "Are you sure you want to trash this page?", comment: "Message of the trash page confirmation alert.") + } +} diff --git a/WordPress/Classes/ViewRelated/Pages/PageListViewController.swift b/WordPress/Classes/ViewRelated/Pages/PageListViewController.swift index 80f26c4c05c9..dac04f114c0b 100644 --- a/WordPress/Classes/ViewRelated/Pages/PageListViewController.swift +++ b/WordPress/Classes/ViewRelated/Pages/PageListViewController.swift @@ -4,23 +4,16 @@ import WordPressShared import WordPressFlux import UIKit -class PageListViewController: AbstractPostListViewController, UIViewControllerRestoration { +final class PageListViewController: AbstractPostListViewController, UIViewControllerRestoration { private struct Constant { struct Size { - static let pageSectionHeaderHeight = CGFloat(40.0) static let pageCellEstimatedRowHeight = CGFloat(44.0) - static let pageCellWithTagEstimatedRowHeight = CGFloat(60.0) - static let pageListTableViewCellLeading = CGFloat(16.0) } struct Identifiers { static let pagesViewControllerRestorationKey = "PagesViewControllerRestorationKey" static let pageCellIdentifier = "PageCellIdentifier" - static let pageCellNibName = "PageListTableViewCell" - static let restorePageCellIdentifier = "RestorePageCellIdentifier" - static let restorePageCellNibName = "RestorePageTableViewCell" static let templatePageCellIdentifier = "TemplatePageCellIdentifier" - static let currentPageListStatusFilterKey = "CurrentPageListStatusFilterKey" } struct Events { @@ -32,32 +25,12 @@ class PageListViewController: AbstractPostListViewController, UIViewControllerRe static let editorUrl = "site-editor.php?canvas=edit" } - fileprivate lazy var sectionFooterSeparatorView: UIView = { - let footer = UIView() - footer.backgroundColor = .neutral(.shade10) - return footer - }() - - private lazy var _tableViewHandler: PageListTableViewHandler = { - let tableViewHandler = PageListTableViewHandler(tableView: self.tableView, blog: self.blog) - tableViewHandler.cacheRowHeights = false - tableViewHandler.delegate = self - tableViewHandler.listensForContentChanges = false - tableViewHandler.updateRowAnimation = .none - return tableViewHandler - }() - - override var tableViewHandler: WPTableViewHandler { - get { - return _tableViewHandler - } set { - super.tableViewHandler = newValue - } + private enum Section: Int { + case templates = 0 + case pages = 1 } - lazy var homepageSettingsService = { - return HomepageSettingsService(blog: blog, coreDataStack: ContextManager.shared) - }() + private lazy var homepageSettingsService = HomepageSettingsService(blog: blog, coreDataStack: ContextManager.shared) private lazy var createButtonCoordinator: CreateButtonCoordinator = { let action = PageAction(handler: { [weak self] in @@ -66,32 +39,30 @@ class PageListViewController: AbstractPostListViewController, UIViewControllerRe return CreateButtonCoordinator(self, actions: [action], source: Constant.Events.source) }() - private lazy var editorSettingsService = { - return BlockEditorSettingsService(blog: blog, coreDataStack: ContextManager.shared) - }() + private var showEditorHomepage: Bool { + guard RemoteFeatureFlag.siteEditorMVP.enabled() else { + return false + } + let isFSETheme = blog.blockEditorSettings?.isFSETheme ?? false + return isFSETheme && filterSettings.currentPostListFilter().filterType == .published + } - // MARK: - GUI + private lazy var editorSettingsService = BlockEditorSettingsService(blog: blog, coreDataStack: ContextManager.shared) - @IBOutlet weak var filterTabBarTopConstraint: NSLayoutConstraint! - @IBOutlet weak var filterTabBariOS10TopConstraint: NSLayoutConstraint! - @IBOutlet weak var filterTabBarBottomConstraint: NSLayoutConstraint! - @IBOutlet weak var tableViewTopConstraint: NSLayoutConstraint! + private var pages: [Page] = [] + + private var fetchAllPagesTask: Task<[TaggedManagedObjectID], Error>? // MARK: - Convenience constructors @objc class func controllerWithBlog(_ blog: Blog) -> PageListViewController { - - let storyBoard = UIStoryboard(name: "Pages", bundle: Bundle.main) - let controller = storyBoard.instantiateViewController(withIdentifier: "PageListViewController") as! PageListViewController - - controller.blog = blog - controller.restorationClass = self - + let vc = PageListViewController() + vc.blog = blog + vc.restorationClass = self if QuickStartTourGuide.shared.isCurrentElement(.pages) { - controller.filterSettings.setFilterWithPostStatus(BasePost.Status.publish) + vc.filterSettings.setFilterWithPostStatus(BasePost.Status.publish) } - - return controller + return vc } static func showForBlog(_ blog: Blog, from sourceController: UIViewController) { @@ -120,7 +91,6 @@ class PageListViewController: AbstractPostListViewController, UIViewControllerRe return controllerWithBlog(restoredBlog) } - // MARK: - UIStateRestoring override func encodeRestorableState(with coder: NSCoder) { @@ -132,16 +102,8 @@ class PageListViewController: AbstractPostListViewController, UIViewControllerRe super.encodeRestorableState(with: coder) } - // MARK: - UIViewController - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - super.refreshNoResultsViewController = { [weak self] noResultsViewController in - self?.handleRefreshNoResultsViewController(noResultsViewController) - } - super.tableViewController = (segue.destination as! UITableViewController) - } - override func viewDidLoad() { super.viewDidLoad() @@ -153,16 +115,11 @@ class PageListViewController: AbstractPostListViewController, UIViewControllerRe title = NSLocalizedString("Pages", comment: "Title of the screen showing the list of pages for a blog.") - configureFilterBarTopConstraint() - createButtonCoordinator.add(to: view, trailingAnchor: view.safeAreaLayoutGuide.trailingAnchor, bottomAnchor: view.safeAreaLayoutGuide.bottomAnchor) - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - _tableViewHandler.status = filterSettings.currentPostListFilter().filterType - _tableViewHandler.refreshTableView() + refreshNoResultsViewController = { [weak self] in + self?.handleRefreshNoResultsViewController($0) + } } override func viewDidAppear(_ animated: Bool) { @@ -176,6 +133,11 @@ class PageListViewController: AbstractPostListViewController, UIViewControllerRe override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) QuickStartTourGuide.shared.endCurrentTour() + + if self.isMovingFromParent { + fetchAllPagesTask?.cancel() + fetchAllPagesTask = nil + } } override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { @@ -189,47 +151,17 @@ class PageListViewController: AbstractPostListViewController, UIViewControllerRe // MARK: - Configuration - private func configureFilterBarTopConstraint() { - filterTabBariOS10TopConstraint.isActive = false - } - override func configureTableView() { + super.configureTableView() + tableView.accessibilityIdentifier = "PagesTable" tableView.estimatedRowHeight = Constant.Size.pageCellEstimatedRowHeight - tableView.rowHeight = UITableView.automaticDimension - - let bundle = Bundle.main - - // Register the cells - let pageCellNib = UINib(nibName: Constant.Identifiers.pageCellNibName, bundle: bundle) - tableView.register(pageCellNib, forCellReuseIdentifier: Constant.Identifiers.pageCellIdentifier) - - let restorePageCellNib = UINib(nibName: Constant.Identifiers.restorePageCellNibName, bundle: bundle) - tableView.register(restorePageCellNib, forCellReuseIdentifier: Constant.Identifiers.restorePageCellIdentifier) + tableView.register(PageListCell.self, forCellReuseIdentifier: Constant.Identifiers.pageCellIdentifier) tableView.register(TemplatePageTableViewCell.self, forCellReuseIdentifier: Constant.Identifiers.templatePageCellIdentifier) - - WPStyleGuide.configureColors(view: view, tableView: tableView) } - override func configureSearchController() { - super.configureSearchController() - - tableView.tableHeaderView = searchController.searchBar - - tableView.verticalScrollIndicatorInsets.top = searchController.searchBar.bounds.height - } - - override func configureFooterView() { - super.configureFooterView() - tableView.tableFooterView = UIView(frame: .zero) - } - - fileprivate func beginRefreshingManually() { - guard let refreshControl = refreshControl else { - return - } - + private func beginRefreshingManually() { refreshControl.beginRefreshing() tableView.setContentOffset(CGPoint(x: 0, y: tableView.contentOffset.y - refreshControl.frame.size.height), animated: true) } @@ -240,6 +172,22 @@ class PageListViewController: AbstractPostListViewController, UIViewControllerRe return .page } + @MainActor + override func syncPosts(isFirstPage: Bool) async throws -> SyncPostResult { + let coreDataStack = ContextManager.shared + let filter = filterSettings.currentPostListFilter() + let author = filterSettings.shouldShowOnlyMyPosts() ? blogUserID() : nil + let blogID = TaggedManagedObjectID(blog) + + let repository = PostRepository(coreDataStack: coreDataStack) + let task = repository.fetchAllPages(statuses: filter.statuses, authorUserID: author, in: blogID) + self.fetchAllPagesTask = task + + let posts = try await task.value.map { try coreDataStack.mainContext.existingObject(with: $0) } + + return (posts, false) + } + override func syncHelper(_ syncHelper: WPContentSyncHelper, syncContentWithUserInteraction userInteraction: Bool, success: ((Bool) -> ())?, failure: ((NSError) -> ())?) { // The success and failure blocks are called in the parent class `AbstractPostListViewController` by the `syncPosts` method. Since that class is // used by both this one and the "Posts" screen, making changes to the sync helper is tough. To get around that, we make the fetch settings call @@ -282,96 +230,57 @@ class PageListViewController: AbstractPostListViewController, UIViewControllerRe return (success: wrappedSuccess, failure: wrappedFailure) } - override internal func lastSyncDate() -> Date? { - return blog?.lastPagesSync - } - - override func selectedFilterDidChange(_ filterBar: FilterTabBar) { - filterSettings.setCurrentFilterIndex(filterBar.selectedIndex) - _tableViewHandler.status = filterSettings.currentPostListFilter().filterType - _tableViewHandler.refreshTableView() - - super.selectedFilterDidChange(filterBar) - } - - override func updateFilterWithPostStatus(_ status: BasePost.Status) { - filterSettings.setFilterWithPostStatus(status) - _tableViewHandler.status = filterSettings.currentPostListFilter().filterType - _tableViewHandler.refreshTableView() - super.updateFilterWithPostStatus(status) - } - override func updateAndPerformFetchRequest() { super.updateAndPerformFetchRequest() - _tableViewHandler.refreshTableView() + Task { + await reloadPagesAndUI() + } } - override func syncPostsMatchingSearchText() { - guard let searchText = searchController.searchBar.text, !searchText.isEmpty() else { - return - } + @MainActor + private func reloadPagesAndUI() async { + let status = filterSettings.currentPostListFilter().filterType + let pages = (fetchResultsController.fetchedObjects ?? []) as! [Page] - postsSyncWithSearchDidBegin() + if status == .published { + let coreDataStack = ContextManager.shared + let pageIDs = pages.map { TaggedManagedObjectID($0) } - let author = filterSettings.shouldShowOnlyMyPosts() ? blogUserID() : nil - let postService = PostService(managedObjectContext: managedObjectContext()) - let options = PostServiceSyncOptions() - options.statuses = filterSettings.availablePostListFilters().flatMap { $0.statuses.strings } - options.authorID = author - options.number = 20 - options.purgesLocalSync = false - options.search = searchText - - postService.syncPosts( - ofType: postTypeToSync(), - with: options, - for: blog, - success: { [weak self] posts in - self?.postsSyncWithSearchEnded() - }, failure: { [weak self] (error) in - self?.postsSyncWithSearchEnded() + do { + self.pages = try await buildPageTree(pageIDs: pageIDs) + .hierarchyList(in: coreDataStack.mainContext) + } catch { + DDLogError("Failed to reload published pages: \(error)") } - ) - } - - override func sortDescriptorsForFetchRequest() -> [NSSortDescriptor] { - if !searchController.isActive { - return super.sortDescriptorsForFetchRequest() + } else { + self.pages = pages } - let descriptor = NSSortDescriptor(key: BasePost.statusKeyPath, ascending: true) - return [descriptor] + tableView.reloadData() + refreshResults() } - override func updateForLocalPostsMatchingSearchText() { - guard searchController.isActive else { - hideNoResultsView() - return - } - - _tableViewHandler.isSearching = true - updateAndPerformFetchRequest() - tableView.reloadData() + /// Build page hierachy in background, which should not take long (less than 2 seconds for 6000+ pages). + @MainActor + func buildPageTree(pageIDs: [TaggedManagedObjectID]? = nil, request: NSFetchRequest? = nil) async throws -> PageTree { + assert(pageIDs != nil || request != nil, "`pageIDs` and `request` can not both be nil") - hideNoResultsView() + let coreDataStack = ContextManager.shared + return try await coreDataStack.performQuery { context in + var pages = [Page]() - if let text = searchController.searchBar.text, - text.isEmpty || - tableViewHandler.resultsController?.fetchedObjects?.count == 0 { - showNoResultsView() - } - } + if let pageIDs { + pages = try pageIDs.map(context.existingObject(with:)) + } else if let request { + pages = try context.fetch(request) + } - override func showNoResultsView() { - super.showNoResultsView() + pages = pages.setHomePageFirst() - if searchController.isActive { - noResultsViewController.view.frame = CGRect(x: 0.0, - y: searchController.searchBar.bounds.height, - width: tableView.frame.width, - height: max(tableView.frame.height, tableView.contentSize.height)) - tableView.bringSubviewToFront(noResultsViewController.view) + let tree = PageTree() + tree.add(pages) + return tree } } @@ -382,25 +291,23 @@ class PageListViewController: AbstractPostListViewController, UIViewControllerRe } } + // MARK: - NSFetchedResultsControllerDelegate - // MARK: - Model Interaction + override func controllerWillChangeContent(_ controller: NSFetchedResultsController) { + // Do nothing + } + + override func controller(_ controller: NSFetchedResultsController, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) { + // Do nothing, refresh all + } - /// Retrieves the page object at the specified index path. - /// - /// - Parameter indexPath: the index path of the page object to retrieve. - /// - /// - Returns: the requested page. - /// - fileprivate func pageAtIndexPath(_ indexPath: IndexPath) -> Page { - if _tableViewHandler.showEditorHomepage { - // Since we're adding a fake homepage cell, we need to adjust the index path to match - let adjustedIndexPath = IndexPath(row: indexPath.row - 1, section: indexPath.section) - return _tableViewHandler.page(at: adjustedIndexPath) + override func controllerDidChangeContent(_ controller: NSFetchedResultsController) { + Task { + await reloadPagesAndUI() } - return _tableViewHandler.page(at: indexPath) } - // MARK: - TableView Handler Delegate Methods + // MARK: - Core Data override func entityName() -> String { return String(describing: Page.self) @@ -422,73 +329,35 @@ class PageListViewController: AbstractPostListViewController, UIViewControllerRe predicates.append(authorPredicate) } - let searchText = currentSearchTerm() ?? "" - let filterPredicate = searchController.isActive ? NSPredicate(format: "postTitle CONTAINS[cd] %@", searchText) : filterSettings.currentPostListFilter().predicateForFetchRequest + let filterPredicate = filterSettings.currentPostListFilter().predicateForFetchRequest + predicates.append(filterPredicate) - // If we have recently trashed posts, create an OR predicate to find posts matching the filter, - // or posts that were recently deleted. - if searchText.count == 0 && recentlyTrashedPostObjectIDs.count > 0 { - - let trashedPredicate = NSPredicate(format: "SELF IN %@", recentlyTrashedPostObjectIDs) - - predicates.append(NSCompoundPredicate(orPredicateWithSubpredicates: [filterPredicate, trashedPredicate])) - } else { - predicates.append(filterPredicate) - } + if filterSettings.shouldShowOnlyMyPosts() { + let myAuthorID = blogUserID() ?? 0 - if searchText.count > 0 { - let searchPredicate = NSPredicate(format: "postTitle CONTAINS[cd] %@", searchText) - predicates.append(searchPredicate) + // Brand new local drafts have an authorID of 0. + let authorPredicate = NSPredicate(format: "authorID = %@ || authorID = 0", myAuthorID) + predicates.append(authorPredicate) } if RemoteFeatureFlag.siteEditorMVP.enabled(), - blog.blockEditorSettings?.isFSETheme ?? false, - let homepageID = blog.homepagePageID, - let homepageType = blog.homepageType, + blog.blockEditorSettings?.isFSETheme ?? false, + let homepageID = blog.homepagePageID, + let homepageType = blog.homepageType, homepageType == .page { - let homepagePredicate = NSPredicate(format: "postID != %i", homepageID) - predicates.append(homepagePredicate) + predicates.append(NSPredicate(format: "postID != %i", homepageID)) } let predicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates) return predicate } + // MARK: - UITableViewDelegate - // MARK: - Table View Handling - - func sectionNameKeyPath() -> String { - let sortField = filterSettings.currentPostListFilter().sortField - return Page.sectionIdentifier(dateKeyPath: sortField.keyPath) - } - - func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - guard _tableViewHandler.groupResults else { - return 0.0 - } - return Constant.Size.pageSectionHeaderHeight - } - - func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - guard _tableViewHandler.groupResults else { - return UIView(frame: .zero) - } - - let sectionInfo = _tableViewHandler.resultsController.sections?[section] - let nibName = String(describing: PageListSectionHeaderView.self) - let headerView = Bundle.main.loadNibNamed(nibName, owner: nil, options: nil)?.first as? PageListSectionHeaderView - - if let sectionInfo = sectionInfo, let headerView = headerView { - headerView.setTitle(PostSearchHeader.title(forStatus: sectionInfo.name)) - } - - return headerView - } - - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) - - if indexPath.row == 0 && _tableViewHandler.showEditorHomepage { + switch Section(rawValue: indexPath.section)! { + case .templates: WPAnalytics.track(.pageListEditHomepageTapped) guard let editorUrl = URL(string: blog.adminUrl(withPath: Constant.editorUrl)) else { return @@ -499,70 +368,72 @@ class PageListViewController: AbstractPostListViewController, UIViewControllerRe source: Constant.Events.editHomepageSource) let navigationController = UINavigationController(rootViewController: webViewController) present(navigationController, animated: true) - } else { - let page = pageAtIndexPath(indexPath) - editPage(page) + case .pages: + let page = pages[indexPath.row] + edit(page) } } - @objc func tableView(_ tableView: UITableView, cellForRowAtIndexPath indexPath: IndexPath) -> UITableViewCell { - if let windowlessCell = dequeCellForWindowlessLoadingIfNeeded(tableView) { - return windowlessCell + func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { + guard indexPath.section == Section.pages.rawValue else { return nil } + return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { [weak self] _ in + guard let self else { return nil } + let page = self.pages[indexPath.row] + let cell = self.tableView.cellForRow(at: indexPath) + return AbstractPostMenuHelper(page).makeMenu(presentingView: cell ?? UIView(), delegate: self) } + } - if indexPath.row == 0 && _tableViewHandler.showEditorHomepage { - let identifier = Constant.Identifiers.templatePageCellIdentifier - let cell = tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath) - return cell - } + func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + guard indexPath.section == Section.pages.rawValue else { return nil } + let actions = AbstractPostHelper.makeLeadingContextualActions(for: pages[indexPath.row], delegate: self) + return UISwipeActionsConfiguration(actions: actions) + } - let page = pageAtIndexPath(indexPath) + func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + guard indexPath.section == Section.pages.rawValue else { return nil } + let actions = AbstractPostHelper.makeTrailingContextualActions(for: pages[indexPath.row], delegate: self) + return UISwipeActionsConfiguration(actions: actions) + } - let identifier = cellIdentifierForPage(page) - let cell = tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath) + // MARK: - UITableViewDataSource - configureCell(cell, at: indexPath) - return cell + func numberOfSections(in tableView: UITableView) -> Int { + 2 } - override func configureCell(_ cell: UITableViewCell, at indexPath: IndexPath) { - guard let cell = cell as? BasePageListCell else { - preconditionFailure("The cell should be of class \(String(describing: BasePageListCell.self))") + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + switch Section(rawValue: section)! { + case .templates: + return showEditorHomepage ? 1 : 0 + case .pages: + return pages.count } + } - cell.accessoryType = .none - - let page = pageAtIndexPath(indexPath) - let filterType = filterSettings.currentPostListFilter().filterType - - if cell.reuseIdentifier == Constant.Identifiers.pageCellIdentifier { - cell.indentationWidth = _tableViewHandler.isSearching ? 0.0 : Constant.Size.pageListTableViewCellLeading - cell.indentationLevel = filterType != .published ? 0 : page.hierarchyIndex - cell.onAction = { [weak self] cell, button, page in - self?.handleMenuAction(fromCell: cell, fromButton: button, forPage: page) - } - } else if cell.reuseIdentifier == Constant.Identifiers.restorePageCellIdentifier { - cell.selectionStyle = .none - cell.onAction = { [weak self] cell, _, page in - self?.handleRestoreAction(fromCell: cell, forPage: page) - } + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + switch Section(rawValue: indexPath.section)! { + case .templates: + let identifier = Constant.Identifiers.templatePageCellIdentifier + let cell = tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath) + return cell + case .pages: + let cell = tableView.dequeueReusableCell(withIdentifier: Constant.Identifiers.pageCellIdentifier, for: indexPath) as! PageListCell + let page = pages[indexPath.row] + let indentation = getIndentationLevel(at: indexPath) + let isFirstSubdirectory = getIndentationLevel(at: IndexPath(row: indexPath.row - 1, section: indexPath.section)) == (indentation - 1) + let viewModel = PageListItemViewModel(page: page) + cell.configure(with: viewModel, indentation: indentation, isFirstSubdirectory: isFirstSubdirectory, delegate: self) + return cell } - - cell.contentView.backgroundColor = UIColor.listForeground - - cell.configureCell(page) } - fileprivate func cellIdentifierForPage(_ page: Page) -> String { - var identifier: String - - if recentlyTrashedPostObjectIDs.contains(page.objectID) == true && filterSettings.currentPostListFilter().filterType != .trashed { - identifier = Constant.Identifiers.restorePageCellIdentifier - } else { - identifier = Constant.Identifiers.pageCellIdentifier + private func getIndentationLevel(at indexPath: IndexPath) -> Int { + guard filterSettings.currentPostListFilter().filterType == .published, + indexPath.row > 0 else { + return 0 } - - return identifier + return pages[indexPath.row].hierarchyIndex } // MARK: - Post Actions @@ -582,324 +453,28 @@ class PageListViewController: AbstractPostListViewController, UIViewControllerRe QuickStartTourGuide.shared.visited(.newPage) } - private func blazePage(_ page: AbstractPost) { - BlazeEventsTracker.trackEntryPointTapped(for: .pagesList) - BlazeFlowCoordinator.presentBlaze(in: self, source: .pagesList, blog: blog, post: page) - } - - fileprivate func editPage(_ page: Page) { - let didOpenEditor = PageEditorPresenter.handle(page: page, in: self, entryPoint: .pagesList) - - if didOpenEditor { - WPAppAnalytics.track(.postListEditAction, withProperties: propertiesForAnalytics(), with: page) - } - } - - fileprivate func copyPage(_ page: Page) { - // Analytics - WPAnalytics.track(.postListDuplicateAction, withProperties: propertiesForAnalytics()) - // Copy Page - let newPage = page.blog.createDraftPage() - newPage.postTitle = page.postTitle - newPage.content = page.content - // Open Editor - let editorViewController = EditPageViewController(page: newPage) - present(editorViewController, animated: false) - } - - fileprivate func copyLink(_ page: Page) { - let pasteboard = UIPasteboard.general - guard let link = page.permaLink else { return } - pasteboard.string = link as String - let noticeTitle = NSLocalizedString("Link Copied to Clipboard", comment: "Link copied to clipboard notice title") - let notice = Notice(title: noticeTitle, feedbackType: .success) - ActionDispatcher.dispatch(NoticeAction.dismiss) // Dismiss any old notices - ActionDispatcher.dispatch(NoticeAction.post(notice)) - } - - fileprivate func retryPage(_ apost: AbstractPost) { - PostCoordinator.shared.save(apost) - } - - fileprivate func draftPage(_ apost: AbstractPost, at indexPath: IndexPath?) { - WPAnalytics.track(.postListDraftAction, withProperties: propertiesForAnalytics()) - - let previousStatus = apost.status - apost.status = .draft - - let contextManager = ContextManager.sharedInstance() - let postService = PostService(managedObjectContext: contextManager.mainContext) - - postService.uploadPost(apost, success: { [weak self] _ in - DispatchQueue.main.async { - self?._tableViewHandler.refreshTableView(at: indexPath) - } - }) { [weak self] (error) in - apost.status = previousStatus - - if let strongSelf = self { - contextManager.save(strongSelf.managedObjectContext()) - } - - WPError.showXMLRPCErrorAlert(error) - } - } - - override func promptThatPostRestoredToFilter(_ filter: PostListFilter) { - var message = NSLocalizedString("Page Restored to Drafts", comment: "Prompts the user that a restored page was moved to the drafts list.") - - switch filter.filterType { - case .published: - message = NSLocalizedString("Page Restored to Published", comment: "Prompts the user that a restored page was moved to the published list.") - break - case .scheduled: - message = NSLocalizedString("Page Restored to Scheduled", comment: "Prompts the user that a restored page was moved to the scheduled list.") - break - default: - break - } - - let alertCancel = NSLocalizedString("OK", comment: "Title of an OK button. Pressing the button acknowledges and dismisses a prompt.") - - let alertController = UIAlertController(title: nil, message: message, preferredStyle: .alert) - alertController.addCancelActionWithTitle(alertCancel, handler: nil) - alertController.presentFromRootViewController() - } - // MARK: - Cell Action Handling - fileprivate func handleMenuAction(fromCell cell: UITableViewCell, fromButton button: UIButton, forPage page: AbstractPost) { - let objectID = page.objectID - - let retryButtonTitle = NSLocalizedString("Retry", comment: "Label for a button that attempts to re-upload a page that previously failed to upload.") - let viewButtonTitle = NSLocalizedString("View", comment: "Label for a button that opens the page when tapped.") - let draftButtonTitle = NSLocalizedString("Move to Draft", comment: "Label for a button that moves a page to the draft folder") - let publishButtonTitle = NSLocalizedString("Publish Immediately", comment: "Label for a button that moves a page to the published folder, publishing with the current date/time.") - let trashButtonTitle = NSLocalizedString("Move to Trash", comment: "Label for a button that moves a page to the trash folder") - let cancelButtonTitle = NSLocalizedString("Cancel", comment: "Label for a cancel button") - let deleteButtonTitle = NSLocalizedString("Delete Permanently", comment: "Label for a button permanently deletes a page.") - - let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) - alertController.addCancelActionWithTitle(cancelButtonTitle, handler: nil) - - let indexPath = tableView.indexPath(for: cell) - - let filter = filterSettings.currentPostListFilter().filterType - let isHomepage = ((page as? Page)?.isSiteHomepage ?? false) - if filter == .trashed { - alertController.addActionWithTitle(draftButtonTitle, style: .default, handler: { [weak self] (action) in - guard let strongSelf = self, - let page = strongSelf.pageForObjectID(objectID) else { - return - } - - strongSelf.draftPage(page, at: indexPath) - }) - - alertController.addActionWithTitle(deleteButtonTitle, style: .destructive, handler: { [weak self] (action) in - guard let strongSelf = self, - let page = strongSelf.pageForObjectID(objectID) else { - return - } - - strongSelf.handleTrashPage(page) - }) - } else if filter == .published { - if page.isFailed { - alertController.addActionWithTitle(retryButtonTitle, style: .default, handler: { [weak self] (action) in - guard let strongSelf = self, - let page = strongSelf.pageForObjectID(objectID) else { - return - } - - strongSelf.retryPage(page) - }) - } else { - addEditAction(to: alertController, for: page) - - alertController.addActionWithTitle(viewButtonTitle, style: .default, handler: { [weak self] (action) in - guard let strongSelf = self, - let page = strongSelf.pageForObjectID(objectID) else { - return - } - - strongSelf.viewPost(page) - }) - - addBlazeAction(to: alertController, for: page) - addSetParentAction(to: alertController, for: page, at: indexPath) - addSetHomepageAction(to: alertController, for: page, at: indexPath) - addSetPostsPageAction(to: alertController, for: page, at: indexPath) - addDuplicateAction(to: alertController, for: page) - - if !isHomepage { - alertController.addActionWithTitle(draftButtonTitle, style: .default, handler: { [weak self] (action) in - guard let strongSelf = self, - let page = strongSelf.pageForObjectID(objectID) else { - return - } - - strongSelf.draftPage(page, at: indexPath) - }) - } - } - - addCopyLinkAction(to: alertController, for: page) - - if !isHomepage { - alertController.addActionWithTitle(trashButtonTitle, style: .destructive, handler: { [weak self] (action) in - guard let strongSelf = self, - let page = strongSelf.pageForObjectID(objectID) else { - return - } - - strongSelf.handleTrashPage(page) - }) - } - } else { - if page.isFailed { - alertController.addActionWithTitle(retryButtonTitle, style: .default, handler: { [weak self] (action) in - guard let strongSelf = self, - let page = strongSelf.pageForObjectID(objectID) else { - return - } - - strongSelf.retryPage(page) - }) - } else { - addEditAction(to: alertController, for: page) - - alertController.addActionWithTitle(viewButtonTitle, style: .default, handler: { [weak self] (action) in - guard let strongSelf = self, - let page = strongSelf.pageForObjectID(objectID) else { - return - } - - strongSelf.viewPost(page) - }) - - addSetParentAction(to: alertController, for: page, at: indexPath) - addDuplicateAction(to: alertController, for: page) - - alertController.addActionWithTitle(publishButtonTitle, style: .default, handler: { [weak self] (action) in - guard let strongSelf = self, - let page = strongSelf.pageForObjectID(objectID) else { - return - } - - strongSelf.publishPost(page) - }) - } - - addCopyLinkAction(to: alertController, for: page) - - alertController.addActionWithTitle(trashButtonTitle, style: .destructive, handler: { [weak self] (action) in - guard let strongSelf = self, - let page = strongSelf.pageForObjectID(objectID) else { - return - } - - strongSelf.handleTrashPage(page) - }) - } - - WPAnalytics.track(.postListOpenedCellMenu, withProperties: propertiesForAnalytics()) - - alertController.modalPresentationStyle = .popover - present(alertController, animated: true) - - if let presentationController = alertController.popoverPresentationController { - presentationController.permittedArrowDirections = .any - presentationController.sourceView = button - presentationController.sourceRect = button.bounds - } - } - - override func deletePost(_ apost: AbstractPost) { - super.deletePost(apost) - } - - private func addBlazeAction(to controller: UIAlertController, for page: AbstractPost) { - guard BlazeHelper.isBlazeFlagEnabled() && page.canBlaze else { - return - } - - let buttonTitle = NSLocalizedString("pages.blaze.actionTitle", value: "Promote with Blaze", comment: "Promote the page with Blaze.") - controller.addActionWithTitle(buttonTitle, style: .default, handler: { [weak self] _ in - self?.blazePage(page) - }) - - BlazeEventsTracker.trackEntryPointDisplayed(for: .pagesList) - } - - private func addEditAction(to controller: UIAlertController, for page: AbstractPost) { - guard let page = page as? Page else { return } - - if page.status == .trash || page.isSitePostsPage { - return - } - - let buttonTitle = NSLocalizedString("Edit", comment: "Label for a button that opens the Edit Page view controller") - controller.addActionWithTitle(buttonTitle, style: .default, handler: { [weak self] _ in - if let page = self?.pageForObjectID(page.objectID) { - self?.editPage(page) - } - }) - } - - private func addDuplicateAction(to controller: UIAlertController, for page: AbstractPost) { - if page.status != .publish && page.status != .draft { - return - } - - let buttonTitle = NSLocalizedString("Duplicate", comment: "Label for page duplicate option. Tapping creates a copy of the page.") - controller.addActionWithTitle(buttonTitle, style: .default, handler: { [weak self] _ in - if let page = self?.pageForObjectID(page.objectID) { - self?.copyPage(page) - } - }) - } - - private func addCopyLinkAction(to controller: UIAlertController, for page: AbstractPost) { - let buttonTitle = NSLocalizedString("Copy Link", comment: "Label for page copy link. Tapping copy the url of page") - controller.addActionWithTitle(buttonTitle, style: .default) { [weak self] _ in - if let page = self?.pageForObjectID(page.objectID) { - self?.copyLink(page) - } - } - } - - private func addSetParentAction(to controller: UIAlertController, for page: AbstractPost, at index: IndexPath?) { - /// This button is disabled for trashed pages - // - if page.status == .trash { - return - } - - let objectID = page.objectID - let setParentButtonTitle = NSLocalizedString("Set Parent", comment: "Label for a button that opens the Set Parent options view controller") - controller.addActionWithTitle(setParentButtonTitle, style: .default, handler: { [weak self] _ in - if let page = self?.pageForObjectID(objectID) { - self?.setParent(for: page, at: index) + @MainActor + func setParentPage(for page: Page) async { + let request = NSFetchRequest(entityName: Page.entityName()) + let filter = PostListFilter.publishedFilter() + request.predicate = filter.predicate(for: blog, author: .everyone) + request.sortDescriptors = filter.sortDescriptors + do { + var pages = try await buildPageTree(request: request).hierarchyList(in: ContextManager.shared.mainContext) + if let index = pages.firstIndex(of: page) { + pages = pages.remove(from: index) } - }) - } - - private func setParent(for page: Page, at index: IndexPath?) { - guard let index = index else { - return + let viewController = ParentPageSettingsViewController.navigationController(with: pages, selectedPage: page, onClose: { [weak self] in + self?.updateAndPerformFetchRequestRefreshingResults() + }, onSuccess: { [weak self] in + self?.handleSetParentSuccess() + } ) + present(viewController, animated: true) + } catch { + assertionFailure("Failed to fetch pages: \(error)") // This should never happen } - - let selectedPage = pageAtIndexPath(index) - let newIndex = _tableViewHandler.index(for: selectedPage) - let pages = _tableViewHandler.removePage(from: newIndex) - let parentPageNavigationController = ParentPageSettingsViewController.navigationController(with: pages, selectedPage: selectedPage, onClose: { [weak self] in - self?._tableViewHandler.isSearching = false - self?._tableViewHandler.refreshTableView(at: index) - }, onSuccess: { [weak self] in - self?.handleSetParentSuccess() - } ) - present(parentPageNavigationController, animated: true) } private func handleSetParentSuccess() { @@ -908,94 +483,28 @@ class PageListViewController: AbstractPostListViewController, UIViewControllerRe ActionDispatcher.global.dispatch(NoticeAction.post(notice)) } - fileprivate func pageForObjectID(_ objectID: NSManagedObjectID) -> Page? { - - var pageManagedOjbect: NSManagedObject - - do { - pageManagedOjbect = try managedObjectContext().existingObject(with: objectID) - - } catch let error as NSError { - DDLogError("\(NSStringFromClass(type(of: self))), \(#function), \(error)") - return nil - } catch _ { - DDLogError("\(NSStringFromClass(type(of: self))), \(#function), Could not find Page with ID \(objectID)") - return nil - } - - let page = pageManagedOjbect as? Page - return page - } - - fileprivate func handleRestoreAction(fromCell cell: UITableViewCell, forPage page: AbstractPost) { - restorePost(page) { [weak self] in - self?._tableViewHandler.refreshTableView(at: self?.tableView.indexPath(for: cell)) - } - } - - private func addSetHomepageAction(to controller: UIAlertController, for page: AbstractPost, at index: IndexPath?) { - let objectID = page.objectID - - /// This button is enabled if - /// - Page is not trashed - /// - The site's homepage type is .page - /// - The page isn't currently the homepage - // - guard page.status != .trash, - let homepageType = blog.homepageType, - homepageType == .page, - let page = pageForObjectID(objectID), - page.isSiteHomepage == false else { - return - } - - let setHomepageButtonTitle = NSLocalizedString("Set as Homepage", comment: "Label for a button that sets the selected page as the site's Homepage") - controller.addActionWithTitle(setHomepageButtonTitle, style: .default, handler: { [weak self] _ in - if let pageID = page.postID?.intValue { - self?.beginRefreshingManually() - WPAnalytics.track(.postListSetHomePageAction) - self?.homepageSettingsService?.setHomepageType(.page, - homePageID: pageID, success: { - self?.refreshAndReload() - self?.handleHomepageSettingsSuccess() - }, failure: { error in - self?.refreshControl?.endRefreshing() - self?.handleHomepageSettingsFailure() - }) - } + func setPageAsHomepage(_ page: Page) { + guard let homePageID = page.postID?.intValue else { return } + beginRefreshingManually() + homepageSettingsService?.setHomepageType(.page, homePageID: homePageID, success: { [weak self] in + self?.refreshAndReload() + self?.handleHomepageSettingsSuccess() + }, failure: { [weak self] error in + self?.refreshControl.endRefreshing() + self?.handleHomepageSettingsFailure() }) } - private func addSetPostsPageAction(to controller: UIAlertController, for page: AbstractPost, at index: IndexPath?) { - let objectID = page.objectID - - /// This button is enabled if - /// - Page is not trashed - /// - The site's homepage type is .page - /// - The page isn't currently the posts page - // - guard page.status != .trash, - let homepageType = blog.homepageType, - homepageType == .page, - let page = pageForObjectID(objectID), - page.isSitePostsPage == false else { - return - } - - let setPostsPageButtonTitle = NSLocalizedString("Set as Posts Page", comment: "Label for a button that sets the selected page as the site's Posts page") - controller.addActionWithTitle(setPostsPageButtonTitle, style: .default, handler: { [weak self] _ in - if let pageID = page.postID?.intValue { - self?.beginRefreshingManually() - WPAnalytics.track(.postListSetAsPostsPageAction) - self?.homepageSettingsService?.setHomepageType(.page, - withPostsPageID: pageID, success: { - self?.refreshAndReload() - self?.handleHomepagePostsPageSettingsSuccess() - }, failure: { error in - self?.refreshControl?.endRefreshing() - self?.handleHomepageSettingsFailure() - }) - } + func togglePageAsPostsPage(_ page: Page) { + let newValue = !page.isSitePostsPage + let postsPageID = page.isSitePostsPage ? 0 : (page.postID?.intValue ?? 0) + beginRefreshingManually() + homepageSettingsService?.setHomepageType(.page, withPostsPageID: postsPageID, success: { [weak self] in + self?.refreshAndReload() + self?.handleHomepagePostsPageSettingsSuccess(isPostsPage: newValue) + }, failure: { [weak self] error in + self?.refreshControl.endRefreshing() + self?.handleHomepageSettingsFailure() }) } @@ -1004,8 +513,9 @@ class PageListViewController: AbstractPostListViewController, UIViewControllerRe ActionDispatcher.global.dispatch(NoticeAction.post(notice)) } - private func handleHomepagePostsPageSettingsSuccess() { - let notice = Notice(title: HomepageSettingsText.updatePostsPageSuccessTitle, feedbackType: .success) + private func handleHomepagePostsPageSettingsSuccess(isPostsPage: Bool) { + let title = isPostsPage ? HomepageSettingsText.updatePostsPageSuccessTitle : HomepageSettingsText.updatePageSuccessTitle + let notice = Notice(title: title, feedbackType: .success) ActionDispatcher.global.dispatch(NoticeAction.post(notice)) } @@ -1014,73 +524,6 @@ class PageListViewController: AbstractPostListViewController, UIViewControllerRe ActionDispatcher.global.dispatch(NoticeAction.post(notice)) } - private func handleTrashPage(_ post: AbstractPost) { - guard ReachabilityUtils.isInternetReachable() else { - let offlineMessage = NSLocalizedString("Unable to trash pages while offline. Please try again later.", comment: "Message that appears when a user tries to trash a page while their device is offline.") - ReachabilityUtils.showNoInternetConnectionNotice(message: offlineMessage) - return - } - - let cancelText = NSLocalizedString("Cancel", comment: "Cancels an Action") - let deleteText: String - let messageText: String - let titleText: String - - if post.status == .trash { - deleteText = NSLocalizedString("Delete Permanently", comment: "Delete option in the confirmation alert when deleting a page from the trash.") - titleText = NSLocalizedString("Delete Permanently?", comment: "Title of the confirmation alert when deleting a page from the trash.") - messageText = NSLocalizedString("Are you sure you want to permanently delete this page?", comment: "Message of the confirmation alert when deleting a page from the trash.") - } else { - deleteText = NSLocalizedString("Move to Trash", comment: "Trash option in the trash page confirmation alert.") - titleText = NSLocalizedString("Trash this page?", comment: "Title of the trash page confirmation alert.") - messageText = NSLocalizedString("Are you sure you want to trash this page?", comment: "Message of the trash page confirmation alert.") - } - - let alertController = UIAlertController(title: titleText, message: messageText, preferredStyle: .alert) - - alertController.addCancelActionWithTitle(cancelText) - alertController.addDestructiveActionWithTitle(deleteText) { [weak self] action in - self?.deletePost(post) - } - alertController.presentFromRootViewController() - } - - // MARK: - UISearchControllerDelegate - - override func willPresentSearchController(_ searchController: UISearchController) { - super.willPresentSearchController(searchController) - - filterTabBar.alpha = WPAlphaZero - - tableView.contentInset.top = -searchController.searchBar.bounds.height - } - - override func updateSearchResults(for searchController: UISearchController) { - super.updateSearchResults(for: searchController) - } - - override func willDismissSearchController(_ searchController: UISearchController) { - _tableViewHandler.isSearching = false - _tableViewHandler.refreshTableView() - super.willDismissSearchController(searchController) - } - - func didPresentSearchController(_ searchController: UISearchController) { - tableView.verticalScrollIndicatorInsets.top = searchController.searchBar.bounds.height + searchController.searchBar.frame.origin.y - view.safeAreaInsets.top - } - - func didDismissSearchController(_ searchController: UISearchController) { - UIView.animate(withDuration: Animations.searchDismissDuration, delay: 0, options: .curveLinear, animations: { - self.filterTabBar.alpha = WPAlphaFull - }) { _ in - self.hideNoResultsView() - } - } - - enum Animations { - static let searchDismissDuration: TimeInterval = 0.3 - } - // MARK: - NetworkAwareUI override func noConnectionMessage() -> String { @@ -1091,6 +534,7 @@ class PageListViewController: AbstractPostListViewController, UIViewControllerRe struct HomepageSettingsText { static let updateErrorTitle = NSLocalizedString("Unable to update homepage settings", comment: "Error informing the user that their homepage settings could not be updated") static let updateErrorMessage = NSLocalizedString("Please try again later.", comment: "Prompt for the user to retry a failed action again later") + static let updatePageSuccessTitle = NSLocalizedString("pages.updatePage.successTitle", value: "Page successfully updated", comment: "Message informing the user that their static homepage page was set successfully") static let updateHomepageSuccessTitle = NSLocalizedString("Homepage successfully updated", comment: "Message informing the user that their static homepage page was set successfully") static let updatePostsPageSuccessTitle = NSLocalizedString("Posts page successfully updated", comment: "Message informing the user that their static homepage for posts was set successfully") } @@ -1107,20 +551,12 @@ private extension PageListViewController { return } - if searchController.isActive { - if currentSearchTerm()?.count == 0 { - noResultsViewController.configureForNoSearchResults(title: NoResultsText.searchPages) - } else { - noResultsViewController.configureForNoSearchResults(title: noResultsTitle()) - } - } else { - let accessoryView = syncHelper.isSyncing ? NoResultsViewController.loadingAccessoryView() : nil + let accessoryView = syncHelper.isSyncing ? NoResultsViewController.loadingAccessoryView() : nil - noResultsViewController.configure(title: noResultsTitle(), - buttonTitle: noResultsButtonTitle(), - image: noResultsImageName, - accessoryView: accessoryView) - } + noResultsViewController.configure(title: noResultsTitle(), + buttonTitle: noResultsButtonTitle(), + image: noResultsImageName, + accessoryView: accessoryView) } var noResultsImageName: String { @@ -1128,7 +564,7 @@ private extension PageListViewController { } func noResultsButtonTitle() -> String? { - if syncHelper.isSyncing == true || isSearching() { + if syncHelper.isSyncing == true { return nil } @@ -1140,11 +576,6 @@ private extension PageListViewController { if syncHelper.isSyncing == true { return NoResultsText.fetchingTitle } - - if isSearching() { - return NoResultsText.noMatchesTitle - } - return noResultsFilteredTitle() } @@ -1167,14 +598,11 @@ private extension PageListViewController { struct NoResultsText { static let buttonTitle = NSLocalizedString("Create Page", comment: "Button title, encourages users to create their first page on their blog.") static let fetchingTitle = NSLocalizedString("Fetching pages...", comment: "A brief prompt shown when the reader is empty, letting the user know the app is currently fetching new pages.") - static let noMatchesTitle = NSLocalizedString("No pages matching your search", comment: "Displayed when the user is searching the pages list and there are no matching pages") static let noDraftsTitle = NSLocalizedString("You don't have any draft pages", comment: "Displayed when the user views drafts in the pages list and there are no pages") static let noScheduledTitle = NSLocalizedString("You don't have any scheduled pages", comment: "Displayed when the user views scheduled pages in the pages list and there are no pages") static let noTrashedTitle = NSLocalizedString("You don't have any trashed pages", comment: "Displayed when the user views trashed in the pages list and there are no pages") static let noPublishedTitle = NSLocalizedString("You haven't published any pages yet", comment: "Displayed when the user views published pages in the pages list and there are no pages") - static let searchPages = NSLocalizedString("Search pages", comment: "Text displayed when the search controller will be presented") static let noConnectionTitle: String = NSLocalizedString("Unable to load pages right now.", comment: "Title for No results full page screen displayedfrom pages list when there is no connection") static let noConnectionSubtitle: String = NSLocalizedString("Check your network connection and try again. Or draft a page.", comment: "Subtitle for No results full page screen displayed from pages list when there is no connection") } - } diff --git a/WordPress/Classes/ViewRelated/Pages/PageMenuViewModelTests.swift b/WordPress/Classes/ViewRelated/Pages/PageMenuViewModelTests.swift new file mode 100644 index 000000000000..9f666889c8b5 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Pages/PageMenuViewModelTests.swift @@ -0,0 +1,200 @@ +import Nimble +import XCTest +@testable import WordPress + +final class PageMenuViewModelTests: CoreDataTestCase { + + func testPublishedPageButtonsWithBlazeEnabled() { + // Given + let page = PageBuilder(mainContext, canBlaze: true) + .withRemote() + .with(status: .publish) + .build() + let viewModel = PageMenuViewModel( + page: page, + isSiteHomepage: false, + isSitePostsPage: false, + isJetpackFeaturesEnabled: true, + isBlazeFlagEnabled: true + ) + + // When & Then + let buttons = viewModel.buttonSections + .filter { !$0.buttons.isEmpty } + .map { $0.buttons } + let expectedButtons: [[AbstractPostButton]] = [ + [.view], + [.moveToDraft, .duplicate], + [.blaze], + [.setParent, .setHomepage, .setPostsPage, .settings], + [.trash] + ] + expect(buttons).to(equal(expectedButtons)) + } + + func testPublishedPageButtonsWithBlazeDisabled() { + // Given + let page = PageBuilder(mainContext, canBlaze: false) + .withRemote() + .with(status: .publish) + .build() + let viewModel = PageMenuViewModel( + page: page, + isSiteHomepage: false, + isSitePostsPage: false, + isJetpackFeaturesEnabled: true, + isBlazeFlagEnabled: false + ) + + // When & Then + let buttons = viewModel.buttonSections + .filter { !$0.buttons.isEmpty } + .map { $0.buttons } + let expectedButtons: [[AbstractPostButton]] = [ + [.view], + [.moveToDraft, .duplicate], + [.setParent, .setHomepage, .setPostsPage, .settings], + [.trash] + ] + expect(buttons).to(equal(expectedButtons)) + } + + func testPublishedPageButtonsWithJetpackFeaturesDisabled() { + // Given + let page = PageBuilder(mainContext) + .withRemote() + .with(status: .publish) + .build() + let viewModel = PageMenuViewModel( + page: page, + isSiteHomepage: false, + isSitePostsPage: false, + isJetpackFeaturesEnabled: false, + isBlazeFlagEnabled: false + ) + + // When & Then + let buttons = viewModel.buttonSections + .filter { !$0.buttons.isEmpty } + .map { $0.buttons } + let expectedButtons: [[AbstractPostButton]] = [ + [.view], + [.moveToDraft, .duplicate], + [.setParent, .setHomepage, .setPostsPage, .settings], + [.trash] + ] + expect(buttons).to(equal(expectedButtons)) + } + + func testPublishedPageButtonsForHomepage() { + // Given + let page = PageBuilder(mainContext, canBlaze: true) + .withRemote() + .with(status: .publish) + .build() + let viewModel = PageMenuViewModel( + page: page, + isSiteHomepage: true, + isSitePostsPage: false, + isJetpackFeaturesEnabled: true, + isBlazeFlagEnabled: true + ) + + // When & Then + let buttons = viewModel.buttonSections + .filter { !$0.buttons.isEmpty } + .map { $0.buttons } + let expectedButtons: [[AbstractPostButton]] = [ + [.view], + [.duplicate], + [.blaze], + [.setParent, .setPostsPage, .settings] + ] + expect(buttons).to(equal(expectedButtons)) + } + + func testPublishedPageButtonsForPostsPage() { + // Given + let page = PageBuilder(mainContext, canBlaze: true) + .withRemote() + .with(status: .publish) + .build() + let viewModel = PageMenuViewModel( + page: page, + isSiteHomepage: false, + isSitePostsPage: true, + isJetpackFeaturesEnabled: true, + isBlazeFlagEnabled: true + ) + + // When & Then + let buttons = viewModel.buttonSections + .filter { !$0.buttons.isEmpty } + .map { $0.buttons } + let expectedButtons: [[AbstractPostButton]] = [ + [.view], + [.moveToDraft, .duplicate], + [.blaze], + [.setParent, .setHomepage, .setRegularPage, .settings], + [.trash] + ] + expect(buttons).to(equal(expectedButtons)) + } + + func testDraftPageButtons() { + // Given + let page = PageBuilder(mainContext) + .with(status: .draft) + .build() + let viewModel = PageMenuViewModel(page: page) + + // When & Then + let buttons = viewModel.buttonSections + .filter { !$0.buttons.isEmpty } + .map { $0.buttons } + let expectedButtons: [[AbstractPostButton]] = [ + [.view], + [.duplicate, .publish], + [.setParent], + [.trash] + ] + expect(buttons).to(equal(expectedButtons)) + } + + func testScheduledPostButtons() { + // Given + let page = PageBuilder(mainContext) + .with(status: .scheduled) + .build() + let viewModel = PageMenuViewModel(page: page) + + // When & Then + let buttons = viewModel.buttonSections + .filter { !$0.buttons.isEmpty } + .map { $0.buttons } + let expectedButtons: [[AbstractPostButton]] = [ + [.view], + [.moveToDraft, .publish], + [.setParent], + [.trash] + ] + expect(buttons).to(equal(expectedButtons)) + } + + func testTrashedPageButtons() { + // Given + let page = PageBuilder(mainContext) + .with(status: .trash) + .build() + let viewModel = PageMenuViewModel(page: page) + // When & Then + let buttons = viewModel.buttonSections + .filter { !$0.buttons.isEmpty } + .map { $0.buttons } + let expectedButtons: [[AbstractPostButton]] = [ + [.moveToDraft], + [.trash] + ] + expect(buttons).to(equal(expectedButtons)) + } +} diff --git a/WordPress/Classes/ViewRelated/Pages/Pages.storyboard b/WordPress/Classes/ViewRelated/Pages/Pages.storyboard index 95f8b6d5052f..4c0fddd24d2a 100644 --- a/WordPress/Classes/ViewRelated/Pages/Pages.storyboard +++ b/WordPress/Classes/ViewRelated/Pages/Pages.storyboard @@ -1,101 +1,12 @@ - - - - + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -126,7 +37,7 @@ - + @@ -168,4 +79,9 @@ + + + + + diff --git a/WordPress/Classes/ViewRelated/Pages/RestorePageTableViewCell.h b/WordPress/Classes/ViewRelated/Pages/RestorePageTableViewCell.h deleted file mode 100644 index 1710b0acb21e..000000000000 --- a/WordPress/Classes/ViewRelated/Pages/RestorePageTableViewCell.h +++ /dev/null @@ -1,6 +0,0 @@ -#import -#import "BasePageListCell.h" - -@interface RestorePageTableViewCell : BasePageListCell - -@end diff --git a/WordPress/Classes/ViewRelated/Pages/RestorePageTableViewCell.m b/WordPress/Classes/ViewRelated/Pages/RestorePageTableViewCell.m deleted file mode 100644 index c05e0d4ce98c..000000000000 --- a/WordPress/Classes/ViewRelated/Pages/RestorePageTableViewCell.m +++ /dev/null @@ -1,42 +0,0 @@ -#import "RestorePageTableViewCell.h" -#import "WPStyleGuide+Pages.h" - -@import Gridicons; - -@interface RestorePageTableViewCell() - -@property (nonatomic, strong) IBOutlet UILabel *restoreLabel; -@property (nonatomic, strong) IBOutlet UIButton *restoreButton; - -@end - -@implementation RestorePageTableViewCell - -#pragma mark - Life Cycle - -- (void)awakeFromNib { - [super awakeFromNib]; - - [self configureView]; - [self applyStyles]; -} - -#pragma mark - Configuration - -- (void)applyStyles -{ - [WPStyleGuide applyRestorePageLabelStyle:self.restoreLabel]; - [WPStyleGuide applyRestorePageButtonStyle:self.restoreButton]; -} - -- (void)configureView -{ - self.restoreLabel.text = NSLocalizedString(@"Page moved to trash.", @"A short message explaining that a page was moved to the trash bin."); - NSString *buttonTitle = NSLocalizedString(@"Undo", @"The title of an 'undo' button. Tapping the button moves a trashed page out of the trash folder."); - [self.restoreButton setTitle:buttonTitle forState:UIControlStateNormal]; - [self.restoreButton setImage:[UIImage gridiconOfType:GridiconTypeUndo - withSize:CGSizeMake(18.0, 18.0)] - forState:UIControlStateNormal]; -} - -@end diff --git a/WordPress/Classes/ViewRelated/Pages/RestorePageTableViewCell.xib b/WordPress/Classes/ViewRelated/Pages/RestorePageTableViewCell.xib deleted file mode 100644 index 2904556a70cb..000000000000 --- a/WordPress/Classes/ViewRelated/Pages/RestorePageTableViewCell.xib +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/WordPress/Classes/ViewRelated/Pages/TemplatePageTableViewCell.swift b/WordPress/Classes/ViewRelated/Pages/TemplatePageTableViewCell.swift index 62cbac0ff17f..da6a05ec790a 100644 --- a/WordPress/Classes/ViewRelated/Pages/TemplatePageTableViewCell.swift +++ b/WordPress/Classes/ViewRelated/Pages/TemplatePageTableViewCell.swift @@ -19,9 +19,8 @@ class TemplatePageTableViewCell: UITableViewCell { private func applyStyles() { backgroundColor = .neutral(.shade5) - separatorInset = .zero + separatorInset = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 0) } - } private struct TemplatePageView: View { @@ -30,18 +29,18 @@ private struct TemplatePageView: View { text icon } - .padding(EdgeInsets(top: 4.0, leading: 16.0, bottom: 8.0, trailing: 16.0)) + .padding(EdgeInsets(top: 12.0, leading: 16.0, bottom: 12.0, trailing: 16.0)) .background(Color(UIColor.listForeground)) } private var text: some View { VStack(alignment: .leading, spacing: 2.0) { Text(Constants.title) - .font(Font(WPStyleGuide.notoBoldFontForTextStyle(.headline))) - .foregroundColor(Color(UIColor.text)) + .font(Font(WPStyleGuide.fontForTextStyle(.callout, fontWeight: .semibold))) + .foregroundColor(Color(UIColor.label)) Text(Constants.subtitle) - .font(.subheadline) - .foregroundColor(Color(UIColor.textSubtle)) + .font(.footnote) + .foregroundColor(Color(UIColor.secondaryLabel)) } .frame(maxWidth: .infinity, alignment: .leading) } diff --git a/WordPress/Classes/ViewRelated/People/PersonViewController.swift b/WordPress/Classes/ViewRelated/People/PersonViewController.swift index b8a4ad68436a..16a7a73c274a 100644 --- a/WordPress/Classes/ViewRelated/People/PersonViewController.swift +++ b/WordPress/Classes/ViewRelated/People/PersonViewController.swift @@ -165,7 +165,6 @@ final class PersonViewController: UITableViewController { // MARK: - Constants private let sectionHeaderHeight = CGFloat(20) - private let gravatarPlaceholderImage = UIImage(named: "gravatar.png") private let roleSegueIdentifier = "editRole" private let userInfoCellIdentifier = "userInfoCellIdentifier" private let actionCellIdentifier = "actionCellIdentifier" diff --git a/WordPress/Classes/ViewRelated/Plans/PlanDetailViewController.swift b/WordPress/Classes/ViewRelated/Plans/PlanDetailViewController.swift index ed90c521b07c..394fd7b739ea 100644 --- a/WordPress/Classes/ViewRelated/Plans/PlanDetailViewController.swift +++ b/WordPress/Classes/ViewRelated/Plans/PlanDetailViewController.swift @@ -3,9 +3,6 @@ import CocoaLumberjack import WordPressShared class PlanDetailViewController: UIViewController { - fileprivate let cellIdentifier = "PlanFeatureListItem" - - fileprivate let tableViewHorizontalMargin: CGFloat = 24.0 fileprivate let planImageDropshadowRadius: CGFloat = 3.0 private var noResultsViewController: NoResultsViewController? diff --git a/WordPress/Classes/ViewRelated/Plugins/PluginViewController.swift b/WordPress/Classes/ViewRelated/Plugins/PluginViewController.swift index d77aa47c9d62..d10ce10f00b8 100644 --- a/WordPress/Classes/ViewRelated/Plugins/PluginViewController.swift +++ b/WordPress/Classes/ViewRelated/Plugins/PluginViewController.swift @@ -153,10 +153,6 @@ private extension PluginViewController { } } - private func addChildController(_ controller: UIViewController) { - - } - private func getNoResultsViewController() -> NoResultsViewController { if let noResultsViewController = self.noResultsViewController { return noResultsViewController diff --git a/WordPress/Classes/ViewRelated/Plugins/PluginViewModel.swift b/WordPress/Classes/ViewRelated/Plugins/PluginViewModel.swift index db15ba3332b9..a6c9bf66398c 100644 --- a/WordPress/Classes/ViewRelated/Plugins/PluginViewModel.swift +++ b/WordPress/Classes/ViewRelated/Plugins/PluginViewModel.swift @@ -540,7 +540,7 @@ class PluginViewModel: Observable { return } - let controller = RegisterDomainSuggestionsViewController.instance(site: blog, domainPurchasedCallback: { [weak self] _, domain in + let coordinator = RegisterDomainCoordinator(site: blog, domainPurchasedCallback: { [weak self] _, domain in guard let strongSelf = self, let atHelper = AutomatedTransferHelper(site: strongSelf.site, plugin: directoryEntry) else { @@ -551,6 +551,11 @@ class PluginViewModel: Observable { atHelper.startAutomatedTransferProcess(retryingAfterFailure: true) }) + let controller = DomainSelectionViewController( + service: DomainsServiceAdapter(coreDataStack: ContextManager.shared), + domainSelectionType: .registerWithPaidPlan, + coordinator: coordinator + ) let navigationController = UINavigationController(rootViewController: controller) self.present?(navigationController) } diff --git a/WordPress/Classes/ViewRelated/Post/AbstractPostHelper+Actions.swift b/WordPress/Classes/ViewRelated/Post/AbstractPostHelper+Actions.swift new file mode 100644 index 000000000000..1c81578acf91 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Post/AbstractPostHelper+Actions.swift @@ -0,0 +1,54 @@ +import UIKit + +extension AbstractPostHelper { + // MARK: - Swipe Actions + + static func makeLeadingContextualActions(for post: AbstractPost, delegate: InteractivePostViewDelegate) -> [UIContextualAction] { + var actions: [UIContextualAction] = [] + + if post.status != .trash { + let viewAction = UIContextualAction(style: .normal, title: Strings.swipeActionView) { [weak delegate] _, _, completion in + delegate?.view(post) + completion(true) + } + viewAction.image = UIImage(systemName: "safari") + viewAction.backgroundColor = .systemBlue + actions.append(viewAction) + } + + return actions + } + + static func makeTrailingContextualActions(for post: AbstractPost, delegate: InteractivePostViewDelegate) -> [UIContextualAction] { + var actions: [UIContextualAction] = [] + + let trashAction = UIContextualAction( + style: .destructive, + title: post.status == .trash ? Strings.swipeActionDeletePermanently : Strings.swipeActionTrash + ) { [weak delegate] _, _, completion in + delegate?.trash(post) { + completion(true) + } + } + trashAction.image = UIImage(systemName: "trash") + actions.append(trashAction) + + if post is Post, post.status == .publish && post.hasRemote() { + let shareAction = UIContextualAction(style: .normal, title: Strings.swipeActionShare) { [weak delegate] _, view, completion in + delegate?.share(post, fromView: view) + completion(true) + } + shareAction.image = UIImage(systemName: "square.and.arrow.up") + actions.append(shareAction) + } + + return actions + } +} + +private enum Strings { + static let swipeActionView = NSLocalizedString("postList.swipeActionView", value: "View", comment: "Title for the 'View' post list row swipe action") + static let swipeActionShare = NSLocalizedString("postList.swipeActionShare", value: "Share", comment: "Title for the 'Share' post list row swipe action") + static let swipeActionTrash = NSLocalizedString("postList.swipeActionDelete", value: "Trash", comment: "Title for the 'Trash' post list row swipe action") + static let swipeActionDeletePermanently = NSLocalizedString("postList.swipeActionDeletePermanently", value: "Delete", comment: "Title for the 'Delete' post list row swipe action") +} diff --git a/WordPress/Classes/ViewRelated/Post/AbstractPostHelper.swift b/WordPress/Classes/ViewRelated/Post/AbstractPostHelper.swift new file mode 100644 index 000000000000..700ac24aa3a6 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Post/AbstractPostHelper.swift @@ -0,0 +1,55 @@ +import Foundation + +enum AbstractPostHelper { + static func getLocalizedStatusWithDate(for post: AbstractPost) -> String? { + let timeZone = post.blog.timeZone + + switch post.status { + case .scheduled: + if let dateCreated = post.dateCreated { + return String(format: Strings.scheduled, dateCreated.mediumStringWithTime(timeZone: timeZone)) + } + case .publish, .publishPrivate: + if let dateCreated = post.dateCreated { + return String(format: Strings.published, dateCreated.toMediumString(inTimeZone: timeZone)) + } + case .trash: + if let dateModified = post.dateModified { + return String(format: Strings.trashed, dateModified.toMediumString(inTimeZone: timeZone)) + } + default: + break + } + if let dateModified = post.dateModified { + return String(format: Strings.edited, dateModified.toMediumString(inTimeZone: timeZone)) + } + if let dateCreated = post.dateCreated { + return String(format: Strings.created, dateCreated.toMediumString(inTimeZone: timeZone)) + } + return nil + } + + static func makeBadgesString(with badges: [(String, UIColor?)]) -> NSAttributedString { + let string = NSMutableAttributedString() + for (badge, color) in badges { + if string.length > 0 { + string.append(NSAttributedString(string: " · ", attributes: [ + .foregroundColor: UIColor.secondaryLabel + ])) + } + string.append(NSAttributedString(string: badge, attributes: [ + .foregroundColor: color ?? UIColor.secondaryLabel + ])) + } + string.addAttribute(.font, value: WPStyleGuide.fontForTextStyle(.footnote), range: NSRange(location: 0, length: string.length)) + return string + } +} + +private enum Strings { + static let published = NSLocalizedString("post.publishedTimeAgo", value: "Published %@", comment: "Post status and date for list cells with %@ a placeholder for the date.") + static let scheduled = NSLocalizedString("post.scheduledForDate", value: "Scheduled %@", comment: "Post status and date for list cells with %@ a placeholder for the date.") + static let created = NSLocalizedString("post.createdTimeAgo", value: "Created %@", comment: "Post status and date for list cells with %@ a placeholder for the date.") + static let edited = NSLocalizedString("post.editedTimeAgo", value: "Edited %@", comment: "Post status and date for list cells with %@ a placeholder for the date.") + static let trashed = NSLocalizedString("post.trashedTimeAgo", value: "Trashed %@", comment: "Post status and date for list cells with %@ a placeholder for the date.") +} diff --git a/WordPress/Classes/ViewRelated/Post/AbstractPostListViewController.swift b/WordPress/Classes/ViewRelated/Post/AbstractPostListViewController.swift index c1892e233ff9..cab1cc536093 100644 --- a/WordPress/Classes/ViewRelated/Post/AbstractPostListViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/AbstractPostListViewController.swift @@ -1,53 +1,24 @@ import Foundation +import CoreData import Gridicons import CocoaLumberjack import WordPressShared import wpxmlrpc import WordPressFlux -// FIXME: comparison operators with optionals were removed from the Swift Standard Libary. -// Consider refactoring the code to use the non-optional operators. -fileprivate func < (lhs: T?, rhs: T?) -> Bool { - switch (lhs, rhs) { - case let (l?, r?): - return l < r - case (nil, _?): - return true - default: - return false - } -} - -// FIXME: comparison operators with optionals were removed from the Swift Standard Libary. -// Consider refactoring the code to use the non-optional operators. -fileprivate func > (lhs: T?, rhs: T?) -> Bool { - switch (lhs, rhs) { - case let (l?, r?): - return l > r - default: - return rhs < lhs - } -} - - class AbstractPostListViewController: UIViewController, - WPContentSyncHelperDelegate, - UISearchControllerDelegate, - UISearchResultsUpdating, - WPTableViewHandlerDelegate, - // This protocol is not in an extension so that subclasses can override noConnectionMessage() - NetworkAwareUI { - - fileprivate static let postsControllerRefreshInterval = TimeInterval(300) - fileprivate static let HTTPErrorCodeForbidden = Int(403) - fileprivate static let postsFetchRequestBatchSize = Int(10) - fileprivate static let pagesNumberOfLoadedElement = Int(100) - fileprivate static let postsLoadMoreThreshold = Int(4) - fileprivate static let preferredFiltersPopoverContentSize = CGSize(width: 320.0, height: 220.0) - - fileprivate static let defaultHeightForFooterView = CGFloat(44.0) - - fileprivate let abstractPostWindowlessCellIdenfitier = "AbstractPostWindowlessCellIdenfitier" + WPContentSyncHelperDelegate, + NSFetchedResultsControllerDelegate, + UITableViewDelegate, + UITableViewDataSource, + NetworkAwareUI // This protocol is not in an extension so that subclasses can override noConnectionMessage() +{ + typealias SyncPostResult = (posts: [AbstractPost], hasMore: Bool) + + private static let httpErrorCodeForbidden = 403 + private static let postsFetchRequestBatchSize = 10 + private static let pagesNumberOfLoadedElement = 100 + private static let postsLoadMoreThreshold = 4 private var fetchBatchSize: Int { return postTypeToSync() == .page ? 0 : type(of: self).postsFetchRequestBatchSize @@ -61,105 +32,82 @@ class AbstractPostListViewController: UIViewController, return postTypeToSync() == .page ? NSNumber(value: type(of: self).pagesNumberOfLoadedElement) : NSNumber(value: numberOfPostsPerSync()) } - private(set) var ghostableTableView = UITableView() - var ghostingEnabled = false - - @objc var blog: Blog! + var blog: Blog! /// This closure will be executed whenever the noResultsView must be visually refreshed. It's up /// to the subclass to define this property. /// - @objc var refreshNoResultsViewController: ((NoResultsViewController) -> ())! - @objc var tableViewController: UITableViewController! - @objc var reloadTableViewBeforeAppearing = false + var refreshNoResultsViewController: ((NoResultsViewController) -> ())! + private var reloadTableViewBeforeAppearing = false - @objc var tableView: UITableView { - get { - return self.tableViewController.tableView - } - } + let tableView = UITableView(frame: .zero, style: .plain) - @objc var refreshControl: UIRefreshControl? { - get { - return self.tableViewController.refreshControl + var shouldHideAuthor: Bool { + guard filterSettings.canFilterByAuthor() else { + return true } + return filterSettings.currentPostAuthorFilter() == .mine } - @objc lazy var tableViewHandler: WPTableViewHandler = { - let tableViewHandler = WPTableViewHandler(tableView: self.tableView) - - tableViewHandler.cacheRowHeights = false - tableViewHandler.delegate = self - tableViewHandler.updateRowAnimation = .none + private let buttonAuthorFilter = AuthorFilterButton() - return tableViewHandler - }() + let refreshControl = UIRefreshControl() - @objc lazy var estimatedHeightsCache: NSCache = { () -> NSCache in - let estimatedHeightsCache = NSCache() - return estimatedHeightsCache - }() + private(set) var fetchResultsController: NSFetchedResultsController! - @objc lazy var syncHelper: WPContentSyncHelper = { + lazy var syncHelper: WPContentSyncHelper = { let syncHelper = WPContentSyncHelper() - syncHelper.delegate = self - return syncHelper }() - @objc lazy var searchHelper: WPContentSearchHelper = { - let searchHelper = WPContentSearchHelper() - return searchHelper - }() - - @objc lazy var noResultsViewController: NoResultsViewController = { + lazy var noResultsViewController: NoResultsViewController = { let noResultsViewController = NoResultsViewController.controller() noResultsViewController.delegate = self - return noResultsViewController }() - @objc lazy var filterSettings: PostListFilterSettings = { + lazy var filterSettings: PostListFilterSettings = { return PostListFilterSettings(blog: self.blog, postType: self.postTypeToSync()) }() + let filterTabBar = FilterTabBar() - @objc var postListFooterView: PostListFooterView! - - @IBOutlet var filterTabBar: FilterTabBar! - - @objc var searchController: UISearchController! - @objc var recentlyTrashedPostObjectIDs = [NSManagedObjectID]() // IDs of trashed posts. Cleared on refresh or when filter changes. + private lazy var searchResultsViewController = PostSearchViewController(viewModel: PostSearchViewModel(blog: blog, filters: filterSettings)) - fileprivate var searchesSyncing = 0 + private lazy var searchController = UISearchController(searchResultsController: searchResultsViewController) private var emptyResults: Bool { - return tableViewHandler.resultsController?.fetchedObjects?.count == 0 + fetchResultsController?.fetchedObjects?.count == 0 } private var atLeastSyncedOnce = false + override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { + super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) + + edgesForExtendedLayout = .all + extendedLayoutIncludesOpaqueBars = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + // MARK: - Lifecycle override func viewDidLoad() { super.viewDidLoad() - refreshControl?.addTarget(self, action: #selector(refresh(_:)), for: .valueChanged) - + configureFetchResultsController() + configureTableView() configureFilterBar() configureTableView() - configureFooterView() - configureWindowlessCell() - configureNavbar() configureSearchController() - configureSearchHelper() configureAuthorFilter() - configureSearchBackingView() - configureGhostableTableView() + configureDefaultNavigationBarAppearance() - WPStyleGuide.configureColors(view: view, tableView: tableView) - tableView.reloadData() + updateAndPerformFetchRequest() observeNetworkStatus() } @@ -167,46 +115,30 @@ class AbstractPostListViewController: UIViewController, override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - startGhost() - if reloadTableViewBeforeAppearing { reloadTableViewBeforeAppearing = false tableView.reloadData() } - filterTabBar.layoutIfNeeded() updateSelectedFilter() refreshResults() - } - - fileprivate var searchBarHeight: CGFloat { - return searchController.searchBar.bounds.height + view.safeAreaInsets.top - } - - fileprivate func localKeyboardFrameFromNotification(_ notification: Foundation.Notification) -> CGRect { - let key = UIResponder.keyboardFrameEndUserInfoKey - guard let keyboardFrame = (notification.userInfo?[key] as? NSValue)?.cgRectValue else { - return .zero - } - // Convert the frame from window coordinates - return view.convert(keyboardFrame, from: nil) + // Show it initially but allow the user to dismiss it by scrolling + navigationItem.hidesSearchBarWhenScrolling = false } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) automaticallySyncIfAppropriate() + + navigationItem.hidesSearchBarWhenScrolling = true } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) - if searchController?.isActive == true { - searchController?.isActive = false - } - dismissAllNetworkErrorNotices() NotificationCenter.default.removeObserver(self, name: UIApplication.didBecomeActiveNotification, object: nil) @@ -214,227 +146,143 @@ class AbstractPostListViewController: UIViewController, // MARK: - Configuration - func heightForFooterView() -> CGFloat { - return type(of: self).defaultHeightForFooterView + private func configureFetchResultsController() { + fetchResultsController = NSFetchedResultsController(fetchRequest: fetchRequest(), managedObjectContext: managedObjectContext(), sectionNameKeyPath: nil, cacheName: nil) + fetchResultsController.delegate = self } - func configureNavbar() { - // IMPORTANT: this code makes sure that the back button in WPPostViewController doesn't show - // this VC's title. - // - let backButton = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil) - navigationItem.backBarButtonItem = backButton + func configureTableView() { + view.addSubview(tableView) + tableView.translatesAutoresizingMaskIntoConstraints = false + view.pinSubviewToAllEdges(tableView) + + tableView.dataSource = self + tableView.delegate = self + tableView.backgroundColor = .systemBackground + tableView.sectionHeaderTopPadding = 0 + tableView.estimatedRowHeight = 110 + tableView.rowHeight = UITableView.automaticDimension + tableView.refreshControl = refreshControl + refreshControl.addTarget(self, action: #selector(refresh), for: .valueChanged) } - func configureFilterBar() { + private func configureFilterBar() { WPStyleGuide.configureFilterTabBar(filterTabBar) - + filterTabBar.backgroundColor = .clear filterTabBar.items = filterSettings.availablePostListFilters() - filterTabBar.addTarget(self, action: #selector(selectedFilterDidChange(_:)), for: .valueChanged) - } - func configureTableView() { - assert(false, "You should implement this method in the subclass") + filterTabBar.translatesAutoresizingMaskIntoConstraints = true + filterTabBar.frame = CGRect(x: 0, y: 0, width: view.bounds.width, height: 40) + tableView.tableHeaderView = filterTabBar } - func configureFooterView() { - - let mainBundle = Bundle.main - - guard let footerView = mainBundle.loadNibNamed("PostListFooterView", owner: nil, options: nil)![0] as? PostListFooterView else { - preconditionFailure("Could not load the footer view from the nib file.") - } - - postListFooterView = footerView - postListFooterView.showSpinner(false) - - var frame = postListFooterView.frame - frame.size.height = heightForFooterView() - - postListFooterView.frame = frame - tableView.tableFooterView = postListFooterView - } - - @objc func configureWindowlessCell() { - tableView.register(UITableViewCell.self, forCellReuseIdentifier: abstractPostWindowlessCellIdenfitier) - } - - private func refreshResults() { + func refreshResults() { guard isViewLoaded == true else { return } let _ = DispatchDelayedAction(delay: .milliseconds(500)) { [weak self] in - self?.refreshControl?.endRefreshing() + self?.refreshControl.endRefreshing() } hideNoResultsView() if emptyResults { - stopGhostIfConnectionIsNotAvailable() showNoResultsView() } - - updateBackgroundColor() - } - - // Update controller's background color to avoid a white line below - // the search bar - due to a margin between searchBar and the tableView - private func updateBackgroundColor() { - if searchController.isActive && emptyResults { - view.backgroundColor = noResultsViewController.view.backgroundColor - } else { - view.backgroundColor = tableView.backgroundColor - } } - private func configureAuthorFilter() { - guard filterSettings.canFilterByAuthor() else { - return - } - - let authorFilter = AuthorFilterButton() - authorFilter.addTarget(self, action: #selector(showAuthorSelectionPopover(_:)), for: .touchUpInside) - filterTabBar.accessoryView = authorFilter + private func configureSearchController() { + assert(self is InteractivePostViewDelegate, "The subclass has to implement InteractivePostViewDelegate protocol") - updateAuthorFilter() - } + searchResultsViewController.configure(searchController, self as? InteractivePostViewDelegate) - /// Subclasses should override this method (and call super) to insert the - /// search controller's search bar into the view hierarchy - @objc func configureSearchController() { - // Required for insets to work out correctly when the search bar becomes active - extendedLayoutIncludesOpaqueBars = true definesPresentationContext = true - - searchController = UISearchController(searchResultsController: nil) - searchController.obscuresBackgroundDuringPresentation = false - - searchController.delegate = self - searchController.searchResultsUpdater = self - - WPStyleGuide.configureSearchBar(searchController.searchBar) - - searchController.searchBar.autocorrectionType = .default - } - - fileprivate func configureInitialScrollInsets() { - tableView.layoutIfNeeded() - tableView.contentInset = .zero - tableView.scrollIndicatorInsets = .zero - tableView.contentOffset = .zero + navigationItem.searchController = searchController + if #available(iOS 16.0, *) { + navigationItem.preferredSearchBarPlacement = .stacked + } } - fileprivate func configureSearchBackingView() { - // This mask view is required to cover the area between the top of the search - // bar and the top of the screen on an iPhone X and on iOS 10. - let topAnchor = view.safeAreaLayoutGuide.topAnchor + func propertiesForAnalytics() -> [String: AnyObject] { + var properties = [String: AnyObject]() - let backingView = UIView() - view.addSubview(backingView) + properties["type"] = postTypeToSync().rawValue as AnyObject? + properties["filter"] = filterSettings.currentPostListFilter().title as AnyObject? - backingView.backgroundColor = searchController.searchBar.barTintColor - backingView.translatesAutoresizingMaskIntoConstraints = false + if let dotComID = blog.dotComID { + properties[WPAppAnalyticsKeyBlogID] = dotComID + } - NSLayoutConstraint.activate([ - backingView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - backingView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - backingView.topAnchor.constraint(equalTo: view.topAnchor), - backingView.bottomAnchor.constraint(equalTo: topAnchor) - ]) + return properties } - func configureGhostableTableView() { - view.addSubview(ghostableTableView) - ghostableTableView.isHidden = true + // MARK: - Author Filter - ghostableTableView.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - ghostableTableView.widthAnchor.constraint(equalTo: tableView.widthAnchor), - ghostableTableView.heightAnchor.constraint(equalTo: tableView.heightAnchor), - ghostableTableView.leadingAnchor.constraint(equalTo: tableView.leadingAnchor), - ghostableTableView.topAnchor.constraint(equalTo: searchController.searchBar.bottomAnchor) - ]) + private func configureAuthorFilter() { + guard filterSettings.canFilterByAuthor() else { + return + } + buttonAuthorFilter.sizeToFit() + navigationItem.rightBarButtonItem = UIBarButtonItem(customView: buttonAuthorFilter) - ghostableTableView.backgroundColor = .white - ghostableTableView.isScrollEnabled = false + buttonAuthorFilter.addTarget(self, action: #selector(showAuthorSelectionPopover(_:)), for: .touchUpInside) + updateAuthorFilter() } - @objc func configureSearchHelper() { - searchHelper.resetConfiguration() - searchHelper.configureImmediateSearch({ [weak self] in - self?.updateForLocalPostsMatchingSearchText() - }) - searchHelper.configureDeferredSearch({ [weak self] in - self?.syncPostsMatchingSearchText() - }) + private func updateAuthorFilter() { + if filterSettings.currentPostAuthorFilter() == .everyone { + buttonAuthorFilter.filterType = .everyone + } else { + buttonAuthorFilter.filterType = .user(gravatarEmail: blog.account?.email) + } } - @objc func propertiesForAnalytics() -> [String: AnyObject] { - var properties = [String: AnyObject]() - - properties["type"] = postTypeToSync().rawValue as AnyObject? - properties["filter"] = filterSettings.currentPostListFilter().title as AnyObject? + @objc private func showAuthorSelectionPopover(_ sender: UIView) { + let filterController = AuthorFilterViewController(initialSelection: filterSettings.currentPostAuthorFilter(), gravatarEmail: blog.account?.email, postType: postTypeToSync()) { [weak self] filter in + if filter != self?.filterSettings.currentPostAuthorFilter() { + UIAccessibility.post(notification: UIAccessibility.Notification.screenChanged, argument: sender) + } - if let dotComID = blog.dotComID { - properties[WPAppAnalyticsKeyBlogID] = dotComID + self?.filterSettings.setCurrentPostAuthorFilter(filter) + self?.updateAuthorFilter() + self?.refreshAndReload() + self?.syncItemsWithUserInteraction(false) + self?.dismiss(animated: true) } - return properties + ForcePopoverPresenter.configurePresentationControllerForViewController(filterController, presentingFromView: sender) + filterController.popoverPresentationController?.permittedArrowDirections = .up + + present(filterController, animated: true) } // MARK: - GUI: No results view logic func hideNoResultsView() { - postListFooterView.isHidden = false noResultsViewController.removeFromView() } func showNoResultsView() { - guard refreshNoResultsViewController != nil, atLeastSyncedOnce else { return } - - postListFooterView.isHidden = true refreshNoResultsViewController(noResultsViewController) // Only add no results view if it isn't already in the table view if noResultsViewController.view.isDescendant(of: tableView) == false { - tableViewController.addChild(noResultsViewController) - tableView.addSubview(withFadeAnimation: noResultsViewController.view) - noResultsViewController.view.frame = tableView.frame - - // Adjust the NRV to accommodate for the search bar. - if let tableHeaderView = tableView.tableHeaderView { - noResultsViewController.view.frame.origin.y = tableHeaderView.frame.origin.y - } - - noResultsViewController.didMove(toParent: tableViewController) + self.addChild(noResultsViewController) + tableView.addSubview(noResultsViewController.view) + noResultsViewController.view.frame = tableView.frame.offsetBy(dx: 0, dy: -view.safeAreaInsets.top + 40) + noResultsViewController.didMove(toParent: self) } tableView.sendSubviewToBack(noResultsViewController.view) } - // MARK: - TableView Helpers - - @objc func dequeCellForWindowlessLoadingIfNeeded(_ tableView: UITableView) -> UITableViewCell? { - // As also seen in ReaderStreamViewController: - // We want to avoid dequeuing card cells when we're not present in a window, on the iPad. - // Doing so can create a situation where cells are not updated with the correct NSTraitCollection. - // The result is the cells do not show the correct layouts relative to superview margins. - // HACK: kurzee, 2016-07-12 - // Use a generic cell in this situation and reload the table view once its back in a window. - if tableView.window == nil { - reloadTableViewBeforeAppearing = true - return tableView.dequeueReusableCell(withIdentifier: abstractPostWindowlessCellIdenfitier) - } - return nil - } - - // MARK: - TableViewHandler Delegate Methods + // MARK: - Core Data - @objc func entityName() -> String { + func entityName() -> String { fatalError("You should implement this method in the subclass") } @@ -442,8 +290,8 @@ class AbstractPostListViewController: UIViewController, return ContextManager.sharedInstance().mainContext } - func fetchRequest() -> NSFetchRequest? { - let fetchRequest = NSFetchRequest(entityName: entityName()) + func fetchRequest() -> NSFetchRequest { + let fetchRequest = NSFetchRequest(entityName: entityName()) fetchRequest.predicate = predicateForFetchRequest() fetchRequest.sortDescriptors = sortDescriptorsForFetchRequest() fetchRequest.fetchBatchSize = fetchBatchSize @@ -451,24 +299,20 @@ class AbstractPostListViewController: UIViewController, return fetchRequest } - @objc func sortDescriptorsForFetchRequest() -> [NSSortDescriptor] { + func sortDescriptorsForFetchRequest() -> [NSSortDescriptor] { return filterSettings.currentPostListFilter().sortDescriptors } - @objc func updateAndPerformFetchRequest() { + func updateAndPerformFetchRequest() { assert(Thread.isMainThread, "AbstractPostListViewController Error: NSFetchedResultsController accessed in BG") var predicate = predicateForFetchRequest() let sortDescriptors = sortDescriptorsForFetchRequest() - guard let fetchRequest = tableViewHandler.resultsController?.fetchRequest else { - DDLogError("Error getting the fetch request") - return - } + let fetchRequest = fetchResultsController.fetchRequest - // Set the predicate based on filtering by the oldestPostDate and not searching. let filter = filterSettings.currentPostListFilter() - if let oldestPostDate = filter.oldestPostDate, !isSearching() { + if let oldestPostDate = filter.oldestPostDate { // Filter posts by any posts newer than the filter's oldestPostDate. // Also include any posts that don't have a date set, such as local posts created without a connection. @@ -477,12 +321,12 @@ class AbstractPostListViewController: UIViewController, predicate = NSCompoundPredicate.init(andPredicateWithSubpredicates: [predicate, datePredicate]) } - // Set up the fetchLimit based on filtering or searching - if filter.oldestPostDate != nil || isSearching() == true { - // If filtering by the oldestPostDate or searching, the fetchLimit should be disabled. + // Set up the fetchLimit based on filtering + if filter.oldestPostDate != nil { + // If filtering by the oldestPostDate, the fetchLimit should be disabled. fetchRequest.fetchLimit = 0 } else { - // If not filtering by the oldestPostDate or searching, set the fetchLimit to the default number of posts. + // If not filtering by the oldestPostDate, set the fetchLimit to the default number of posts. fetchRequest.fetchLimit = fetchLimit } @@ -490,66 +334,72 @@ class AbstractPostListViewController: UIViewController, fetchRequest.sortDescriptors = sortDescriptors do { - try tableViewHandler.resultsController?.performFetch() + try fetchResultsController.performFetch() } catch { DDLogError("Error fetching posts after updating the fetch request predicate: \(error)") } } - @objc func updateAndPerformFetchRequestRefreshingResults() { + func updateAndPerformFetchRequestRefreshingResults() { updateAndPerformFetchRequest() tableView.reloadData() refreshResults() } - @objc func resetTableViewContentOffset(_ animated: Bool = false) { - // Reset the tableView contentOffset to the top before we make any dataSource changes. - var tableOffset = tableView.contentOffset - tableOffset.y = -tableView.contentInset.top - tableView.setContentOffset(tableOffset, animated: animated) - } - - @objc func predicateForFetchRequest() -> NSPredicate { + func predicateForFetchRequest() -> NSPredicate { fatalError("You should implement this method in the subclass") } - // MARK: - Table View Handling + // MARK: - NSFetchedResultsControllerDelegate - func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { - // When using UITableViewAutomaticDimension for auto-sizing cells, UITableView - // likes to reload rows in a strange way. - // It uses the estimated height as a starting value for reloading animations. - // So this estimated value needs to be as accurate as possible to avoid any "jumping" in - // the cell heights during reload animations. - // Note: There may (and should) be a way to get around this, but there is currently no obvious solution. - // Brent C. August 2/2016 - if let height = estimatedHeightsCache.object(forKey: indexPath as AnyObject) as? CGFloat { - // Return the previously known height as it was cached via willDisplayCell. - return height - } - // Otherwise return whatever we have set to the tableView explicitly, and ideally a pretty close value. - return tableView.estimatedRowHeight + func controllerWillChangeContent(_ controller: NSFetchedResultsController) { + tableView.beginUpdates() } - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - return UITableView.automaticDimension + func controller(_ controller: NSFetchedResultsController, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) { + switch type { + case .insert: + guard let newIndexPath else { return } + tableView.insertRows(at: [newIndexPath], with: .none) + case .delete: + guard let indexPath else { return } + tableView.deleteRows(at: [indexPath], with: .fade) + case .update: + guard let indexPath else { return } + tableView.reloadRows(at: [indexPath], with: .none) + case .move: + guard let indexPath, let newIndexPath else { return } + tableView.moveRow(at: indexPath, to: newIndexPath) + @unknown default: + break + } } - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - assert(false, "You should implement this method in the subclass") + func controllerDidChangeContent(_ controller: NSFetchedResultsController) { + do { // Some defensive code, just in case + try WPException.objcTry { + self.tableView.endUpdates() + } + } catch { + tableView.reloadData() + } + refreshResults() } - func tableViewDidChangeContent(_ tableView: UITableView) { - refreshResults() + // MARK: - UITableViewDataSource + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + fetchResultsController.fetchedObjects?.count ?? 0 } - func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + fatalError("Not implemented") + } - // Cache the cell's layout height as the currently known height, for estimation. - // See estimatedHeightForRowAtIndexPath - estimatedHeightsCache.setObject(cell.frame.height as AnyObject, forKey: indexPath as AnyObject) + // MARK: - UITableViewDelegate - guard isViewOnScreen() && !isSearching() else { + func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { + guard isViewOnScreen() else { return } @@ -564,54 +414,17 @@ class AbstractPostListViewController: UIViewController, } } - func configureCell(_ cell: UITableViewCell, at indexPath: IndexPath) { - assert(false, "You should implement this method in the subclass") - } - // MARK: - Actions - @IBAction func refresh(_ sender: AnyObject) { + @objc private func refresh(_ sender: AnyObject) { syncItemsWithUserInteraction(true) WPAnalytics.track(.postListPullToRefresh, withProperties: propertiesForAnalytics()) } - @objc - private func showAuthorSelectionPopover(_ sender: UIView) { - let filterController = AuthorFilterViewController(initialSelection: filterSettings.currentPostAuthorFilter(), - gravatarEmail: blog.account?.email) { [weak self] filter in - if filter != self?.filterSettings.currentPostAuthorFilter() { - UIAccessibility.post(notification: UIAccessibility.Notification.screenChanged, argument: sender) - } - - self?.filterSettings.setCurrentPostAuthorFilter(filter) - self?.updateAuthorFilter() - self?.refreshAndReload() - self?.syncItemsWithUserInteraction(false) - self?.dismiss(animated: true) - } - - ForcePopoverPresenter.configurePresentationControllerForViewController(filterController, presentingFromView: sender) - filterController.popoverPresentationController?.permittedArrowDirections = .up - - present(filterController, animated: true) - } - - private func updateAuthorFilter() { - guard let accessoryView = filterTabBar.accessoryView as? AuthorFilterButton else { - return - } - - if filterSettings.currentPostAuthorFilter() == .everyone { - accessoryView.filterType = .everyone - } else { - accessoryView.filterType = .user(gravatarEmail: blog.account?.email) - } - } + // MARK: - Syncing - // MARK: - Synching - - @objc func automaticallySyncIfAppropriate() { + private func automaticallySyncIfAppropriate() { // Only automatically refresh if the view is loaded and visible on the screen if !isViewLoaded || view.window == nil { DDLogVerbose("View is not visible and will not check for auto refresh.") @@ -628,13 +441,8 @@ class AbstractPostListViewController: UIViewController, return } - if let lastSynced = lastSyncDate(), abs(lastSynced.timeIntervalSinceNow) <= type(of: self).postsControllerRefreshInterval { - - refreshResults() - } else { - // Update in the background - syncItemsWithUserInteraction(false) - } + // Update in the background + syncItemsWithUserInteraction(false) } @objc func syncItemsWithUserInteraction(_ userInteraction: Bool) { @@ -642,7 +450,7 @@ class AbstractPostListViewController: UIViewController, refreshResults() } - @objc func updateFilter(_ filter: PostListFilter, withSyncedPosts posts: [AbstractPost], syncOptions options: PostServiceSyncOptions) { + @objc func updateFilter(_ filter: PostListFilter, withSyncedPosts posts: [AbstractPost], hasMore: Bool) { guard posts.count > 0 else { assertionFailure("This method should not be called with no posts.") return @@ -650,9 +458,9 @@ class AbstractPostListViewController: UIViewController, // Reset the filter to only show the latest sync point, based on the oldest post date in the posts just synced. // Note: Getting oldest date manually as the API may return results out of order if there are // differing time offsets in the created dates. - let oldestPost = posts.min {$0.date_created_gmt < $1.date_created_gmt} + let oldestPost = posts.min { ($0.date_created_gmt ?? .distantPast) < ($1.date_created_gmt ?? .distantPast) } filter.oldestPostDate = oldestPost?.date_created_gmt - filter.hasMore = posts.count >= options.number.intValue + filter.hasMore = hasMore updateAndPerformFetchRequestRefreshingResults() } @@ -668,122 +476,92 @@ class AbstractPostListViewController: UIViewController, return .any } - @objc func lastSyncDate() -> Date? { - return blog.lastPostsSync + @MainActor + func syncPosts(isFirstPage: Bool) async throws -> SyncPostResult { + let postType = postTypeToSync() + let filter = filterSettings.currentPostListFilter() + let author = filterSettings.shouldShowOnlyMyPosts() ? blogUserID() : nil + + let coreDataStack = ContextManager.shared + let blogID = TaggedManagedObjectID(blog) + let number = numberOfLoadedElement.intValue + + let repository = PostRepository(coreDataStack: coreDataStack) + let result = try await repository.paginate( + type: postType == .post ? Post.self : Page.self, + statuses: filter.statuses, + authorUserID: author, + offset: isFirstPage ? 0 : fetchResultsController.fetchedObjects?.count ?? 0, + number: number, + in: blogID + ) + + let posts = try result.map { try coreDataStack.mainContext.existingObject(with: $0) } + let hasMore = result.count >= number + + return (posts, hasMore) } func syncHelper(_ syncHelper: WPContentSyncHelper, syncContentWithUserInteraction userInteraction: Bool, success: ((_ hasMore: Bool) -> ())?, failure: ((_ error: NSError) -> ())?) { - if recentlyTrashedPostObjectIDs.count > 0 { - refreshAndReload() - } - - let postType = postTypeToSync() let filter = filterSettings.currentPostListFilter() - let author = filterSettings.shouldShowOnlyMyPosts() ? blogUserID() : nil + Task { @MainActor [weak self] in + do { + guard let (posts, hasMore) = try await self?.syncPosts(isFirstPage: true) else { return } - let postService = PostService(managedObjectContext: managedObjectContext()) - - let options = PostServiceSyncOptions() - options.statuses = filter.statuses.strings - options.authorID = author - options.number = numberOfLoadedElement - options.purgesLocalSync = true - - postService.syncPosts( - ofType: postType, - with: options, - for: blog, - success: {[weak self] posts in - guard let strongSelf = self, - let posts = posts else { - return - } + guard let self else { return } if posts.count > 0 { - strongSelf.updateFilter(filter, withSyncedPosts: posts, syncOptions: options) + self.updateFilter(filter, withSyncedPosts: posts, hasMore: hasMore) SearchManager.shared.indexItems(posts) } success?(filter.hasMore) - - if strongSelf.isSearching() { - // If we're currently searching, go ahead and request a sync with the searchText since - // an action was triggered to syncContent. - strongSelf.syncPostsMatchingSearchText() - } - }, failure: {[weak self] (error: Error?) -> () in - - guard let strongSelf = self, - let error = error else { - return - } + } catch { + guard let self else { return } failure?(error as NSError) if userInteraction == true { - strongSelf.handleSyncFailure(error as NSError) + self.handleSyncFailure(error as NSError) } - }) + } + } } - let loadMoreCounter = LoadMoreCounter() func syncHelper(_ syncHelper: WPContentSyncHelper, syncMoreWithSuccess success: ((_ hasMore: Bool) -> Void)?, failure: ((_ error: NSError) -> Void)?) { - // See https://github.com/wordpress-mobile/WordPress-iOS/issues/6819 - loadMoreCounter.increment(properties: propertiesForAnalytics()) - - postListFooterView.showSpinner(true) + setFooterHidden(false) - let postType = postTypeToSync() let filter = filterSettings.currentPostListFilter() - let author = filterSettings.shouldShowOnlyMyPosts() ? blogUserID() : nil - let postService = PostService(managedObjectContext: managedObjectContext()) - - let options = PostServiceSyncOptions() - options.statuses = filter.statuses.strings - options.authorID = author - options.number = numberOfLoadedElement - options.offset = tableViewHandler.resultsController?.fetchedObjects?.count as NSNumber? - - postService.syncPosts( - ofType: postType, - with: options, - for: blog, - success: {[weak self] posts in - guard let strongSelf = self, - let posts = posts else { - return - } + Task { @MainActor [weak self] in + do { + guard let (posts, hasMore) = try await self?.syncPosts(isFirstPage: false) else { return } + + // User may have exit the screen when the "syncPosts" call above completes. + guard let self else { return } if posts.count > 0 { - strongSelf.updateFilter(filter, withSyncedPosts: posts, syncOptions: options) + self.updateFilter(filter, withSyncedPosts: posts, hasMore: hasMore) SearchManager.shared.indexItems(posts) } success?(filter.hasMore) - }, failure: { (error) -> () in - - guard let error = error else { - return - } - + } catch { failure?(error as NSError) - }) + } + } } func syncContentStart(_ syncHelper: WPContentSyncHelper) { - startGhost() atLeastSyncedOnce = true } func syncContentEnded(_ syncHelper: WPContentSyncHelper) { - refreshControl?.endRefreshing() - postListFooterView.showSpinner(false) + refreshControl.endRefreshing() + setFooterHidden(true) noResultsViewController.removeFromView() - stopGhost() - if emptyResults { // This is a special case. Core data can be a bit slow about notifying // NSFetchedResultsController delegates about changes to the fetched results. @@ -798,13 +576,11 @@ class AbstractPostListViewController: UIViewController, @objc func handleSyncFailure(_ error: NSError) { if error.domain == WPXMLRPCFaultErrorDomain - && error.code == type(of: self).HTTPErrorCodeForbidden { - promptForPassword() + && error.code == type(of: self).httpErrorCodeForbidden { + WordPressAppDelegate.shared?.showPasswordInvalidPrompt(for: blog) return } - stopGhost() - dismissAllNetworkErrorNotices() // If there is no internet connection, we'll show the specific error message defined in @@ -818,131 +594,9 @@ class AbstractPostListViewController: UIViewController, } } - @objc func promptForPassword() { - let message = NSLocalizedString("The username or password stored in the app may be out of date. Please re-enter your password in the settings and try again.", comment: "Error message informing a user about an invalid password.") - - // bad login/pass combination - let editSiteViewController = SiteSettingsViewController(blog: blog) - - let navController = UINavigationController(rootViewController: editSiteViewController!) - navController.navigationBar.isTranslucent = false - - navController.modalTransitionStyle = .crossDissolve - navController.modalPresentationStyle = .formSheet - - WPError.showAlert(withTitle: NSLocalizedString("Unable to Connect", comment: "An error message."), message: message, withSupportButton: true) { _ in - self.present(navController, animated: true) - } - } - - // MARK: - Ghost cells - - final func startGhost() { - guard ghostingEnabled, emptyResults else { - return - } - - if isViewOnScreen() { - ghostableTableView.startGhostAnimation() - } - ghostableTableView.isHidden = false - noResultsViewController.view.isHidden = true - } - - final func stopGhost() { - ghostableTableView.isHidden = true - ghostableTableView.stopGhostAnimation() - noResultsViewController.view.isHidden = false - } - - private func stopGhostIfConnectionIsNotAvailable() { - guard WordPressAppDelegate.shared?.connectionAvailable == false else { - return - } - - atLeastSyncedOnce = true - stopGhost() - } - - // MARK: - Searching - - @objc func isSearching() -> Bool { - return searchController.isActive && currentSearchTerm()?.count > 0 - } - - @objc func currentSearchTerm() -> String? { - return searchController.searchBar.text - } - - @objc func updateForLocalPostsMatchingSearchText() { - updateAndPerformFetchRequest() - tableView.reloadData() - - let filter = filterSettings.currentPostListFilter() - if filter.hasMore && emptyResults { - // If the filter detects there are more posts, but there are none that match the current search - // hide the no results view while the upcoming syncPostsMatchingSearchText() may in fact load results. - hideNoResultsView() - postListFooterView.isHidden = true - } else { - refreshResults() - } - } - - @objc func isSyncingPostsWithSearch() -> Bool { - return searchesSyncing > 0 - } - - @objc func postsSyncWithSearchDidBegin() { - searchesSyncing += 1 - postListFooterView.showSpinner(true) - postListFooterView.isHidden = false - } - - @objc func postsSyncWithSearchEnded() { - searchesSyncing -= 1 - assert(searchesSyncing >= 0, "Expected Int searchesSyncing to be 0 or greater while searching.") - if !isSyncingPostsWithSearch() { - postListFooterView.showSpinner(false) - refreshResults() - } - } - - @objc func syncPostsMatchingSearchText() { - guard let searchText = searchController.searchBar.text, !searchText.isEmpty() else { - return - } - let filter = filterSettings.currentPostListFilter() - guard filter.hasMore else { - return - } - - postsSyncWithSearchDidBegin() - - let author = filterSettings.shouldShowOnlyMyPosts() ? blogUserID() : nil - let postService = PostService(managedObjectContext: managedObjectContext()) - let options = PostServiceSyncOptions() - options.statuses = filter.statuses.strings - options.authorID = author - options.number = 20 - options.purgesLocalSync = false - options.search = searchText - - postService.syncPosts( - ofType: postTypeToSync(), - with: options, - for: blog, - success: { [weak self] posts in - self?.postsSyncWithSearchEnded() - }, failure: { [weak self] (error) in - self?.postsSyncWithSearchEnded() - } - ) - } - // MARK: - Actions - @objc func publishPost(_ apost: AbstractPost, completion: (() -> Void)? = nil) { + @objc func publishPost(_ post: AbstractPost, completion: (() -> Void)? = nil) { let title = NSLocalizedString("Are you sure you want to publish?", comment: "Title of the message shown when the user taps Publish in the post list.") let cancelTitle = NSLocalizedString("Cancel", comment: "Button shown when the author is asked for publishing confirmation.") @@ -955,23 +609,23 @@ class AbstractPostListViewController: UIViewController, alertController.addDefaultActionWithTitle(publishTitle) { [unowned self] _ in WPAnalytics.track(.postListPublishAction, withProperties: self.propertiesForAnalytics()) - PostCoordinator.shared.publish(apost) + PostCoordinator.shared.publish(post) completion?() } present(alertController, animated: true) } - @objc func moveToDraft(_ apost: AbstractPost) { + @objc func moveToDraft(_ post: AbstractPost) { WPAnalytics.track(.postListDraftAction, withProperties: propertiesForAnalytics()) - PostCoordinator.shared.moveToDraft(apost) + PostCoordinator.shared.moveToDraft(post) } - @objc func viewPost(_ apost: AbstractPost) { + @objc func viewPost(_ post: AbstractPost) { WPAnalytics.track(.postListViewAction, withProperties: propertiesForAnalytics()) - let post = apost.hasRevision() ? apost.revision! : apost + let post = post.hasRevision() ? post.revision! : post let controller = PreviewWebKitViewController(post: post, source: "posts_pages_view_post") controller.trackOpenEvent() @@ -985,130 +639,15 @@ class AbstractPostListViewController: UIViewController, navigationController?.present(navWrapper, animated: true) } - @objc func deletePost(_ apost: AbstractPost) { - WPAnalytics.track(.postListTrashAction, withProperties: propertiesForAnalytics()) - - let postObjectID = apost.objectID - - recentlyTrashedPostObjectIDs.append(postObjectID) - - // Remove the trashed post from spotlight - SearchManager.shared.deleteSearchableItem(apost) - - // Update the fetch request *before* making the service call. - updateAndPerformFetchRequest() - - let indexPath = tableViewHandler.resultsController?.indexPath(forObject: apost) - - if let indexPath = indexPath { - tableView.reloadRows(at: [indexPath], with: .fade) - } - - let postService = PostService(managedObjectContext: ContextManager.sharedInstance().mainContext) - - let trashed = (apost.status == .trash) - - postService.trashPost(apost, success: { - // If we permanently deleted the post - if trashed { - PostCoordinator.shared.cancelAnyPendingSaveOf(post: apost) - MediaCoordinator.shared.cancelUploadOfAllMedia(for: apost) - } - }, failure: { [weak self] (error) in - - guard let strongSelf = self else { - return - } - - if let error = error as NSError?, error.code == type(of: strongSelf).HTTPErrorCodeForbidden { - strongSelf.promptForPassword() - } else { - WPError.showXMLRPCErrorAlert(error) - } - - if let index = strongSelf.recentlyTrashedPostObjectIDs.firstIndex(of: postObjectID) { - strongSelf.recentlyTrashedPostObjectIDs.remove(at: index) - // We don't really know what happened here, why did the request fail? - // Maybe we could not delete the post or maybe the post was already deleted - // It is safer to re fetch the results than to reload that specific row - DispatchQueue.main.async { - strongSelf.updateAndPerformFetchRequestRefreshingResults() - } - } - }) - } - - @objc func restorePost(_ apost: AbstractPost, completion: (() -> Void)? = nil) { - WPAnalytics.track(.postListRestoreAction, withProperties: propertiesForAnalytics()) - - // if the post was recently deleted, update the status helper and reload the cell to display a spinner - let postObjectID = apost.objectID - - if let index = recentlyTrashedPostObjectIDs.firstIndex(of: postObjectID) { - recentlyTrashedPostObjectIDs.remove(at: index) - } - - if filterSettings.currentPostListFilter().filterType != .draft { - // Needed or else the post will remain in the published list. - updateAndPerformFetchRequest() - tableView.reloadData() - } - - let postService = PostService(managedObjectContext: ContextManager.sharedInstance().mainContext) - - postService.restore(apost, success: { [weak self] in - - guard let strongSelf = self else { - return - } - - var apost: AbstractPost - - // Make sure the post still exists. - do { - apost = try strongSelf.managedObjectContext().existingObject(with: postObjectID) as! AbstractPost - } catch { - DDLogError("\(error)") - return - } - - DispatchQueue.main.async { - completion?() - } - - if let postStatus = apost.status { - // If the post was restored, see if it appears in the current filter. - // If not, prompt the user to let it know under which filter it appears. - let filter = strongSelf.filterSettings.filterThatDisplaysPostsWithStatus(postStatus) - - if filter.filterType == strongSelf.filterSettings.currentPostListFilter().filterType { - return - } - - strongSelf.promptThatPostRestoredToFilter(filter) - - // Reindex the restored post in spotlight - SearchManager.shared.indexItem(apost) - } - }) { [weak self] (error) in - - guard let strongSelf = self else { - return - } - - if let error = error as NSError?, error.code == type(of: strongSelf).HTTPErrorCodeForbidden { - strongSelf.promptForPassword() - } else { - WPError.showXMLRPCErrorAlert(error) - } - - strongSelf.recentlyTrashedPostObjectIDs.append(postObjectID) + func deletePost(_ post: AbstractPost) { + Task { + await PostCoordinator.shared.delete(post) } } - @objc func copyPostLink(_ apost: AbstractPost) { + @objc func copyPostLink(_ post: AbstractPost) { let pasteboard = UIPasteboard.general - guard let link = apost.permaLink else { return } + guard let link = post.permaLink else { return } pasteboard.string = link as String let noticeTitle = NSLocalizedString("Link Copied to Clipboard", comment: "Link copied to clipboard notice title") let notice = Notice(title: noticeTitle, feedbackType: .success) @@ -1116,10 +655,6 @@ class AbstractPostListViewController: UIViewController, ActionDispatcher.dispatch(NoticeAction.post(notice)) } - @objc func promptThatPostRestoredToFilter(_ filter: PostListFilter) { - assert(false, "You should implement this method in the subclass") - } - private func dismissAllNetworkErrorNotices() { dismissNoNetworkAlert() WPError.dismissNetworkingNotice() @@ -1145,9 +680,7 @@ class AbstractPostListViewController: UIViewController, // MARK: - Filtering @objc func refreshAndReload() { - recentlyTrashedPostObjectIDs.removeAll() updateSelectedFilter() - resetTableViewContentOffset() updateAndPerformFetchRequestRefreshingResults() } @@ -1157,12 +690,7 @@ class AbstractPostListViewController: UIViewController, WPAnalytics.track(.postListStatusFilterChanged, withProperties: propertiesForAnalytics()) } - func updateFilter(index: Int) { - filterSettings.setCurrentFilterIndex(index) - refreshAndReload() - } - - func updateSelectedFilter() { + private func updateSelectedFilter() { if filterTabBar.selectedIndex != filterSettings.currentFilterIndex() { filterTabBar.setSelectedIndex(filterSettings.currentFilterIndex(), animated: false) } @@ -1173,44 +701,31 @@ class AbstractPostListViewController: UIViewController, refreshAndReload() - startGhost() - syncItemsWithUserInteraction(false) - configureInitialScrollInsets() - WPAnalytics.track(.postListStatusFilterChanged, withProperties: propertiesForAnalytics()) } - // MARK: - Search Controller Delegate Methods - - func willPresentSearchController(_ searchController: UISearchController) { - WPAnalytics.track(.postListSearchOpened, withProperties: propertiesForAnalytics()) - } - - func willDismissSearchController(_ searchController: UISearchController) { - searchController.searchBar.text = nil - searchHelper.searchCanceled() - - configureInitialScrollInsets() - } - - func updateSearchResults(for searchController: UISearchController) { - resetTableViewContentOffset() - searchHelper.searchUpdated(searchController.searchBar.text) - } - // MARK: - NetworkAwareUI func contentIsEmpty() -> Bool { - return tableViewHandler.resultsController?.isEmpty() ?? true + fetchResultsController.isEmpty() } func noConnectionMessage() -> String { return ReachabilityUtils.noConnectionMessage() } - // MARK: - Others + // MARK: - Misc + + private func setFooterHidden(_ isHidden: Bool) { + if isHidden { + tableView.tableFooterView = nil + } else { + tableView.tableFooterView = PagingFooterView(state: .loading) + tableView.sizeToFitFooterView() + } + } override func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) { // We override this method to dismiss any Notice that is currently being shown. If we @@ -1219,17 +734,6 @@ class AbstractPostListViewController: UIViewController, dismissAllNetworkErrorNotices() super.present(viewControllerToPresent, animated: flag, completion: completion) } - - // MARK: - Accessibility - - override func accessibilityPerformEscape() -> Bool { - guard searchController.isActive else { - return super.accessibilityPerformEscape() - } - - searchController.isActive = false - return true - } } extension AbstractPostListViewController: NetworkStatusDelegate { diff --git a/WordPress/Classes/ViewRelated/Post/AbstractPostMenuHelper.swift b/WordPress/Classes/ViewRelated/Post/AbstractPostMenuHelper.swift new file mode 100644 index 000000000000..eedf463b274b --- /dev/null +++ b/WordPress/Classes/ViewRelated/Post/AbstractPostMenuHelper.swift @@ -0,0 +1,203 @@ +import Foundation +import UIKit + +struct AbstractPostMenuHelper { + let post: AbstractPost + + init(_ post: AbstractPost) { + self.post = post + } + + /// Creates a menu for post actions + /// + /// - parameters: + /// - presentingView: The view presenting the menu + /// - delegate: The delegate that performs post actions + func makeMenu(presentingView: UIView, delegate: InteractivePostViewDelegate) -> UIMenu { + return UIMenu(title: "", options: .displayInline, children: [ + UIDeferredMenuElement.uncached { [weak presentingView, weak delegate] completion in + guard let presentingView, let delegate else { return } + completion(makeSections(presentingView: presentingView, delegate: delegate)) + } + ]) + } + + private func makeSections() -> [AbstractPostButtonSection] { + switch post { + case let post as Post: + return PostCardStatusViewModel(post: post).buttonSections + case let page as Page: + return PageMenuViewModel(page: page).buttonSections + default: + assertionFailure("Unsupported entity: \(post)") + return [] + } + } + + /// Creates post actions grouped into sections + /// + /// - parameters: + /// - presentingView: The view presenting the menu + /// - delegate: The delegate that performs post actions + private func makeSections(presentingView: UIView, delegate: InteractivePostViewDelegate) -> [UIMenu] { + return makeSections() + .filter { !$0.buttons.isEmpty } + .map { section in + let actions = makeActions(for: section.buttons, presentingView: presentingView, delegate: delegate) + let menu = UIMenu(title: "", options: .displayInline, children: actions) + + if let submenuButton = section.submenuButton { + return UIMenu( + title: submenuButton.title(for: post), + image: submenuButton.icon, + children: [menu] + ) + } else { + return menu + } + } + } + + /// Creates post actions + /// + /// - parameters: + /// - buttons: The list of buttons to turn into post actions + /// - presentingView: The view presenting the menu + /// - delegate: The delegate that performs post actions + private func makeActions( + for buttons: [AbstractPostButton], + presentingView: UIView, + delegate: InteractivePostViewDelegate + ) -> [UIAction] { + return buttons.map { button in + UIAction(title: button.title(for: post), image: button.icon, attributes: button.attributes ?? [], handler: { [weak presentingView, weak delegate] _ in + guard let presentingView, let delegate else { return } + button.performAction(for: post, view: presentingView, delegate: delegate) + }) + } + } +} + +protocol AbstractPostMenuAction { + var icon: UIImage? { get } + var attributes: UIMenuElement.Attributes? { get } + func title(for post: AbstractPost) -> String + func performAction(for post: AbstractPost, view: UIView, delegate: InteractivePostViewDelegate) +} + +extension AbstractPostButton: AbstractPostMenuAction { + + var icon: UIImage? { + switch self { + case .retry: return UIImage(systemName: "arrow.clockwise") + case .view: return UIImage(systemName: "safari") + case .publish: return UIImage(systemName: "globe") + case .stats: return UIImage(systemName: "chart.bar.xaxis") + case .duplicate: return UIImage(systemName: "doc.on.doc") + case .moveToDraft: return UIImage(systemName: "pencil.line") + case .trash: return UIImage(systemName: "trash") + case .cancelAutoUpload: return UIImage(systemName: "xmark.icloud") + case .share: return UIImage(systemName: "square.and.arrow.up") + case .blaze: return UIImage(systemName: "flame") + case .comments: return UIImage(systemName: "bubble.right") + case .settings: return UIImage(systemName: "gearshape") + case .setParent: return UIImage(systemName: "text.append") + case .setHomepage: return UIImage(systemName: "house") + case .setPostsPage: return UIImage(systemName: "text.word.spacing") + case .setRegularPage: return UIImage(systemName: "arrow.uturn.backward") + case .pageAttributes: return UIImage(systemName: "doc") + } + } + + var attributes: UIMenuElement.Attributes? { + switch self { + case .trash: + return [UIMenuElement.Attributes.destructive] + default: + return nil + } + } + + func title(for post: AbstractPost) -> String { + switch self { + case .retry: return Strings.retry + case .view: return post.status == .publish ? Strings.view : Strings.preview + case .publish: return Strings.publish + case .stats: return Strings.stats + case .duplicate: return Strings.duplicate + case .moveToDraft: return Strings.draft + case .trash: return post.status == .trash ? Strings.delete : Strings.trash + case .cancelAutoUpload: return Strings.cancelAutoUpload + case .share: return Strings.share + case .blaze: return Strings.blaze + case .comments: return Strings.comments + case .settings: return Strings.settings + case .setParent: return Strings.setParent + case .setHomepage: return Strings.setHomepage + case .setPostsPage: return Strings.setPostsPage + case .setRegularPage: return Strings.setRegularPage + case .pageAttributes: return Strings.pageAttributes + } + } + + func performAction(for post: AbstractPost, view: UIView, delegate: InteractivePostViewDelegate) { + switch self { + case .retry: + delegate.retry(post) + case .view: + delegate.view(post) + case .publish: + delegate.publish(post) + case .stats: + delegate.stats(for: post) + case .duplicate: + delegate.duplicate(post) + case .moveToDraft: + delegate.draft(post) + case .trash: + delegate.trash(post) + case .cancelAutoUpload: + delegate.cancelAutoUpload(post) + case .share: + delegate.share(post, fromView: view) + case .blaze: + delegate.blaze(post) + case .comments: + delegate.comments(post) + case .settings: + delegate.showSettings(for: post) + case .setParent: + delegate.setParent(for: post) + case .setHomepage: + delegate.setHomepage(for: post) + case .setPostsPage: + delegate.setPostsPage(for: post) + case .setRegularPage: + delegate.setRegularPage(for: post) + case .pageAttributes: + break + } + } + + private enum Strings { + static let cancelAutoUpload = NSLocalizedString("posts.cancelUpload.actionTitle", value: "Cancel upload", comment: "Label for the Post List option that cancels automatic uploading of a post.") + static let stats = NSLocalizedString("posts.stats.actionTitle", value: "Stats", comment: "Label for post stats option. Tapping displays statistics for a post.") + static let comments = NSLocalizedString("posts.comments.actionTitle", value: "Comments", comment: "Label for post comments option. Tapping displays comments for a post.") + static let settings = NSLocalizedString("posts.settings.actionTitle", value: "Settings", comment: "Label for post settings option. Tapping displays settings for a post.") + static let duplicate = NSLocalizedString("posts.duplicate.actionTitle", value: "Duplicate", comment: "Label for post duplicate option. Tapping creates a copy of the post.") + static let publish = NSLocalizedString("posts.publish.actionTitle", value: "Publish now", comment: "Label for an option that moves a publishes a post immediately") + static let draft = NSLocalizedString("posts.draft.actionTitle", value: "Move to draft", comment: "Label for an option that moves a post to the draft folder") + static let delete = NSLocalizedString("posts.delete.actionTitle", value: "Delete permanently", comment: "Label for the delete post option. Tapping permanently deletes a post.") + static let trash = NSLocalizedString("posts.trash.actionTitle", value: "Move to trash", comment: "Label for a option that moves a post to the trash folder") + static let view = NSLocalizedString("posts.view.actionTitle", value: "View", comment: "Label for the view post button. Tapping displays the post as it appears on the web.") + static let preview = NSLocalizedString("posts.preview.actionTitle", value: "Preview", comment: "Label for the preview post button. Tapping displays the post as it appears on the web.") + static let retry = NSLocalizedString("posts.retry.actionTitle", value: "Retry", comment: "Retry uploading the post.") + static let share = NSLocalizedString("posts.share.actionTitle", value: "Share", comment: "Share the post.") + static let blaze = NSLocalizedString("posts.blaze.actionTitle", value: "Promote with Blaze", comment: "Promote the post with Blaze.") + static let setParent = NSLocalizedString("posts.setParent.actionTitle", value: "Set parent", comment: "Set the parent page for the selected page.") + static let setHomepage = NSLocalizedString("posts.setHomepage.actionTitle", value: "Set as homepage", comment: "Set the selected page as the homepage.") + static let setPostsPage = NSLocalizedString("posts.setPostsPage.actionTitle", value: "Set as posts page", comment: "Set the selected page as a posts page.") + static let setRegularPage = NSLocalizedString("posts.setRegularPage.actionTitle", value: "Set as regular page", comment: "Set the selected page as a regular page.") + static let pageAttributes = NSLocalizedString("posts.pageAttributes.actionTitle", value: "Page attributes", comment: "Opens a submenu for page attributes.") + } +} diff --git a/WordPress/Classes/ViewRelated/Post/AbstractPostMenuViewModel.swift b/WordPress/Classes/ViewRelated/Post/AbstractPostMenuViewModel.swift new file mode 100644 index 000000000000..47f0f3994035 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Post/AbstractPostMenuViewModel.swift @@ -0,0 +1,37 @@ +import Foundation + +protocol AbstractPostMenuViewModel { + var buttonSections: [AbstractPostButtonSection] { get } +} + +struct AbstractPostButtonSection { + let buttons: [AbstractPostButton] + let submenuButton: AbstractPostButton? + + init(buttons: [AbstractPostButton], submenuButton: AbstractPostButton? = nil) { + self.buttons = buttons + self.submenuButton = submenuButton + } +} + +enum AbstractPostButton: Equatable { + case retry + case view + case publish + case stats + case duplicate + case moveToDraft + case trash + case cancelAutoUpload + case share + case blaze + case comments + case settings + + /// Specific to pages + case pageAttributes + case setParent + case setHomepage + case setPostsPage + case setRegularPage +} diff --git a/WordPress/Classes/ViewRelated/Post/AuthorFilterButton.swift b/WordPress/Classes/ViewRelated/Post/AuthorFilterButton.swift index fea2e21f82a1..e8b07987998e 100644 --- a/WordPress/Classes/ViewRelated/Post/AuthorFilterButton.swift +++ b/WordPress/Classes/ViewRelated/Post/AuthorFilterButton.swift @@ -17,39 +17,15 @@ private extension AuthorFilterType { } } -/// Displays an author gravatar image with a dropdown arrow. -/// -class AuthorFilterButton: UIControl { +final class AuthorFilterButton: UIControl { private let authorImageView: CircularImageView = { let imageView = CircularImageView(image: UIImage.gravatarPlaceholderImage) imageView.translatesAutoresizingMaskIntoConstraints = false imageView.setContentHuggingPriority(.required, for: .horizontal) - imageView.backgroundColor = .neutral(.shade10) imageView.tintColor = .neutral(.shade70) - - return imageView - }() - - private let chevronImageView: UIImageView = { - let imageView = UIImageView(image: UIImage.makeChevronDownImage(with: .neutral(.shade40), size: Metrics.chevronSize)) - imageView.translatesAutoresizingMaskIntoConstraints = false - imageView.setContentHuggingPriority(.required, for: .horizontal) - imageView.contentMode = .bottom - return imageView }() - private let stackView: UIStackView = { - let stackView = UIStackView() - stackView.translatesAutoresizingMaskIntoConstraints = false - stackView.axis = .horizontal - stackView.alignment = .center - stackView.spacing = Metrics.stackViewSpacing - stackView.isUserInteractionEnabled = false - - return stackView - }() - override var intrinsicContentSize: CGSize { return Metrics.contentSize } @@ -58,7 +34,7 @@ class AuthorFilterButton: UIControl { didSet { switch filterType { case .everyone: - authorImageView.image = .gridicon(.multipleUsers, size: Metrics.multipleUsersGravatarSize) + authorImageView.image = UIImage(named: "icon-people")?.withTintColor(.text, renderingMode: .alwaysTemplate) authorImageView.contentMode = .center case .user(let email): authorImageView.contentMode = .scaleAspectFill @@ -86,17 +62,14 @@ class AuthorFilterButton: UIControl { } private func commonInit() { - stackView.addArrangedSubview(authorImageView) - stackView.addArrangedSubview(chevronImageView) - addSubview(stackView) + addSubview(authorImageView) NSLayoutConstraint.activate([ - stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Metrics.leadingPadding), - stackView.centerYAnchor.constraint(equalTo: centerYAnchor), + authorImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Metrics.leadingPadding), + authorImageView.centerYAnchor.constraint(equalTo: centerYAnchor), authorImageView.widthAnchor.constraint(equalToConstant: Metrics.gravatarSize.width), authorImageView.heightAnchor.constraint(equalToConstant: Metrics.gravatarSize.height), - widthAnchor.constraint(equalToConstant: intrinsicContentSize.width), - chevronImageView.heightAnchor.constraint(equalToConstant: Metrics.chevronSize.height + Metrics.chevronVerticalPadding) - ]) + widthAnchor.constraint(equalToConstant: intrinsicContentSize.width) + ]) authorImageView.image = gravatarPlaceholder @@ -106,12 +79,8 @@ class AuthorFilterButton: UIControl { private let gravatarPlaceholder: UIImage = .gridicon(.user, size: Metrics.gravatarSize) private enum Metrics { - static let chevronSize = CGSize(width: 10.0, height: 5.0) - static let contentSize = CGSize(width: 72.0, height: 44.0) + static let contentSize = CGSize(width: 44.0, height: 44.0) static let gravatarSize = CGSize(width: 28.0, height: 28.0) - static let multipleUsersGravatarSize = CGSize(width: 20.0, height: 20.0) - static let stackViewSpacing: CGFloat = 7.0 - static let chevronVerticalPadding: CGFloat = 2.0 static let leadingPadding: CGFloat = 12.0 } } @@ -125,21 +94,3 @@ extension AuthorFilterButton: Accessible { accessibilityValue = filterType.accessibilityValue } } - -private extension UIImage { - // Draws a small down facing arrow - static func makeChevronDownImage(with color: UIColor, size: CGSize) -> UIImage { - let renderer = UIGraphicsImageRenderer(size: size) - let image = renderer.image { context in - color.setFill() - color.setStroke() - let path = UIBezierPath() - path.move(to: .zero) - path.addLine(to: CGPoint(x: size.width, y: 0)) - path.addLine(to: CGPoint(x: size.width / 2, y: size.height)) - path.close() - path.fill() - } - return image - } -} diff --git a/WordPress/Classes/ViewRelated/Post/AuthorFilterViewController.swift b/WordPress/Classes/ViewRelated/Post/AuthorFilterViewController.swift index e8182a810192..f352c20191c6 100644 --- a/WordPress/Classes/ViewRelated/Post/AuthorFilterViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/AuthorFilterViewController.swift @@ -31,13 +31,17 @@ class AuthorFilterViewController: UITableViewController { AuthorFilter.everyone ] + private let postType: PostServiceType + init(initialSelection: PostListFilterSettings.AuthorFilter, gravatarEmail: String? = nil, + postType: PostServiceType, onSelectionChanged: ((PostListFilterSettings.AuthorFilter) -> Void)? = nil) { self.gravatarEmail = gravatarEmail self.onSelectionChanged = onSelectionChanged self.currentSelection = initialSelection + self.postType = postType super.init(style: .grouped) @@ -74,27 +78,39 @@ class AuthorFilterViewController: UITableViewController { } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: Identifiers.authorFilterCell, for: indexPath) - + let cell = tableView.dequeueReusableCell(withIdentifier: Identifiers.authorFilterCell, for: indexPath) as! AuthorFilterCell - if let cell = cell as? AuthorFilterCell, - let filter = PostListFilterSettings.AuthorFilter(rawValue: UInt(indexPath.row)) { + if let filter = PostListFilterSettings.AuthorFilter(rawValue: UInt(indexPath.row)) { switch filter { case .everyone: cell.filterType = .everyone case .mine: cell.filterType = .user(gravatarEmail: gravatarEmail) } - - cell.accessoryType = (filter == currentSelection) ? .checkmark : .none - - cell.title = filter.stringValue + cell.title = makeTitle(for: filter) cell.separatorIsHidden = indexPath.row != 0 } return cell } + private func makeTitle(for filter: PostListFilterSettings.AuthorFilter) -> String { + switch postType { + case .post: + switch filter { + case .mine: return Strings.postsByMe + case .everyone: return Strings.postsByEveryone + } + case .page: + switch filter { + case .mine: return Strings.pagesByMe + case .everyone: return Strings.pagesByEveryone + } + default: + fatalError("Unsupported post type: \(postType)") + } + } + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { guard let filter = PostListFilterSettings.AuthorFilter(rawValue: UInt(indexPath.row)) else { return @@ -149,13 +165,11 @@ class AuthorFilterViewController: UITableViewController { /// an optional gravatar in a circular image view. /// private class AuthorFilterCell: UITableViewCell { - private let gravatarImageView: CircularImageView = { let imageView = CircularImageView() imageView.translatesAutoresizingMaskIntoConstraints = false imageView.setContentHuggingPriority(.required, for: .horizontal) imageView.setContentCompressionResistancePriority(.required, for: .horizontal) - imageView.backgroundColor = Appearance.placeholderBackgroundColor imageView.tintColor = Appearance.placeholderTintColor return imageView }() @@ -164,7 +178,7 @@ private class AuthorFilterCell: UITableViewCell { let titleLabel = UILabel() titleLabel.translatesAutoresizingMaskIntoConstraints = false titleLabel.font = Fonts.titleFont - titleLabel.textColor = Appearance.textColor + titleLabel.textColor = .label return titleLabel }() @@ -199,26 +213,26 @@ private class AuthorFilterCell: UITableViewCell { override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) - addSubview(stackView) - addSubview(separator) + contentView.addSubview(stackView) + contentView.addSubview(separator) NSLayoutConstraint.activate([ - stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Metrics.horizontalPadding), - stackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -Metrics.horizontalPadding), - stackView.topAnchor.constraint(equalTo: topAnchor), - stackView.bottomAnchor.constraint(equalTo: bottomAnchor), + stackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: Metrics.horizontalPadding), + stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -Metrics.horizontalPadding), + stackView.topAnchor.constraint(equalTo: contentView.topAnchor), + stackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), gravatarImageView.widthAnchor.constraint(equalToConstant: Metrics.gravatarSize.width), gravatarImageView.heightAnchor.constraint(equalToConstant: Metrics.gravatarSize.height), - ]) + ]) - stackView.addArrangedSubview(gravatarImageView) stackView.addArrangedSubview(titleLabel) + stackView.addArrangedSubview(gravatarImageView) NSLayoutConstraint.activate([ - separator.leadingAnchor.constraint(equalTo: leadingAnchor), - separator.trailingAnchor.constraint(equalTo: trailingAnchor), - separator.bottomAnchor.constraint(equalTo: bottomAnchor), + separator.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + separator.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + separator.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), separator.heightAnchor.constraint(equalToConstant: .hairlineBorderWidth) - ]) + ]) tintColor = .primary(.shade40) } @@ -231,15 +245,15 @@ private class AuthorFilterCell: UITableViewCell { didSet { switch filterType { case .everyone: - gravatarImageView.image = .gridicon(.multipleUsers, size: Metrics.multipleGravatarSize) + gravatarImageView.image = UIImage(named: "icon-people") gravatarImageView.contentMode = .center accessibilityHint = NSLocalizedString("Select to show everyone's posts.", comment: "Voiceover accessibility hint, informing the user they can select an item to show posts written by all users on the site") case .user(let email): gravatarImageView.contentMode = .scaleAspectFill - let placeholder = UIImage.gridicon(.user, size: Metrics.gravatarSize) + let placeholder = UIImage(named: "comment-author-gravatar") if let email = email { - gravatarImageView.downloadGravatarWithEmail(email, placeholderImage: placeholder) + gravatarImageView.downloadGravatarWithEmail(email, placeholderImage: placeholder ?? UIImage()) } else { gravatarImageView.image = placeholder } @@ -252,19 +266,23 @@ private class AuthorFilterCell: UITableViewCell { // MARK: - Constants private enum Fonts { - static let titleFont = UIFont.systemFont(ofSize: 16.0) + static let titleFont = UIFont.systemFont(ofSize: 17) } private enum Appearance { - static let textColor = UIColor.neutral(.shade70) static let placeholderTintColor = UIColor.neutral(.shade70) - static let placeholderBackgroundColor = UIColor.neutral(.shade10) } private enum Metrics { - static let stackViewSpacing: CGFloat = 10.0 - static let horizontalPadding: CGFloat = 16.0 - static let gravatarSize = CGSize(width: 28.0, height: 28.0) - static let multipleGravatarSize = CGSize(width: 20.0, height: 20.0) + static let stackViewSpacing: CGFloat = 10 + static let horizontalPadding: CGFloat = 16 + static let gravatarSize = CGSize(width: 24, height: 24) } } + +private enum Strings { + static let postsByMe = NSLocalizedString("postsList.postsByMe", value: "Posts by me", comment: "Menu option for filtering posts by me") + static let postsByEveryone = NSLocalizedString("postsList.postsByEveryone", value: "Posts by everyone", comment: "Menu option for filtering posts by everyone") + static let pagesByMe = NSLocalizedString("pagesList.pagesByMe", value: "Pages by me", comment: "Menu option for filtering posts by me") + static let pagesByEveryone = NSLocalizedString("pagesList.pagesByEveryone", value: "Pages by everyone", comment: "Menu option for filtering posts by everyone") +} diff --git a/WordPress/Classes/ViewRelated/Post/Categories/PostCategoriesViewController.swift b/WordPress/Classes/ViewRelated/Post/Categories/PostCategoriesViewController.swift index d8965f0ca5f5..947d6edf4bad 100644 --- a/WordPress/Classes/ViewRelated/Post/Categories/PostCategoriesViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Categories/PostCategoriesViewController.swift @@ -25,8 +25,6 @@ import Foundation private var categoryIndentationDict = [Int: Int]() private var selectedCategories = [PostCategory]() - private var saveButtonItem: UIBarButtonItem? - private var hasSyncedCategories = false @objc init(blog: Blog, currentSelection: [PostCategory]?, selectionMode: CategoriesSelectionMode) { diff --git a/WordPress/Classes/ViewRelated/Post/ConfigurablePostView.h b/WordPress/Classes/ViewRelated/Post/ConfigurablePostView.h deleted file mode 100644 index 14afdc5fd250..000000000000 --- a/WordPress/Classes/ViewRelated/Post/ConfigurablePostView.h +++ /dev/null @@ -1,16 +0,0 @@ -#import - -@class Post; - -/// Protocol that any view representing a post can implement. -/// -@protocol ConfigurablePostView - -/// When called, the view should start representing the specified post object. -/// -/// - Parameters: -/// - post: the post to visually represent. -/// -- (void)configureWithPost:(nonnull Post*)post; - -@end diff --git a/WordPress/Classes/ViewRelated/Post/InteractivePostView.swift b/WordPress/Classes/ViewRelated/Post/InteractivePostView.swift deleted file mode 100644 index 4ba9cada6d06..000000000000 --- a/WordPress/Classes/ViewRelated/Post/InteractivePostView.swift +++ /dev/null @@ -1,6 +0,0 @@ -import Foundation - -protocol InteractivePostView { - func setInteractionDelegate(_ delegate: InteractivePostViewDelegate) - func setActionSheetDelegate(_ delegate: PostActionSheetDelegate) -} diff --git a/WordPress/Classes/ViewRelated/Post/InteractivePostViewDelegate.swift b/WordPress/Classes/ViewRelated/Post/InteractivePostViewDelegate.swift index 6e169d6f0da4..ec4a2ac0113c 100644 --- a/WordPress/Classes/ViewRelated/Post/InteractivePostViewDelegate.swift +++ b/WordPress/Classes/ViewRelated/Post/InteractivePostViewDelegate.swift @@ -6,12 +6,27 @@ protocol InteractivePostViewDelegate: AnyObject { func stats(for post: AbstractPost) func duplicate(_ post: AbstractPost) func publish(_ post: AbstractPost) - func trash(_ post: AbstractPost) - func restore(_ post: AbstractPost) + func trash(_ post: AbstractPost, completion: @escaping () -> Void) func draft(_ post: AbstractPost) func retry(_ post: AbstractPost) func cancelAutoUpload(_ post: AbstractPost) func share(_ post: AbstractPost, fromView view: UIView) - func copyLink(_ post: AbstractPost) func blaze(_ post: AbstractPost) + func comments(_ post: AbstractPost) + func showSettings(for post: AbstractPost) + func setParent(for post: AbstractPost) + func setHomepage(for post: AbstractPost) + func setPostsPage(for post: AbstractPost) + func setRegularPage(for post: AbstractPost) +} + +extension InteractivePostViewDelegate { + func setParent(for post: AbstractPost) {} + func setHomepage(for post: AbstractPost) {} + func setPostsPage(for post: AbstractPost) {} + func setRegularPage(for post: AbstractPost) {} + + func trash(_ post: AbstractPost) { + self.trash(post, completion: {}) + } } diff --git a/WordPress/Classes/ViewRelated/Post/LoadMoreCounter.swift b/WordPress/Classes/ViewRelated/Post/LoadMoreCounter.swift deleted file mode 100644 index 31f00dcd95a0..000000000000 --- a/WordPress/Classes/ViewRelated/Post/LoadMoreCounter.swift +++ /dev/null @@ -1,47 +0,0 @@ -import Foundation - -/// Temporary counter to help investigate an old issue with load more analytics being spammed. -/// See https://github.com/wordpress-mobile/WordPress-iOS/issues/6819 -class LoadMoreCounter { - - private(set) var count: Int = 0 - private var dryRun = false - - /// Initializer - /// - /// Parameters: - /// - startingCount - Optionally set the starting number for the counter. Default is 0. - /// - dryRun - pass true to avoid dispatching tracks events. Useful for testing. Default is false. - /// - init(startingCount: Int = 0, dryRun: Bool = false) { - self.count = startingCount - self.dryRun = dryRun - } - - /// Increments the counter. - /// Returns true if Analytics were bumped. False otherwise. - /// - @discardableResult - func increment(properties: [String: AnyObject]) -> Bool { - count += 1 - - // For thresholds use the following: - // 1: Baseline. Confirms we're bumping the stat and lets us roughly calculate uniques. - // 100: Its unlikely this should happen in a normal session. Bears more investigation. - // 1000: We should never see this many in a normal session. Something is probably broken. - // 10000: Ditto - let benchmarks = [1, 100, 1000, 10000] - - guard benchmarks.contains(count) else { - return false - } - - if !dryRun { - var props = properties - props["count"] = count as AnyObject - WPAnalytics.track(.postListExcessiveLoadMoreDetected, withProperties: props) - } - - return true - } -} diff --git a/WordPress/Classes/ViewRelated/Post/PageMenuViewModel.swift b/WordPress/Classes/ViewRelated/Post/PageMenuViewModel.swift new file mode 100644 index 000000000000..407673651b13 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Post/PageMenuViewModel.swift @@ -0,0 +1,118 @@ +import Foundation + +final class PageMenuViewModel: AbstractPostMenuViewModel { + + private let page: Page + private let isSiteHomepage: Bool + private let isSitePostsPage: Bool + private let isJetpackFeaturesEnabled: Bool + private let isBlazeFlagEnabled: Bool + + var buttonSections: [AbstractPostButtonSection] { + [ + createPrimarySection(), + createSecondarySection(), + createBlazeSection(), + createSetPageAttributesSection(), + createTrashSection() + ] + } + + convenience init(page: Page) { + self.init(page: page, isSiteHomepage: page.isSiteHomepage, isSitePostsPage: page.isSitePostsPage) + } + + init( + page: Page, + isSiteHomepage: Bool, + isSitePostsPage: Bool, + isJetpackFeaturesEnabled: Bool = JetpackFeaturesRemovalCoordinator.jetpackFeaturesEnabled(), + isBlazeFlagEnabled: Bool = BlazeHelper.isBlazeFlagEnabled() + ) { + self.page = page + self.isSiteHomepage = isSiteHomepage + self.isSitePostsPage = isSitePostsPage + self.isJetpackFeaturesEnabled = isJetpackFeaturesEnabled + self.isBlazeFlagEnabled = isBlazeFlagEnabled + } + + private func createPrimarySection() -> AbstractPostButtonSection { + var buttons = [AbstractPostButton]() + + if !page.isFailed && page.status != .trash { + buttons.append(.view) + } + + return AbstractPostButtonSection(buttons: buttons) + } + + private func createSecondarySection() -> AbstractPostButtonSection { + var buttons = [AbstractPostButton]() + + if page.status != .draft && !isSiteHomepage { + buttons.append(.moveToDraft) + } + + if page.status == .publish || page.status == .draft { + buttons.append(.duplicate) + } + + if page.status != .trash && page.isFailed { + buttons.append(.retry) + } + + if !page.isFailed, page.status != .publish && page.status != .trash { + buttons.append(.publish) + } + + return AbstractPostButtonSection(buttons: buttons) + } + + private func createBlazeSection() -> AbstractPostButtonSection { + var buttons = [AbstractPostButton]() + + if isBlazeFlagEnabled && page.canBlaze { + BlazeEventsTracker.trackEntryPointDisplayed(for: .pagesList) + buttons.append(.blaze) + } + + return AbstractPostButtonSection(buttons: buttons) + } + + private func createSetPageAttributesSection() -> AbstractPostButtonSection { + var buttons = [AbstractPostButton]() + + guard page.status != .trash else { + return AbstractPostButtonSection(buttons: buttons) + } + + buttons.append(.setParent) + + guard page.status == .publish else { + return AbstractPostButtonSection(buttons: buttons) + } + + if !isSiteHomepage { + buttons.append(.setHomepage) + } + + if !isSitePostsPage { + buttons.append(.setPostsPage) + } else { + buttons.append(.setRegularPage) + } + if page.status != .trash { + buttons.append(.settings) + } + + return AbstractPostButtonSection(buttons: buttons, submenuButton: .pageAttributes) + } + + private func createTrashSection() -> AbstractPostButtonSection { + guard !isSiteHomepage else { + return AbstractPostButtonSection(buttons: []) + } + + return AbstractPostButtonSection(buttons: [.trash]) + } +} diff --git a/WordPress/Classes/ViewRelated/Post/PostActionSheet.swift b/WordPress/Classes/ViewRelated/Post/PostActionSheet.swift deleted file mode 100644 index cc214f729202..000000000000 --- a/WordPress/Classes/ViewRelated/Post/PostActionSheet.swift +++ /dev/null @@ -1,118 +0,0 @@ -import Foundation -import AutomatticTracks - -protocol PostActionSheetDelegate: AnyObject { - func showActionSheet(_ postCardStatusViewModel: PostCardStatusViewModel, from view: UIView) -} - -class PostActionSheet { - - weak var viewController: UIViewController? - weak var interactivePostViewDelegate: InteractivePostViewDelegate? - - init(viewController: UIViewController, interactivePostViewDelegate: InteractivePostViewDelegate) { - self.viewController = viewController - self.interactivePostViewDelegate = interactivePostViewDelegate - } - - func show(for postCardStatusViewModel: PostCardStatusViewModel, from view: UIView, isCompactOrSearching: Bool = false) { - let unsupportedButtons: [PostCardStatusViewModel.Button] = [.edit, .more] - let post = postCardStatusViewModel.post - - let buttons: [PostCardStatusViewModel.Button] = { - let groups = postCardStatusViewModel.buttonGroups - if isCompactOrSearching { - return groups.primary + groups.secondary - } else { - return groups.secondary - } - }() - - let actionSheetController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) - actionSheetController.addCancelActionWithTitle(Titles.cancel) - - buttons - .filter { !unsupportedButtons.contains($0) } - .forEach { button in - switch button { - case .view: - actionSheetController.addDefaultActionWithTitle(Titles.view) { [weak self] _ in - self?.interactivePostViewDelegate?.view(post) - } - case .stats: - actionSheetController.addDefaultActionWithTitle(Titles.stats) { [weak self] _ in - self?.interactivePostViewDelegate?.stats(for: post) - } - case .duplicate: - actionSheetController.addDefaultActionWithTitle(Titles.duplicate) { [weak self] _ in - self?.interactivePostViewDelegate?.duplicate(post) - } - case .publish: - actionSheetController.addDefaultActionWithTitle(Titles.publish) { [weak self] _ in - self?.interactivePostViewDelegate?.publish(post) - } - case .moveToDraft: - actionSheetController.addDefaultActionWithTitle(Titles.draft) { [weak self] _ in - self?.interactivePostViewDelegate?.draft(post) - } - case .trash: - let destructiveTitle = post.status == .trash ? Titles.delete : Titles.trash - actionSheetController.addDestructiveActionWithTitle(destructiveTitle) { [weak self] _ in - self?.interactivePostViewDelegate?.trash(post) - } - case .cancelAutoUpload: - actionSheetController.addDefaultActionWithTitle(Titles.cancelAutoUpload) { [weak self] _ in - self?.interactivePostViewDelegate?.cancelAutoUpload(post) - } - case .retry: - actionSheetController.addDefaultActionWithTitle(Titles.retry) { [weak self] _ in - self?.interactivePostViewDelegate?.retry(post) - } - case .edit: - actionSheetController.addDefaultActionWithTitle(Titles.edit) { [weak self] _ in - self?.interactivePostViewDelegate?.edit(post) - } - case .share: - actionSheetController.addDefaultActionWithTitle(Titles.share) { [weak self] _ in - self?.interactivePostViewDelegate?.share(post, fromView: view) - } - case .blaze: - BlazeEventsTracker.trackEntryPointDisplayed(for: .postsList) - actionSheetController.addDefaultActionWithTitle(Titles.blaze) { [weak self] _ in - self?.interactivePostViewDelegate?.blaze(post) - } - case .more: - WordPressAppDelegate.crashLogging?.logMessage("Cannot handle unexpected button for post action sheet: \(button). This is a configuration error.", level: .error) - case .copyLink: - actionSheetController.addDefaultActionWithTitle(Titles.copyLink) { [weak self] _ in - self?.interactivePostViewDelegate?.copyLink(post) - } - } - } - - if let presentationController = actionSheetController.popoverPresentationController { - presentationController.permittedArrowDirections = .any - presentationController.sourceView = view - presentationController.sourceRect = view.bounds - } - - viewController?.present(actionSheetController, animated: true) - } - - struct Titles { - static let cancel = NSLocalizedString("Cancel", comment: "Dismiss the post action sheet") - static let cancelAutoUpload = NSLocalizedString("Cancel Upload", comment: "Label for the Post List option that cancels automatic uploading of a post.") - static let stats = NSLocalizedString("Stats", comment: "Label for post stats option. Tapping displays statistics for a post.") - static let duplicate = NSLocalizedString("Duplicate", comment: "Label for post duplicate option. Tapping creates a copy of the post.") - static let publish = NSLocalizedString("Publish Now", comment: "Label for an option that moves a publishes a post immediately") - static let draft = NSLocalizedString("Move to Draft", comment: "Label for an option that moves a post to the draft folder") - static let delete = NSLocalizedString("Delete Permanently", comment: "Label for the delete post option. Tapping permanently deletes a post.") - static let trash = NSLocalizedString("Move to Trash", comment: "Label for a option that moves a post to the trash folder") - static let view = NSLocalizedString("View", comment: "Label for the view post button. Tapping displays the post as it appears on the web.") - static let retry = NSLocalizedString("Retry", comment: "Retry uploading the post.") - static let edit = NSLocalizedString("Edit", comment: "Edit the post.") - static let share = NSLocalizedString("Share", comment: "Share the post.") - static let blaze = NSLocalizedString("posts.blaze.actionTitle", value: "Promote with Blaze", comment: "Promote the post with Blaze.") - static let copyLink = NSLocalizedString("Copy Link", comment: "Copy the post url and paste anywhere in phone") - } -} diff --git a/WordPress/Classes/ViewRelated/Post/PostCardCell.swift b/WordPress/Classes/ViewRelated/Post/PostCardCell.swift deleted file mode 100644 index e8224fddf831..000000000000 --- a/WordPress/Classes/ViewRelated/Post/PostCardCell.swift +++ /dev/null @@ -1,484 +0,0 @@ -import AutomatticTracks -import UIKit -import Gridicons - -class PostCardCell: UITableViewCell, ConfigurablePostView { - @IBOutlet weak var featuredImageStackView: UIStackView! - @IBOutlet weak var featuredImage: CachedAnimatedImageView! - @IBOutlet weak var featuredImageHeight: NSLayoutConstraint! - @IBOutlet weak var titleLabel: UILabel! - @IBOutlet weak var snippetLabel: UILabel! - @IBOutlet weak var dateLabel: UILabel! - @IBOutlet weak var authorLabel: UILabel! - @IBOutlet weak var separatorLabel: UILabel! - @IBOutlet weak var statusLabel: UILabel! - @IBOutlet weak var statusView: UIStackView! - @IBOutlet weak var progressView: UIProgressView! - @IBOutlet weak var editButton: UIButton! - @IBOutlet weak var retryButton: UIButton! - @IBOutlet weak var cancelAutoUploadButton: UIButton! - @IBOutlet weak var publishButton: UIButton! - @IBOutlet weak var viewButton: UIButton! - @IBOutlet weak var trashButton: UIButton! - @IBOutlet weak var moreButton: UIButton! - @IBOutlet weak var actionBarView: UIStackView! - @IBOutlet weak var containerView: UIView! - @IBOutlet weak var upperBorder: UIView! - @IBOutlet weak var bottomBorder: UIView! - @IBOutlet weak var actionBarSeparator: UIView! - @IBOutlet weak var topPadding: NSLayoutConstraint! - @IBOutlet weak var contentStackView: UIStackView! - @IBOutlet weak var ghostStackView: UIStackView! - @IBOutlet weak var ghostHolder: UIView! - - lazy var imageLoader: ImageLoader = { - return ImageLoader(imageView: featuredImage, gifStrategy: .mediumGIFs) - }() - - private var post: Post? - private var viewModel: PostCardStatusViewModel? - private var currentLoadedFeaturedImage: String? - private weak var interactivePostViewDelegate: InteractivePostViewDelegate? - private weak var actionSheetDelegate: PostActionSheetDelegate? - var shouldHideAuthor: Bool = false { - didSet { - let emptyAuthor = viewModel?.author.isEmpty ?? true - - authorLabel.isHidden = shouldHideAuthor || emptyAuthor - separatorLabel.isHidden = shouldHideAuthor || emptyAuthor - } - } - - func configure(with post: Post) { - assert(post.managedObjectContext != nil) - - if post != self.post { - viewModel = PostCardStatusViewModel(post: post) - } - - self.post = post - - resetGhost() - configureFeaturedImage() - configureTitle() - configureSnippet() - configureDate() - configureAuthor() - configureStatusLabel() - configureProgressView() - configureActionBar() - configureAccessibility() - configureActionBarInteraction() - } - - override func awakeFromNib() { - super.awakeFromNib() - - applyStyles() - adjustInsetsForTextDirection() - } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - if previousTraitCollection?.preferredContentSizeCategory != traitCollection.preferredContentSizeCategory { - applyStyles() - } - } - - override func prepareForReuse() { - super.prepareForReuse() - imageLoader.prepareForReuse() - setNeedsDisplay() - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - // Don't respond to taps in margins. - if !containerView.frame.contains(point) { - return nil - } - return super.hitTest(point, with: event) - } - - @IBAction func edit() { - guard let post = post else { - return - } - - interactivePostViewDelegate?.edit(post) - } - - @IBAction func view() { - guard let post = post else { - return - } - - interactivePostViewDelegate?.view(post) - } - - @IBAction func more(_ sender: Any) { - guard let button = sender as? UIButton, let viewModel = viewModel else { - return - } - - actionSheetDelegate?.showActionSheet(viewModel, from: button) - } - - @IBAction func retry() { - guard let post = post else { - return - } - - interactivePostViewDelegate?.retry(post) - } - - @IBAction func cancelAutoUpload() { - if let post = post { - interactivePostViewDelegate?.cancelAutoUpload(post) - } - } - - @IBAction func publish() { - if let post = post { - interactivePostViewDelegate?.publish(post) - } - } - - @IBAction func trash() { - if let post = post { - interactivePostViewDelegate?.trash(post) - } - } - - private func applyStyles() { - WPStyleGuide.applyPostCardStyle(self) - WPStyleGuide.applyPostTitleStyle(titleLabel) - WPStyleGuide.applyPostSnippetStyle(snippetLabel) - WPStyleGuide.applyPostDateStyle(dateLabel) - WPStyleGuide.applyPostDateStyle(separatorLabel) - WPStyleGuide.applyPostDateStyle(authorLabel) - WPStyleGuide.configureLabel(statusLabel, textStyle: UIFont.TextStyle.subheadline) - WPStyleGuide.applyPostProgressViewStyle(progressView) - WPStyleGuide.applyPostButtonStyle(editButton) - WPStyleGuide.applyPostButtonStyle(retryButton) - WPStyleGuide.applyPostButtonStyle(viewButton) - WPStyleGuide.applyPostButtonStyle(moreButton) - WPStyleGuide.applyPostButtonStyle(cancelAutoUploadButton) - WPStyleGuide.applyPostButtonStyle(publishButton) - WPStyleGuide.applyPostButtonStyle(trashButton) - - setupActionBar() - setupFeaturedImage() - setupBorders() - setupBackgrounds() - setupLabels() - setupSeparatorLabel() - setupSelectedBackgroundView() - setupReadableGuideForiPad() - } - - private func setupFeaturedImage() { - featuredImageHeight.constant = Constants.featuredImageHeightConstant - } - - private func resetGhost() { - isUserInteractionEnabled = true - actionBarView.layer.opacity = Constants.actionBarOpacity - toggleGhost(visible: false) - } - - private func configureFeaturedImage() { - guard let post = post?.latest() else { - return - } - - if let url = post.featuredImageURL, - let desiredWidth = UIApplication.shared.mainWindow?.frame.size.width { - featuredImageStackView.isHidden = false - topPadding.constant = Constants.margin - loadFeaturedImageIfNeeded(url, preferredSize: CGSize(width: desiredWidth, height: featuredImage.frame.height)) - } else { - featuredImageStackView.isHidden = true - topPadding.constant = Constants.paddingWithoutImage - } - } - - private func loadFeaturedImageIfNeeded(_ url: URL, preferredSize: CGSize) { - guard let post = post else { - return - } - - let host = MediaHost(with: post) { error in - // We'll log the error, so we know it's there, but we won't halt execution. - WordPressAppDelegate.crashLogging?.logError(error) - } - - if currentLoadedFeaturedImage != url.absoluteString { - currentLoadedFeaturedImage = url.absoluteString - imageLoader.loadImage(with: url, from: host, preferredSize: preferredSize) - } - } - - private func configureTitle() { - guard let post = post?.latest() else { - return - } - - if let titleForDisplay = post.titleForDisplay() { - WPStyleGuide.applyPostTitleStyle(titleForDisplay, into: titleLabel) - } - - self.accessibilityIdentifier = post.slugForDisplay() - } - - private func configureSnippet() { - guard let post = post?.latest() else { - return - } - - if let contentPreviewForDisplay = post.contentPreviewForDisplay(), - !contentPreviewForDisplay.isEmpty { - WPStyleGuide.applyPostSnippetStyle(contentPreviewForDisplay, into: snippetLabel) - snippetLabel.isHidden = false - } else { - snippetLabel.isHidden = true - } - } - - private func configureDate() { - guard let post = post?.latest() else { - return - } - - dateLabel.text = post.displayDate() - } - - private func configureAuthor() { - guard let viewModel = viewModel else { - return - } - - authorLabel.text = viewModel.author - } - - private func configureStatusLabel() { - guard let viewModel = viewModel else { - return - } - - let status = viewModel.statusAndBadges(separatedBy: Constants.separator) - statusLabel.textColor = viewModel.statusColor - statusLabel.text = status - statusView.isHidden = status.isEmpty - } - - private func configureProgressView() { - guard let viewModel = viewModel else { - return - } - - let shouldHide = viewModel.shouldHideProgressView - - progressView.isHidden = shouldHide - - progressView.progress = viewModel.progress - - if !shouldHide && viewModel.progressBlock == nil { - viewModel.progressBlock = { [weak self] progress in - self?.progressView.setProgress(progress, animated: true) - if progress >= 1.0, let post = self?.post { - self?.configure(with: post) - } - } - } - } - - private func configureActionBar() { - guard let viewModel = viewModel else { - return - } - - // Convert to Set for O(1) complexity of contains() - let primaryButtons = Set(viewModel.buttonGroups.primary) - - editButton.isHidden = !primaryButtons.contains(.edit) - retryButton.isHidden = !primaryButtons.contains(.retry) - cancelAutoUploadButton.isHidden = !primaryButtons.contains(.cancelAutoUpload) - publishButton.isHidden = !primaryButtons.contains(.publish) - viewButton.isHidden = !primaryButtons.contains(.view) - moreButton.isHidden = !primaryButtons.contains(.more) - trashButton.isHidden = !primaryButtons.contains(.trash) - } - - private func configureAccessibility() { - guard let viewModel = viewModel else { - accessibilityLabel = nil - return - } - - let post = viewModel.post - - let titleAndDateChunk: String = { - let format = NSLocalizedString("%@, %@.", comment: "Accessibility label for a post in the post list." + - " The parameters are the title, and date respectively." + - " For example, \"Let it Go, 1 hour ago.\"") - return String(format: format, post.titleForDisplay(), post.dateStringForDisplay()) - }() - - let authorChunk: String? = { - let author = viewModel.author - guard !author.isEmpty else { - return nil - } - let format = NSLocalizedString("By %@.", comment: "Accessibility label for the post author in the post list." + - " The parameter is the author name. For example, \"By Elsa.\"") - return String(format: format, author) - }() - - let stickyChunk = - post.isStickyPost ? NSLocalizedString("Sticky.", comment: "Accessibility label for a sticky post in the post list.") : nil - - let statusChunk: String? = { - guard let status = viewModel.status else { - return nil - } - - return "\(status)." - }() - - let excerptChunk: String? = { - let excerpt = post.contentPreviewForDisplay() - guard !excerpt.isEmpty else { - return nil - } - - let format = NSLocalizedString("Excerpt. %@.", comment: "Accessibility label for a post's excerpt in the post list." + - " The parameter is the post excerpt. For example, \"Excerpt. This is the first paragraph.\"") - return String(format: format, excerpt) - }() - - accessibilityLabel = [titleAndDateChunk, authorChunk, stickyChunk, statusChunk, excerptChunk] - .compactMap { $0 } - .joined(separator: " ") - } - - private func configureActionBarInteraction() { - guard let viewModel = viewModel else { - return - } - - let isProgressBarVisible = !viewModel.shouldHideProgressView - - if isProgressBarVisible { - actionBarView.isUserInteractionEnabled = false - actionBarView.alpha = 0.3 - } else { - actionBarView.isUserInteractionEnabled = true - actionBarView.alpha = 1.0 - } - } - - private func setupBorders() { - WPStyleGuide.applyBorderStyle(upperBorder) - WPStyleGuide.applyBorderStyle(bottomBorder) - WPStyleGuide.applyBorderStyle(actionBarSeparator) - } - - private func setupBackgrounds() { - containerView.backgroundColor = .listForeground - titleLabel.backgroundColor = .listForeground - snippetLabel.backgroundColor = .listForeground - dateLabel.backgroundColor = .listForeground - authorLabel.backgroundColor = .listForeground - separatorLabel.backgroundColor = .listForeground - ghostHolder.backgroundColor = .listForeground - } - - private func setupActionBar() { - actionBarView.subviews.compactMap({ $0 as? UIButton }).forEach { button in - WPStyleGuide.applyActionBarButtonStyle(button) - } - } - - private func setupLabels() { - retryButton.setTitle(NSLocalizedString("Retry", comment: "Label for the retry post upload button. Tapping attempts to upload the post again."), for: .normal) - retryButton.setImage(.gridicon(.refresh, size: CGSize(width: 18, height: 18)), for: .normal) - - cancelAutoUploadButton.setTitle(NSLocalizedString("Cancel", comment: "Label for the auto-upload cancelation button in the post list. Tapping will prevent the app from auto-uploading the post."), - for: .normal) - - editButton.setTitle(NSLocalizedString("Edit", comment: "Label for the edit post button. Tapping displays the editor."), for: .normal) - - viewButton.setTitle(NSLocalizedString("View", comment: "Label for the view post button. Tapping displays the post as it appears on the web."), for: .normal) - - moreButton.setTitle(NSLocalizedString("More", comment: "Label for the more post button. Tapping displays an action sheet with post options."), for: .normal) - } - - private func setupSelectedBackgroundView() { - if let selectedBackgroundView = selectedBackgroundView { - WPStyleGuide.insertSelectedBackgroundSubview(selectedBackgroundView, topMargin: Constants.margin) - } - } - - private func setupReadableGuideForiPad() { - guard WPDeviceIdentification.isiPad() else { return } - - contentStackView.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor).isActive = true - contentStackView.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor).isActive = true - - contentStackView.subviews.forEach { $0.changeLayoutMargins(left: 0, right: 0) } - } - - private func setupSeparatorLabel() { - separatorLabel.text = Constants.separator - } - - private func adjustInsetsForTextDirection() { - actionBarView.subviews.compactMap({ $0 as? UIButton }).forEach { - $0.flipInsetsForRightToLeftLayoutDirection() - } - } - - private enum Constants { - static let separator = " · " - static let margin: CGFloat = 16 - static let paddingWithoutImage: CGFloat = 8 - static let featuredImageHeightConstant: CGFloat = WPDeviceIdentification.isiPad() ? 226 : 100 - static let actionBarOpacity: Float = 1 - } -} - -extension PostCardCell: InteractivePostView { - func setInteractionDelegate(_ delegate: InteractivePostViewDelegate) { - interactivePostViewDelegate = delegate - } - - func setActionSheetDelegate(_ delegate: PostActionSheetDelegate) { - actionSheetDelegate = delegate - } -} - -extension PostCardCell: GhostableView { - func ghostAnimationWillStart() { - progressView.isHidden = true - actionBarView.layer.opacity = GhostConstants.actionBarOpacity - isUserInteractionEnabled = false - - topPadding.constant = Constants.margin - - toggleGhost(visible: true) - - actionBarView.isGhostableDisabled = true - upperBorder.isGhostableDisabled = true - bottomBorder.isGhostableDisabled = true - } - - private func toggleGhost(visible: Bool) { - contentStackView.subviews.forEach { $0.isHidden = visible } - ghostStackView.isHidden = !visible - actionBarView.isHidden = false - } - - private enum GhostConstants { - static let actionBarOpacity: Float = 0.5 - } -} diff --git a/WordPress/Classes/ViewRelated/Post/PostCardCell.xib b/WordPress/Classes/ViewRelated/Post/PostCardCell.xib deleted file mode 100644 index 919b583fd874..000000000000 --- a/WordPress/Classes/ViewRelated/Post/PostCardCell.xib +++ /dev/null @@ -1,355 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/WordPress/Classes/ViewRelated/Post/PostCardStatusViewModel.swift b/WordPress/Classes/ViewRelated/Post/PostCardStatusViewModel.swift index 15c7a51bbad3..783a7dead13e 100644 --- a/WordPress/Classes/ViewRelated/Post/PostCardStatusViewModel.swift +++ b/WordPress/Classes/ViewRelated/Post/PostCardStatusViewModel.swift @@ -3,31 +3,7 @@ import Gridicons /// Encapsulates status display logic for PostCardTableViewCells. /// -class PostCardStatusViewModel: NSObject { - private static let maximumPrimaryButtons = 3 - - enum Button { - case edit - case retry - case view - case more - case publish - case stats - case duplicate - case moveToDraft - case trash - case cancelAutoUpload - case share - case copyLink - case blaze - } - - struct ButtonGroups: Equatable { - /// The main buttons shown in the Post List - let primary: [Button] - /// Shown under the _More_ - let secondary: [Button] - } +class PostCardStatusViewModel: NSObject, AbstractPostMenuViewModel { let post: Post private var progressObserverUUID: UUID? = nil @@ -35,7 +11,7 @@ class PostCardStatusViewModel: NSObject { private let autoUploadInteractor = PostAutoUploadInteractor() private let isInternetReachable: Bool - + private let isJetpackFeaturesEnabled: Bool private let isBlazeFlagEnabled: Bool var progressBlock: ((Float) -> Void)? = nil { @@ -56,9 +32,11 @@ class PostCardStatusViewModel: NSObject { init(post: Post, isInternetReachable: Bool = ReachabilityUtils.isInternetReachable(), + isJetpackFeaturesEnabled: Bool = JetpackFeaturesRemovalCoordinator.jetpackFeaturesEnabled(), isBlazeFlagEnabled: Bool = BlazeHelper.isBlazeFlagEnabled()) { self.post = post self.isInternetReachable = isInternetReachable + self.isJetpackFeaturesEnabled = isJetpackFeaturesEnabled self.isBlazeFlagEnabled = isBlazeFlagEnabled super.init() } @@ -67,6 +45,8 @@ class PostCardStatusViewModel: NSObject { // TODO Move these string constants to the StatusMessages enum if MediaCoordinator.shared.isUploadingMedia(for: post) { return NSLocalizedString("Uploading media...", comment: "Message displayed on a post's card while the post is uploading media") + } else if PostCoordinator.shared.isDeleting(post) { + return post.status == .trash ? Strings.deletingPostPermanently : Strings.movingPostToTrash } else if post.isFailed { return generateFailedStatusMessage() } else if post.remoteStatus == .pushing { @@ -103,6 +83,10 @@ class PostCardStatusViewModel: NSObject { return .neutral(.shade30) } + if PostCoordinator.shared.isDeleting(post) { + return .systemRed + } + if post.isFailed && isInternetReachable { return .error } @@ -145,96 +129,86 @@ class PostCardStatusViewModel: NSObject { } /// Returns what buttons are visible - /// - /// The order matters here. For the primary buttons, we do not currently support dynamic - /// buttons in the UI. Technically, we may end up with situations where there are no buttons - /// visible. But we've carefully considered the possible situations so this does not happen. - /// - /// The order of the Buttons are important here, especially for the secondary buttons which - /// dictate what buttons are shown in the action sheet after pressing _More_. - var buttonGroups: ButtonGroups { - let maxPrimaryButtons = PostCardStatusViewModel.maximumPrimaryButtons + var buttonSections: [AbstractPostButtonSection] { + return [ + createPrimarySection(), + createSecondarySection(), + createBlazeSection(), + createNavigationSection(), + createTrashSection() + ] + } - let allButtons: [Button] = { - var buttons = [Button]() + private func createPrimarySection() -> AbstractPostButtonSection { + var buttons = [AbstractPostButton]() - buttons.append(.edit) + if !post.isFailed && post.status != .trash { + buttons.append(.view) + } - if !post.isFailed { - buttons.append(.view) - } + return AbstractPostButtonSection(buttons: buttons) + } - if autoUploadInteractor.canRetryUpload(of: post) { - buttons.append(.retry) - } + private func createSecondarySection() -> AbstractPostButtonSection { + var buttons = [AbstractPostButton]() - if post.isFailed && isInternetReachable { - buttons.append(.retry) - } + if post.status != .draft { + buttons.append(.moveToDraft) + } - if canCancelAutoUpload && !isInternetReachable { - buttons.append(.cancelAutoUpload) - } + if post.status == .publish || post.status == .draft { + buttons.append(.duplicate) + } - if autoUploadInteractor.autoUploadAttemptState(of: post) == .reachedLimit { - buttons.append(.retry) - } + if post.status == .publish && post.hasRemote() { + buttons.append(.share) + } - if canPublish { - buttons.append(.publish) - } + if autoUploadInteractor.canRetryUpload(of: post) || + autoUploadInteractor.autoUploadAttemptState(of: post) == .reachedLimit || + post.isFailed && isInternetReachable { + buttons.append(.retry) + } - if post.status == .publish && post.hasRemote() { - if JetpackFeaturesRemovalCoordinator.jetpackFeaturesEnabled() { - buttons.append(.stats) - } - buttons.append(.share) - } + if canCancelAutoUpload && !isInternetReachable { + buttons.append(.cancelAutoUpload) + } - if isBlazeFlagEnabled && post.canBlaze { - buttons.append(.blaze) - } + if canPublish { + buttons.append(.publish) + } - if post.status == .publish || post.status == .draft { - buttons.append(.duplicate) - } + return AbstractPostButtonSection(buttons: buttons) + } - if post.status != .draft { - buttons.append(.moveToDraft) - } + private func createBlazeSection() -> AbstractPostButtonSection { + var buttons = [AbstractPostButton]() - if post.status != .trash { - buttons.append(.copyLink) - } + if isBlazeFlagEnabled && post.canBlaze { + BlazeEventsTracker.trackEntryPointDisplayed(for: .postsList) + buttons.append(.blaze) + } - buttons.append(.trash) + return AbstractPostButtonSection(buttons: buttons) + } - return buttons - }() - // If allButtons is [one, two, three, four], set the primary to [one, two, “more”]. - // If allButtons is [one, two, three], set the primary to the same. - let primaryButtons: [Button] = { - if allButtons.count <= maxPrimaryButtons { - return allButtons - } + private func createNavigationSection() -> AbstractPostButtonSection { + var buttons = [AbstractPostButton]() - var primary = allButtons.prefix(maxPrimaryButtons - 1) - primary.append(.more) - return Array(primary) - }() - - // If allButtons is [one, two, three, four], set the secondary to [three, four]. - // If allButtons is [one, two, three], set the secondary to []. - let secondaryButtons: [Button] = { - if allButtons.count > maxPrimaryButtons { - return Array(allButtons.suffix(from: maxPrimaryButtons - 1)) - } else { - return [] - } - }() + if isJetpackFeaturesEnabled, post.status == .publish && post.hasRemote() { + buttons.append(contentsOf: [.stats, .comments]) + } + if post.status != .trash { + buttons.append(.settings) + } - return ButtonGroups(primary: primaryButtons, secondary: secondaryButtons) + return AbstractPostButtonSection(buttons: buttons) + } + + + private func createTrashSection() -> AbstractPostButtonSection { + return AbstractPostButtonSection(buttons: [.trash]) } private var canCancelAutoUpload: Bool { @@ -288,3 +262,8 @@ class PostCardStatusViewModel: NSObject { comment: "Message displayed on a post's card when the post has unsaved changes") } } + +private enum Strings { + static let movingPostToTrash = NSLocalizedString("post.movingToTrashStatusMessage", value: "Moving post to trash...", comment: "Status mesasge for post cells") + static let deletingPostPermanently = NSLocalizedString("post.deletingPostPermanentlyStatusMessage", value: "Deleting post...", comment: "Status mesasge for post cells") +} diff --git a/WordPress/Classes/ViewRelated/Post/PostCompactCell.swift b/WordPress/Classes/ViewRelated/Post/PostCompactCell.swift index 4377f3cfdd97..0d7975411d61 100644 --- a/WordPress/Classes/ViewRelated/Post/PostCompactCell.swift +++ b/WordPress/Classes/ViewRelated/Post/PostCompactCell.swift @@ -1,8 +1,9 @@ import AutomatticTracks import UIKit import Gridicons +import WordPressShared -class PostCompactCell: UITableViewCell, ConfigurablePostView { +class PostCompactCell: UITableViewCell { @IBOutlet weak var titleLabel: UILabel! @IBOutlet weak var timestampLabel: UILabel! @IBOutlet weak var badgesLabel: UILabel! @@ -20,8 +21,6 @@ class PostCompactCell: UITableViewCell, ConfigurablePostView { private var iPadReadableLeadingAnchor: NSLayoutConstraint? private var iPadReadableTrailingAnchor: NSLayoutConstraint? - private weak var actionSheetDelegate: PostActionSheetDelegate? - lazy var imageLoader: ImageLoader = { return ImageLoader(imageView: featuredImageView, gifStrategy: .mediumGIFs) }() @@ -51,11 +50,7 @@ class PostCompactCell: UITableViewCell, ConfigurablePostView { } @IBAction func more(_ sender: Any) { - guard let viewModel = viewModel, let button = sender as? UIButton else { - return - } - - actionSheetDelegate?.showActionSheet(viewModel, from: button) + // Do nothing. The compact cell is only shown in the dashboard, where the more button is hidden. } override func awakeFromNib() { @@ -203,24 +198,12 @@ class PostCompactCell: UITableViewCell, ConfigurablePostView { private enum Constants { static let separator = " · " - static let contentSpacing: CGFloat = 8 static let imageRadius: CGFloat = 2 - static let labelsVerticalAlignment: CGFloat = -1 static let opacity: Float = 1 static let margin: CGFloat = 16 } } -extension PostCompactCell: InteractivePostView { - func setInteractionDelegate(_ delegate: InteractivePostViewDelegate) { - // Do nothing, since this cell doesn't support actions in `InteractivePostViewDelegate`. - } - - func setActionSheetDelegate(_ delegate: PostActionSheetDelegate) { - actionSheetDelegate = delegate - } -} - extension PostCompactCell: GhostableView { func ghostAnimationWillStart() { toggleGhost(visible: true) @@ -262,10 +245,6 @@ extension PostCompactCell { } } - func hideSeparator() { - separator.isHidden = true - } - func disableiPadReadableMargin() { iPadReadableLeadingAnchor?.isActive = false iPadReadableTrailingAnchor?.isActive = false diff --git a/WordPress/Classes/ViewRelated/Post/PostEditor+Publish.swift b/WordPress/Classes/ViewRelated/Post/PostEditor+Publish.swift index 53f914b7f661..74244b582fab 100644 --- a/WordPress/Classes/ViewRelated/Post/PostEditor+Publish.swift +++ b/WordPress/Classes/ViewRelated/Post/PostEditor+Publish.swift @@ -212,17 +212,6 @@ extension PublishingEditor { present(alertController, animated: true, completion: nil) } - fileprivate func displayHasFailedMediaAlert(then: @escaping () -> ()) { - let alertController = UIAlertController(title: FailedMediaRemovalAlert.title, message: FailedMediaRemovalAlert.message, preferredStyle: .alert) - alertController.addDefaultActionWithTitle(FailedMediaRemovalAlert.acceptTitle) { [weak self] alertAction in - self?.removeFailedMedia() - then() - } - - alertController.addCancelActionWithTitle(FailedMediaRemovalAlert.cancelTitle) - present(alertController, animated: true, completion: nil) - } - /// If the user is publishing a post, displays the Prepublishing Nudges /// Otherwise, shows a confirmation Action Sheet. /// @@ -663,10 +652,3 @@ private struct PostUploadingAlert { static let message = NSLocalizedString("Your post is currently being uploaded. Please wait until this completes.", comment: "This is a notification the user receives if they are trying to preview a post before the upload process is complete.") static let acceptTitle = NSLocalizedString("OK", comment: "Accept Action") } - -private struct FailedMediaRemovalAlert { - static let title = NSLocalizedString("Uploads failed", comment: "Title for alert when trying to save post with failed media items") - static let message = NSLocalizedString("Some media uploads failed. This action will remove all failed media from the post.\nSave anyway?", comment: "Confirms with the user if they save the post all media that failed to upload will be removed from it.") - static let acceptTitle = NSLocalizedString("Yes", comment: "Accept Action") - static let cancelTitle = NSLocalizedString("Not Now", comment: "Nicer dialog answer for \"No\".") -} diff --git a/WordPress/Classes/ViewRelated/Post/PostEditor.swift b/WordPress/Classes/ViewRelated/Post/PostEditor.swift index 9659e966968d..9742f66a8956 100644 --- a/WordPress/Classes/ViewRelated/Post/PostEditor.swift +++ b/WordPress/Classes/ViewRelated/Post/PostEditor.swift @@ -80,9 +80,6 @@ protocol PostEditor: PublishingEditor, UIViewControllerTransitioningDelegate { /// Returns true if the site mode is on var isSingleSiteMode: Bool { get } - /// MediaLibraryPickerDataSource - var mediaLibraryDataSource: MediaLibraryPickerDataSource { get set } - /// Returns the media attachment removed version of html func contentByStrippingMediaAttachments() -> String diff --git a/WordPress/Classes/ViewRelated/Post/PostEditorNavigationBarManager.swift b/WordPress/Classes/ViewRelated/Post/PostEditorNavigationBarManager.swift index 3c75ff1d7edc..1c9af4ceb0d6 100644 --- a/WordPress/Classes/ViewRelated/Post/PostEditorNavigationBarManager.swift +++ b/WordPress/Classes/ViewRelated/Post/PostEditorNavigationBarManager.swift @@ -254,19 +254,9 @@ class PostEditorNavigationBarManager { } extension PostEditorNavigationBarManager { - private enum Constants { - static let closeButtonInsets = NSDirectionalEdgeInsets(top: 3, leading: 3, bottom: 3, trailing: 3) - static let closeButtonEdgeInsets = UIEdgeInsets(top: 3, left: 3, bottom: 3, right: 3) - } - private enum Fonts { - static let semiBold = WPFontManager.systemSemiBoldFont(ofSize: 16) static var blogTitle: UIFont { WPStyleGuide.navigationBarStandardFont } } - - private enum Assets { - static let closeButtonModalImage = UIImage.gridicon(.cross) - } } diff --git a/WordPress/Classes/ViewRelated/Post/PostEditorState.swift b/WordPress/Classes/ViewRelated/Post/PostEditorState.swift index 4765eac86ac0..982b526b0f90 100644 --- a/WordPress/Classes/ViewRelated/Post/PostEditorState.swift +++ b/WordPress/Classes/ViewRelated/Post/PostEditorState.swift @@ -115,10 +115,6 @@ public enum PostEditorAction { } } - fileprivate var isPostPostShown: Bool { - return false - } - fileprivate var secondaryPublishAction: PostEditorAction? { switch self { case .publish: @@ -359,29 +355,12 @@ public class PostEditorStateContext { return action.publishActionLabel } - var publishQuestionTitleText: String { - return action.publishingActionQuestionLabel - } - /// Returns the WPAnalyticsStat enum to be tracked when this post is published /// var publishActionAnalyticsStat: WPAnalyticsStat { return action.publishActionAnalyticsStat } - // TODO: Remove as dead code? - /// Indicates if the editor should be dismissed when the publish button is tapped - /// - var publishActionDismissesEditor: Bool { - return action != .update - } - - /// Should post-post be shown for the current editor when publishing has happened - /// - var isPostPostShown: Bool { - return action.isPostPostShown - } - /// Returns whether the secondary publish button should be displayed, or not /// var isSecondaryPublishButtonShown: Bool { @@ -451,11 +430,3 @@ fileprivate func isFutureDated(_ date: Date?) -> Bool { return comparison == .orderedAscending } - -fileprivate func isPastDated(_ date: Date?) -> Bool { - guard let date = date else { - return false - } - - return date < Date() -} diff --git a/WordPress/Classes/ViewRelated/Post/PostListCell.swift b/WordPress/Classes/ViewRelated/Post/PostListCell.swift new file mode 100644 index 000000000000..5d063493a170 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Post/PostListCell.swift @@ -0,0 +1,138 @@ +import Foundation +import UIKit + +final class PostListCell: UITableViewCell, PostSearchResultCell, Reusable { + var isEnabled = true + + // MARK: - Views + + private lazy var mainStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + return stackView + }() + + private lazy var contentStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .horizontal + return stackView + }() + + private let headerView = PostListHeaderView() + private let contentLabel = UILabel() + private let featuredImageView = CachedAnimatedImageView() + private let statusLabel = UILabel() + + // MARK: - Properties + + private lazy var imageLoader = ImageLoader(imageView: featuredImageView, loadingIndicator: SolidColorActivityIndicator()) + + // MARK: - PostSearchResultCell + + var attributedText: NSAttributedString? { + get { contentLabel.attributedText } + set { contentLabel.attributedText = newValue } + } + + // MARK: - Initializers + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setupViews() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Public + + override func prepareForReuse() { + super.prepareForReuse() + + imageLoader.prepareForReuse() + } + + func configure(with viewModel: PostListItemViewModel, delegate: InteractivePostViewDelegate? = nil) { + headerView.configure(with: viewModel, delegate: delegate) + contentLabel.attributedText = viewModel.content + + featuredImageView.isHidden = viewModel.imageURL == nil + if let imageURL = viewModel.imageURL { + let host = MediaHost(with: viewModel.post) { error in + WordPressAppDelegate.crashLogging?.logError(error) + } + imageLoader.loadImage(with: imageURL, from: host, preferredSize: Constants.imageSize) + } + + statusLabel.text = viewModel.status + statusLabel.textColor = viewModel.statusColor + statusLabel.isHidden = viewModel.status.isEmpty + + contentView.isUserInteractionEnabled = viewModel.isEnabled + + accessibilityLabel = viewModel.accessibilityLabel + } + + // MARK: - Setup + + private func setupViews() { + separatorInset = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 0) + + setupContentLabel() + setupFeaturedImageView() + setupStatusLabel() + + contentStackView.translatesAutoresizingMaskIntoConstraints = false + contentStackView.addArrangedSubviews([ + contentLabel, + featuredImageView + ]) + contentStackView.spacing = 16 + contentStackView.alignment = .top + + mainStackView.translatesAutoresizingMaskIntoConstraints = false + mainStackView.addArrangedSubviews([ + headerView, + contentStackView, + statusLabel + ]) + mainStackView.spacing = 4 + mainStackView.isLayoutMarginsRelativeArrangement = true + mainStackView.directionalLayoutMargins = NSDirectionalEdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16) + + contentView.addSubview(mainStackView) + contentView.pinSubviewToAllEdges(mainStackView) + } + + private func setupContentLabel() { + contentLabel.translatesAutoresizingMaskIntoConstraints = false + contentLabel.adjustsFontForContentSizeCategory = true + contentLabel.numberOfLines = 3 + contentLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal) + } + + private func setupFeaturedImageView() { + featuredImageView.translatesAutoresizingMaskIntoConstraints = false + featuredImageView.contentMode = .scaleAspectFill + featuredImageView.layer.masksToBounds = true + featuredImageView.layer.cornerRadius = 5 + featuredImageView.setContentCompressionResistancePriority(.defaultHigh, for: .vertical) + + NSLayoutConstraint.activate([ + featuredImageView.widthAnchor.constraint(equalToConstant: Constants.imageSize.width), + featuredImageView.heightAnchor.constraint(equalToConstant: Constants.imageSize.height), + ]) + } + + private func setupStatusLabel() { + statusLabel.translatesAutoresizingMaskIntoConstraints = false + statusLabel.adjustsFontForContentSizeCategory = true + statusLabel.numberOfLines = 1 + statusLabel.font = WPStyleGuide.fontForTextStyle(.footnote, fontWeight: .regular) + } +} + +private enum Constants { + static let imageSize = CGSize(width: 64, height: 64) +} diff --git a/WordPress/Classes/ViewRelated/Post/PostListFilter.swift b/WordPress/Classes/ViewRelated/Post/PostListFilter.swift index aae36c9450ea..9c9198064bdb 100644 --- a/WordPress/Classes/ViewRelated/Post/PostListFilter.swift +++ b/WordPress/Classes/ViewRelated/Post/PostListFilter.swift @@ -219,7 +219,7 @@ import Foundation return filter } - func predicate(for blog: Blog) -> NSPredicate { + func predicate(for blog: Blog, author: PostListFilterSettings.AuthorFilter = .mine) -> NSPredicate { var predicates = [NSPredicate]() // Show all original posts without a revision & revision posts. @@ -228,7 +228,7 @@ import Foundation predicates.append(predicateForFetchRequest) - if let myAuthorID = blog.userID { + if author == .mine, let myAuthorID = blog.userID { // Brand new local drafts have an authorID of 0. let authorPredicate = NSPredicate(format: "authorID = %@ || authorID = 0", myAuthorID) predicates.append(authorPredicate) diff --git a/WordPress/Classes/ViewRelated/Post/PostListFilterSettings.swift b/WordPress/Classes/ViewRelated/Post/PostListFilterSettings.swift index 319305c3f6ed..8cd5b8e3e041 100644 --- a/WordPress/Classes/ViewRelated/Post/PostListFilterSettings.swift +++ b/WordPress/Classes/ViewRelated/Post/PostListFilterSettings.swift @@ -6,7 +6,6 @@ import WordPressShared class PostListFilterSettings: NSObject { fileprivate static let currentPostAuthorFilterKey = "CurrentPostAuthorFilterKey" fileprivate static let currentPageListStatusFilterKey = "CurrentPageListStatusFilterKey" - fileprivate static let currentPostListStatusFilterKey = "CurrentPostListStatusFilterKey" @objc let blog: Blog @objc let postType: PostServiceType @@ -15,15 +14,6 @@ class PostListFilterSettings: NSObject { enum AuthorFilter: UInt { case mine = 0 case everyone = 1 - - var stringValue: String { - switch self { - case .mine: - return NSLocalizedString("Me", comment: "Label for the post author filter. This filter shows posts only authored by the current user.") - case .everyone: - return NSLocalizedString("Everyone", comment: "Label for the post author filter. This filter shows posts for all users on the blog.") - } - } } /// Initializes a new PostListFilterSettings instance /// - Parameter blog: the blog which owns the list of posts @@ -42,11 +32,6 @@ class PostListFilterSettings: NSObject { return allPostListFilters! } - func filterThatDisplaysPostsWithStatus(_ postStatus: BasePost.Status) -> PostListFilter { - let index = indexOfFilterThatDisplaysPostsWithStatus(postStatus) - return availablePostListFilters()[index] - } - func indexOfFilterThatDisplaysPostsWithStatus(_ postStatus: BasePost.Status) -> Int { var index = 0 var found = false @@ -147,13 +132,12 @@ class PostListFilterSettings: NSObject { return .everyone } - if let filter = UserPersistentStoreFactory.instance().object(forKey: type(of: self).currentPostAuthorFilterKey) { - if (filter as AnyObject).uintValue == AuthorFilter.everyone.rawValue { - return .everyone - } + if let rawValue = UserPersistentStoreFactory.instance().object(forKey: type(of: self).currentPostAuthorFilterKey), + let filter = AuthorFilter(rawValue: (rawValue as AnyObject).uintValue) { + return filter } - return .mine + return .everyone } /// currentPostListFilter: stores the last active AuthorFilter diff --git a/WordPress/Classes/ViewRelated/Post/PostListHeaderView.swift b/WordPress/Classes/ViewRelated/Post/PostListHeaderView.swift new file mode 100644 index 000000000000..329e969c205d --- /dev/null +++ b/WordPress/Classes/ViewRelated/Post/PostListHeaderView.swift @@ -0,0 +1,62 @@ +import UIKit + +final class PostListHeaderView: UIView { + + // MARK: - Views + + private let textLabel = UILabel() + private let ellipsisButton = UIButton(type: .custom) + + // MARK: - Properties + + private var post: Post? + private var viewModel: PostListItemViewModel? + + // MARK: - Initializer + + override init(frame: CGRect) { + super.init(frame: frame) + setupView() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Public + + func configure(with viewModel: PostListItemViewModel, delegate: InteractivePostViewDelegate? = nil) { + if let delegate { + configureEllipsisButton(with: viewModel.post, delegate: delegate) + } + textLabel.attributedText = viewModel.badges + } + + private func configureEllipsisButton(with post: Post, delegate: InteractivePostViewDelegate) { + let menuHelper = AbstractPostMenuHelper(post) + ellipsisButton.showsMenuAsPrimaryAction = true + ellipsisButton.menu = menuHelper.makeMenu(presentingView: ellipsisButton, delegate: delegate) + } + + // MARK: - Setup + + private func setupView() { + setupEllipsisButton() + + let stackView = UIStackView(arrangedSubviews: [textLabel, ellipsisButton]) + stackView.spacing = 12 + addSubview(stackView) + stackView.translatesAutoresizingMaskIntoConstraints = false + pinSubviewToAllEdges(stackView) + } + + private func setupEllipsisButton() { + ellipsisButton.translatesAutoresizingMaskIntoConstraints = false + ellipsisButton.setImage(UIImage(named: "more-horizontal-mobile"), for: .normal) + ellipsisButton.tintColor = .listIcon + + NSLayoutConstraint.activate([ + ellipsisButton.widthAnchor.constraint(equalToConstant: 24) + ]) + } +} diff --git a/WordPress/Classes/ViewRelated/Post/PostListItemViewModel.swift b/WordPress/Classes/ViewRelated/Post/PostListItemViewModel.swift new file mode 100644 index 000000000000..72cf12317dda --- /dev/null +++ b/WordPress/Classes/ViewRelated/Post/PostListItemViewModel.swift @@ -0,0 +1,134 @@ +import Foundation + +final class PostListItemViewModel { + let post: Post + let content: NSAttributedString + let imageURL: URL? + let badges: NSAttributedString + let isEnabled: Bool + private let statusViewModel: PostCardStatusViewModel + + var status: String { statusViewModel.statusAndBadges(separatedBy: " · ")} + var statusColor: UIColor { statusViewModel.statusColor } + var accessibilityLabel: String? { makeAccessibilityLabel(for: post, statusViewModel: statusViewModel) } + + init(post: Post, shouldHideAuthor: Bool = false) { + self.post = post + self.content = makeContentString(for: post) + self.imageURL = post.featuredImageURL + self.badges = makeBadgesString(for: post, shouldHideAuthor: shouldHideAuthor) + self.statusViewModel = PostCardStatusViewModel(post: post) + self.isEnabled = !PostCoordinator.shared.isDeleting(post) + } +} + +private func makeAccessibilityLabel(for post: Post, statusViewModel: PostCardStatusViewModel) -> String? { + let titleAndDateChunk: String = { + return String(format: Strings.Accessibility.titleAndDateChunkFormat, post.titleForDisplay(), post.dateStringForDisplay()) + }() + + let authorChunk: String? = { + let author = statusViewModel.author + guard !author.isEmpty else { + return nil + } + return String(format: Strings.Accessibility.authorChunkFormat, author) + }() + + let stickyChunk = post.isStickyPost ? Strings.Accessibility.sticky : nil + + let statusChunk: String? = { + guard let status = statusViewModel.status else { + return nil + } + + return "\(status)." + }() + + let excerptChunk: String? = { + let excerpt = post.contentPreviewForDisplay() + guard !excerpt.isEmpty else { + return nil + } + return String(format: Strings.Accessibility.exerptChunkFormat, excerpt) + }() + + return [titleAndDateChunk, authorChunk, stickyChunk, statusChunk, excerptChunk] + .compactMap { $0 } + .joined(separator: " ") +} + +private func makeContentString(for post: Post) -> NSAttributedString { + let title = post.titleForDisplay() + let snippet = post.contentPreviewForDisplay() + + let string = NSMutableAttributedString() + if !title.isEmpty { + let attributes: [NSAttributedString.Key: Any] = [ + .font: WPStyleGuide.fontForTextStyle(.callout, fontWeight: .semibold), + .foregroundColor: UIColor.text + ] + let titleAttributedString = NSAttributedString(string: title, attributes: attributes) + string.append(titleAttributedString) + } + if !snippet.isEmpty { + // Normalize newlines by collapsing multiple occurrences of newlines to a single newline + let adjustedSnippet = snippet.replacingOccurrences(of: "[\n]{2,}", with: "\n", options: .regularExpression) + if string.length > 0 { + string.append(NSAttributedString(string: "\n")) + } + let attributes: [NSAttributedString.Key: Any] = [ + .font: WPStyleGuide.fontForTextStyle(.footnote, fontWeight: .regular), + .foregroundColor: UIColor.textSubtle + ] + let snippetAttributedString = NSAttributedString(string: adjustedSnippet, attributes: attributes) + string.append(snippetAttributedString) + } + + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.paragraphSpacing = 4 + string.addAttribute(.paragraphStyle, value: paragraphStyle, range: NSRange(location: 0, length: string.length)) + + return string +} + +private func makeBadgesString(for post: Post, shouldHideAuthor: Bool) -> NSAttributedString { + var badges: [(String, UIColor?)] = [] + if let date = AbstractPostHelper.getLocalizedStatusWithDate(for: post) { + let color: UIColor? = post.status == .trash ? .systemRed : nil + badges.append((date, color)) + } + if !shouldHideAuthor, let author = post.authorForDisplay() { + badges.append((author, nil)) + } + return AbstractPostHelper.makeBadgesString(with: badges) +} + +private enum Strings { + + enum Accessibility { + static let titleAndDateChunkFormat = NSLocalizedString( + "postList.a11y.titleAndDateChunkFormat", + value: "%1$@, %2$@.", + comment: "Accessibility label for a post in the post list. The first placeholder is the post title. The second placeholder is the date." + ) + + static let authorChunkFormat = NSLocalizedString( + "postList.a11y.authorChunkFormat", + value: "By %@.", + comment: "Accessibility label for the post author in the post list. The parameter is the author name. For example, \"By Elsa.\"" + ) + + static let exerptChunkFormat = NSLocalizedString( + "postList.a11y.exerptChunkFormat", + value: "Excerpt. %@.", + comment: "Accessibility label for a post's excerpt in the post list. The parameter is the post excerpt. For example, \"Excerpt. This is the first paragraph.\"" + ) + + static let sticky = NSLocalizedString( + "postList.a11y.sticky", + value: "Sticky.", + comment: "Accessibility label for a sticky post in the post list." + ) + } +} diff --git a/WordPress/Classes/ViewRelated/Post/PostListTableViewHandler.swift b/WordPress/Classes/ViewRelated/Post/PostListTableViewHandler.swift deleted file mode 100644 index da6307ddb33b..000000000000 --- a/WordPress/Classes/ViewRelated/Post/PostListTableViewHandler.swift +++ /dev/null @@ -1,53 +0,0 @@ -import Foundation - -class PostListTableViewHandler: WPTableViewHandler { - var isSearching: Bool = false - - private lazy var searchResultsController: NSFetchedResultsController = { - return resultsController(with: fetchRequest(), context: managedObjectContext(), keyPath: BasePost.statusKeyPath, performFetch: false) - }() - - override var resultsController: NSFetchedResultsController? { - if isSearching { - return searchResultsController - } - - return super.resultsController - } - - private func resultsController(with request: NSFetchRequest?, - context: NSManagedObjectContext?, - keyPath: String? = nil, - performFetch: Bool = true) -> NSFetchedResultsController { - guard let request = request, let context = context else { - fatalError("A request and a context must exist") - } - - let controller = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: keyPath, cacheName: nil) - if performFetch { - do { - try controller.performFetch() - } catch { - DDLogError("Error fetching pages after refreshing the table: \(error)") - } - } - - return controller - } - - private func fetchRequest() -> NSFetchRequest? { - return delegate?.fetchRequest() - } - - private func managedObjectContext() -> NSManagedObjectContext? { - return delegate?.managedObjectContext() - } - - private func sectionNameKeyPath() -> String? { - return delegate?.sectionNameKeyPath?() - } - - override func refreshTableView() { - super.refreshTableView() - } -} diff --git a/WordPress/Classes/ViewRelated/Post/PostListViewController.swift b/WordPress/Classes/ViewRelated/Post/PostListViewController.swift index e66e91b6f2a4..6466912def9f 100644 --- a/WordPress/Classes/ViewRelated/Post/PostListViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/PostListViewController.swift @@ -4,87 +4,19 @@ import WordPressShared import Gridicons import UIKit -class PostListViewController: AbstractPostListViewController, UIViewControllerRestoration, InteractivePostViewDelegate { - - private let postCompactCellIdentifier = "PostCompactCellIdentifier" - private let postCardTextCellIdentifier = "PostCardTextCellIdentifier" - private let postCardRestoreCellIdentifier = "PostCardRestoreCellIdentifier" - private let postCompactCellNibName = "PostCompactCell" - private let postCardTextCellNibName = "PostCardCell" - private let postCardRestoreCellNibName = "RestorePostTableViewCell" - private let statsStoryboardName = "SiteStats" - private let currentPostListStatusFilterKey = "CurrentPostListStatusFilterKey" - private var postCellIdentifier: String { - return isCompact || isSearching() ? postCompactCellIdentifier : postCardTextCellIdentifier - } - +final class PostListViewController: AbstractPostListViewController, UIViewControllerRestoration, InteractivePostViewDelegate { static private let postsViewControllerRestorationKey = "PostsViewControllerRestorationKey" - private let statsCacheInterval = TimeInterval(300) // 5 minutes - - private let postCardEstimatedRowHeight = CGFloat(300.0) - private let postListHeightForFooterView = CGFloat(50.0) - - @IBOutlet var searchWrapperView: UIView! - @IBOutlet weak var filterTabBarTopConstraint: NSLayoutConstraint! - @IBOutlet weak var filterTabBariOS10TopConstraint: NSLayoutConstraint! - @IBOutlet weak var filterTabBarBottomConstraint: NSLayoutConstraint! - @IBOutlet weak var tableViewTopConstraint: NSLayoutConstraint! - - private var database: UserPersistentRepository = UserPersistentStoreFactory.instance() - - private lazy var _tableViewHandler: PostListTableViewHandler = { - let tableViewHandler = PostListTableViewHandler(tableView: tableView) - tableViewHandler.cacheRowHeights = false - tableViewHandler.delegate = self - tableViewHandler.updateRowAnimation = .none - return tableViewHandler - }() - - override var tableViewHandler: WPTableViewHandler { - get { - return _tableViewHandler - } set { - super.tableViewHandler = newValue - } - } - - private var postViewIcon: UIImage? { - return isCompact ? UIImage(named: "icon-post-view-card") : .gridicon(.listUnordered) - } - - private lazy var postActionSheet: PostActionSheet = { - return PostActionSheet(viewController: self, interactivePostViewDelegate: self) - }() - - private lazy var postsViewButtonItem: UIBarButtonItem = { - return UIBarButtonItem(image: postViewIcon, style: .done, target: self, action: #selector(togglePostsView)) - }() - - private var showingJustMyPosts: Bool { - return filterSettings.currentPostAuthorFilter() == .mine - } - - private var isCompact: Bool = false { - didSet { - database.set(isCompact, forKey: Constants.exhibitionModeKey) - showCompactOrDefault() - } - } - /// If set, when the post list appear it will show the tab for this status - var initialFilterWithPostStatus: BasePost.Status? + private var initialFilterWithPostStatus: BasePost.Status? // MARK: - Convenience constructors @objc class func controllerWithBlog(_ blog: Blog) -> PostListViewController { - - let storyBoard = UIStoryboard(name: "Posts", bundle: Bundle.main) - let controller = storyBoard.instantiateViewController(withIdentifier: "PostListViewController") as! PostListViewController - controller.blog = blog - controller.restorationClass = self - - return controller + let vc = PostListViewController() + vc.blog = blog + vc.restorationClass = self + return vc } static func showForBlog(_ blog: Blog, from sourceController: UIViewController, withPostStatus postStatus: BasePost.Status? = nil) { @@ -98,15 +30,13 @@ class PostListViewController: AbstractPostListViewController, UIViewControllerRe // MARK: - UIViewControllerRestoration - class func viewController(withRestorationIdentifierPath identifierComponents: [String], - coder: NSCoder) -> UIViewController? { - + class func viewController(withRestorationIdentifierPath identifierComponents: [String], coder: NSCoder) -> UIViewController? { let context = ContextManager.sharedInstance().mainContext guard let blogID = coder.decodeObject(forKey: postsViewControllerRestorationKey) as? String, - let objectURL = URL(string: blogID), - let objectID = context.persistentStoreCoordinator?.managedObjectID(forURIRepresentation: objectURL), - let restoredBlog = (try? context.existingObject(with: objectID)) as? Blog else { + let objectURL = URL(string: blogID), + let objectID = context.persistentStoreCoordinator?.managedObjectID(forURIRepresentation: objectURL), + let restoredBlog = (try? context.existingObject(with: objectID)) as? Blog else { return nil } @@ -127,49 +57,30 @@ class PostListViewController: AbstractPostListViewController, UIViewControllerRe // MARK: - UIViewController - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - - precondition(segue.destination is UITableViewController) - - super.refreshNoResultsViewController = { [weak self] noResultsViewController in - self?.handleRefreshNoResultsViewController(noResultsViewController) - } - - super.tableViewController = (segue.destination as! UITableViewController) - } - override func viewDidLoad() { super.viewDidLoad() title = NSLocalizedString("Posts", comment: "Title of the screen showing the list of posts for a blog.") - configureFilterBarTopConstraint() - updateGhostableTableViewOptions() - - configureNavigationButtons() - configureInitialFilterIfNeeded() listenForAppComingToForeground() createButtonCoordinator.add(to: view, trailingAnchor: view.safeAreaLayoutGuide.trailingAnchor, bottomAnchor: view.safeAreaLayoutGuide.bottomAnchor) + + refreshNoResultsViewController = { [weak self] in + self?.handleRefreshNoResultsViewController($0) + } + + NotificationCenter.default.addObserver(self, selector: #selector(postCoordinatorDidUpdate), name: .postCoordinatorDidUpdate, object: nil) } private lazy var createButtonCoordinator: CreateButtonCoordinator = { var actions: [ActionSheetItem] = [ PostAction(handler: { [weak self] in - self?.dismiss(animated: false, completion: nil) - self?.createPost() + self?.dismiss(animated: false, completion: nil) + self?.createPost() }, source: Constants.source) ] - if blog.supports(.stories) { - actions.insert(StoryAction(handler: { [weak self] in - guard let self = self else { - return - } - let presenter = RootViewCoordinator.sharedPresenter - presenter.showStoryEditor(blog: self.blog, title: nil, content: nil) - }, source: Constants.source), at: 0) - } return CreateButtonCoordinator(self, actions: actions, source: Constants.source, blog: blog) }() @@ -181,20 +92,13 @@ class PostListViewController: AbstractPostListViewController, UIViewControllerRe } } - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - configureCompactOrDefault() - } - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) toggleCreateButton() } /// Shows/hides the create button based on the trait collection horizontal size class - @objc - private func toggleCreateButton() { + @objc private func toggleCreateButton() { if traitCollection.horizontalSizeClass == .compact { createButtonCoordinator.showCreateButton(for: blog) } else { @@ -202,144 +106,30 @@ class PostListViewController: AbstractPostListViewController, UIViewControllerRe } } - func configureNavigationButtons() { - navigationItem.rightBarButtonItems = [postsViewButtonItem] - } - - @objc func togglePostsView() { - isCompact.toggle() - - WPAppAnalytics.track(.postListToggleButtonPressed, withProperties: ["mode": isCompact ? Constants.compact: Constants.card]) - } - - // MARK: - Configuration - - override func heightForFooterView() -> CGFloat { - return postListHeightForFooterView - } + // MARK: - Notifications - func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - guard _tableViewHandler.isSearching else { - return 0.0 + @objc private func postCoordinatorDidUpdate(_ notification: Foundation.Notification) { + guard let updatedObjects = (notification.userInfo?[NSUpdatedObjectsKey] as? Set) else { + return } - return Constants.searchHeaderHeight - } - - func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView! { - guard _tableViewHandler.isSearching, - let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: ActivityListSectionHeaderView.identifier) as? ActivityListSectionHeaderView else { - return UIView(frame: .zero) + let updatedIndexPaths = (tableView.indexPathsForVisibleRows ?? []).filter { + let post = fetchResultsController.object(at: $0) + return updatedObjects.contains(post) } - - let sectionInfo = _tableViewHandler.resultsController?.sections?[section] - - if let sectionInfo = sectionInfo { - headerView.titleLabel.text = PostSearchHeader.title(forStatus: sectionInfo.name) + if !updatedIndexPaths.isEmpty { + tableView.beginUpdates() + tableView.reloadRows(at: updatedIndexPaths, with: .automatic) + tableView.endUpdates() } - - return headerView - } - - private func configureFilterBarTopConstraint() { - filterTabBariOS10TopConstraint.isActive = false - } - - - override func selectedFilterDidChange(_ filterBar: FilterTabBar) { - updateGhostableTableViewOptions() - super.selectedFilterDidChange(filterBar) - } - - override func refresh(_ sender: AnyObject) { - updateGhostableTableViewOptions() - super.refresh(sender) } - /// Update the `GhostOptions` to correctly show compact or default cells - private func updateGhostableTableViewOptions() { - let ghostOptions = GhostOptions(displaysSectionHeader: false, reuseIdentifier: postCellIdentifier, rowsPerSection: [50]) - let style = GhostStyle(beatDuration: GhostStyle.Defaults.beatDuration, - beatStartColor: .placeholderElement, - beatEndColor: .placeholderElementFaded) - ghostableTableView.removeGhostContent() - ghostableTableView.displayGhostContent(options: ghostOptions, style: style) - } - - private func configureCompactOrDefault() { - isCompact = database.object(forKey: Constants.exhibitionModeKey) as? Bool ?? false - } + // MARK: - Configuration override func configureTableView() { - tableView.accessibilityIdentifier = "PostsTable" - tableView.separatorStyle = .none - tableView.estimatedRowHeight = postCardEstimatedRowHeight - tableView.rowHeight = UITableView.automaticDimension - tableView.separatorStyle = .none - - let bundle = Bundle.main - - // Register the cells - let postCardTextCellNib = UINib(nibName: postCardTextCellNibName, bundle: bundle) - tableView.register(postCardTextCellNib, forCellReuseIdentifier: postCardTextCellIdentifier) - - let postCompactCellNib = UINib(nibName: postCompactCellNibName, bundle: bundle) - tableView.register(postCompactCellNib, forCellReuseIdentifier: postCompactCellIdentifier) - - let postCardRestoreCellNib = UINib(nibName: postCardRestoreCellNibName, bundle: bundle) - tableView.register(postCardRestoreCellNib, forCellReuseIdentifier: postCardRestoreCellIdentifier) + super.configureTableView() - let headerNib = UINib(nibName: ActivityListSectionHeaderView.identifier, bundle: nil) - tableView.register(headerNib, forHeaderFooterViewReuseIdentifier: ActivityListSectionHeaderView.identifier) - - WPStyleGuide.configureColors(view: view, tableView: tableView) - } - - override func configureGhostableTableView() { - super.configureGhostableTableView() - - ghostingEnabled = true - - // Register the cells - let postCardTextCellNib = UINib(nibName: postCardTextCellNibName, bundle: Bundle.main) - ghostableTableView.register(postCardTextCellNib, forCellReuseIdentifier: postCardTextCellIdentifier) - - let postCompactCellNib = UINib(nibName: postCompactCellNibName, bundle: Bundle.main) - ghostableTableView.register(postCompactCellNib, forCellReuseIdentifier: postCompactCellIdentifier) - } - - override func configureSearchController() { - super.configureSearchController() - - searchWrapperView.addSubview(searchController.searchBar) - - tableView.verticalScrollIndicatorInsets.top = searchController.searchBar.bounds.height - - updateTableHeaderSize() - } - - fileprivate func updateTableHeaderSize() { - if searchController.isActive { - // Account for the search bar being moved to the top of the screen. - searchWrapperView.frame.size.height = 0 - } else { - searchWrapperView.frame.size.height = searchController.searchBar.bounds.height - } - - // Resetting the tableHeaderView is necessary to get the new height to take effect - tableView.tableHeaderView = searchWrapperView - } - - func showCompactOrDefault() { - updateGhostableTableViewOptions() - - postsViewButtonItem.accessibilityLabel = NSLocalizedString("List style", comment: "The accessibility label for the list style button in the Post List.") - postsViewButtonItem.accessibilityValue = isCompact ? NSLocalizedString("Compact", comment: "Accessibility indication that the current Post List style is currently Compact.") : NSLocalizedString("Expanded", comment: "Accessibility indication that the current Post List style is currently Expanded.") - postsViewButtonItem.image = postViewIcon - - if isViewOnScreen() { - tableView.reloadSections([0], with: .automatic) - ghostableTableView.reloadSections([0], with: .automatic) - } + tableView.accessibilityIdentifier = "PostsTable" + tableView.register(PostListCell.self, forCellReuseIdentifier: PostListCell.defaultReuseID) } private func configureInitialFilterIfNeeded() { @@ -357,19 +147,6 @@ class PostListViewController: AbstractPostListViewController, UIViewControllerRe name: UIApplication.willEnterForegroundNotification, object: nil) } - // Mark - Layout Methods - - override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) { - // Need to reload the table alongside a traitCollection change. - // This is mainly because we target Reg W and Any H vs all other size classes. - // If we transition between the two, the tableView may not update the cell heights accordingly. - // Brent C. Aug 3/2016 - coordinator.animate(alongsideTransition: { context in - if self.isViewLoaded { - self.tableView.reloadData() - } - }) - } // MARK: - Sync Methods @@ -377,29 +154,12 @@ class PostListViewController: AbstractPostListViewController, UIViewControllerRe return .post } - override func lastSyncDate() -> Date? { - return blog?.lastPostsSync - } - // MARK: - Data Model Interaction - /// Retrieves the post object at the specified index path. - /// - /// - Parameter indexPath: the index path of the post object to retrieve. - /// - /// - Returns: the requested post. - /// - fileprivate func postAtIndexPath(_ indexPath: IndexPath) -> Post { - guard let post = tableViewHandler.resultsController?.object(at: indexPath) as? Post else { - // Retrieving anything other than a post object means we have an App with an invalid - // state. Ignoring this error would be counter productive as we have no idea how this - // can affect the App. This controlled interruption is intentional. - // - // - Diego Rey Mendez, May 18 2016 - // + private func postAtIndexPath(_ indexPath: IndexPath) -> Post { + guard let post = fetchResultsController.object(at: indexPath) as? Post else { fatalError("Expected a post object.") } - return post } @@ -418,18 +178,8 @@ class PostListViewController: AbstractPostListViewController, UIViewControllerRe predicates.append(basePredicate) } - let searchText = currentSearchTerm() ?? "" - let filterPredicate = searchController.isActive ? NSPredicate(format: "postTitle CONTAINS[cd] %@", searchText) : filterSettings.currentPostListFilter().predicateForFetchRequest - - // If we have recently trashed posts, create an OR predicate to find posts matching the filter, - // or posts that were recently deleted. - if searchText.count == 0 && recentlyTrashedPostObjectIDs.count > 0 { - let trashedPredicate = NSPredicate(format: "SELF IN %@", recentlyTrashedPostObjectIDs) - - predicates.append(NSCompoundPredicate(orPredicateWithSubpredicates: [filterPredicate, trashedPredicate])) - } else { - predicates.append(filterPredicate) - } + let filterPredicate = filterSettings.currentPostListFilter().predicateForFetchRequest + predicates.append(filterPredicate) if filterSettings.shouldShowOnlyMyPosts() { let myAuthorID = blogUserID() ?? 0 @@ -439,89 +189,52 @@ class PostListViewController: AbstractPostListViewController, UIViewControllerRe predicates.append(authorPredicate) } - if searchText.count > 0 { - let searchPredicate = NSPredicate(format: "postTitle CONTAINS[cd] %@", searchText) - predicates.append(searchPredicate) - } - let predicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates) return predicate } - // MARK: - Table View Handling - - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - tableView.deselectRow(at: indexPath, animated: true) - - let post = postAtIndexPath(indexPath) - - guard post.status != .trash else { - // No editing posts that are trashed. - return - } - - editPost(apost: post) - } - - @objc func tableView(_ tableView: UITableView, cellForRowAtIndexPath indexPath: IndexPath) -> UITableViewCell { - if let windowlessCell = dequeCellForWindowlessLoadingIfNeeded(tableView) { - return windowlessCell - } + // MARK: - UITableViewDataSource + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: PostListCell.defaultReuseID, for: indexPath) as! PostListCell let post = postAtIndexPath(indexPath) - let identifier = cellIdentifierForPost(post) - let cell = tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath) - - configureCell(cell, at: indexPath) - + cell.accessoryType = .none + cell.configure(with: PostListItemViewModel(post: post, shouldHideAuthor: shouldHideAuthor), delegate: self) return cell } - override func configureCell(_ cell: UITableViewCell, at indexPath: IndexPath) { - cell.accessoryType = .none + // MARK: - UITableViewDelegate + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) let post = postAtIndexPath(indexPath) - guard let interactivePostView = cell as? InteractivePostView, - let configurablePostView = cell as? ConfigurablePostView else { - fatalError("Cell does not implement the required protocols") + guard post.status != .trash else { + // No editing posts that are trashed. + return } - interactivePostView.setInteractionDelegate(self) - interactivePostView.setActionSheetDelegate(self) - - configurablePostView.configure(with: post) - - configurePostCell(cell) - configureRestoreCell(cell) + editPost(post) } - fileprivate func cellIdentifierForPost(_ post: Post) -> String { - var identifier: String - - if recentlyTrashedPostObjectIDs.contains(post.objectID) == true && filterSettings.currentPostListFilter().filterType != .trashed { - identifier = postCardRestoreCellIdentifier - } else { - identifier = postCellIdentifier + func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { + UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { [weak self] _ in + guard let self else { return nil } + let post = self.postAtIndexPath(indexPath) + let cell = self.tableView.cellForRow(at: indexPath) + return AbstractPostMenuHelper(post).makeMenu(presentingView: cell ?? UIView(), delegate: self) } - - return identifier } - private func configurePostCell(_ cell: UITableViewCell) { - guard let cell = cell as? PostCardCell else { - return - } - - cell.shouldHideAuthor = showingJustMyPosts + func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + let actions = AbstractPostHelper.makeLeadingContextualActions(for: postAtIndexPath(indexPath), delegate: self) + return UISwipeActionsConfiguration(actions: actions) } - private func configureRestoreCell(_ cell: UITableViewCell) { - guard let cell = cell as? RestorePostTableViewCell else { - return - } - - cell.isCompact = isCompact + func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + let actions = AbstractPostHelper.makeTrailingContextualActions(for: postAtIndexPath(indexPath), delegate: self) + return UISwipeActionsConfiguration(actions: actions) } // MARK: - Post Actions @@ -534,47 +247,24 @@ class PostListViewController: AbstractPostListViewController, UIViewControllerRe WPAppAnalytics.track(.editorCreatedPost, withProperties: [WPAppAnalyticsKeyTapSource: "posts_view", WPAppAnalyticsKeyPostType: "post"], with: blog) } - private func editPost(apost: AbstractPost) { - guard let post = apost as? Post else { + private func editPost(_ post: AbstractPost) { + guard let post = post as? Post else { return } - WPAppAnalytics.track(.postListEditAction, withProperties: propertiesForAnalytics(), with: post) PostListEditorPresenter.handle(post: post, in: self, entryPoint: .postsList) } - private func editDuplicatePost(apost: AbstractPost) { - guard let post = apost as? Post else { + private func editDuplicatePost(_ post: AbstractPost) { + guard let post = post as? Post else { return } - PostListEditorPresenter.handleCopy(post: post, in: self) } - override func promptThatPostRestoredToFilter(_ filter: PostListFilter) { - var message = NSLocalizedString("Post Restored to Drafts", comment: "Prompts the user that a restored post was moved to the drafts list.") - - switch filter.filterType { - case .published: - message = NSLocalizedString("Post Restored to Published", comment: "Prompts the user that a restored post was moved to the published list.") - break - case .scheduled: - message = NSLocalizedString("Post Restored to Scheduled", comment: "Prompts the user that a restored post was moved to the scheduled list.") - break - default: - break - } - - let alertCancel = NSLocalizedString("OK", comment: "Title of an OK button. Pressing the button acknowledges and dismisses a prompt.") - - let alertController = UIAlertController(title: nil, message: message, preferredStyle: .alert) - alertController.addCancelActionWithTitle(alertCancel, handler: nil) - alertController.presentFromRootViewController() - } - - fileprivate func viewStatsForPost(_ apost: AbstractPost) { + fileprivate func viewStatsForPost(_ post: AbstractPost) { // Check the blog - let blog = apost.blog + let blog = post.blog guard blog.supports(.stats) else { // Needs Jetpack. @@ -584,7 +274,7 @@ class PostListViewController: AbstractPostListViewController, UIViewControllerRe WPAnalytics.track(.postListStatsAction, withProperties: propertiesForAnalytics()) // Push the Post Stats ViewController - guard let postID = apost.postID as? Int else { + guard let postID = post.postID as? Int else { return } @@ -592,9 +282,9 @@ class PostListViewController: AbstractPostListViewController, UIViewControllerRe SiteStatsInformation.sharedInstance.oauth2Token = blog.authToken SiteStatsInformation.sharedInstance.siteID = blog.dotComID - let postURL = URL(string: apost.permaLink! as String) + let postURL = URL(string: post.permaLink! as String) let postStatsTableViewController = PostStatsTableViewController.withJPBannerForBlog(postID: postID, - postTitle: apost.titleForDisplay(), + postTitle: post.titleForDisplay(), postURL: postURL) navigationController?.pushViewController(postStatsTableViewController, animated: true) } @@ -602,7 +292,7 @@ class PostListViewController: AbstractPostListViewController, UIViewControllerRe // MARK: - InteractivePostViewDelegate func edit(_ post: AbstractPost) { - editPost(apost: post) + editPost(post) } func view(_ post: AbstractPost) { @@ -610,18 +300,15 @@ class PostListViewController: AbstractPostListViewController, UIViewControllerRe } func stats(for post: AbstractPost) { - ReachabilityUtils.onAvailableInternetConnectionDo { - viewStatsForPost(post) - } + viewStatsForPost(post) } func duplicate(_ post: AbstractPost) { - editDuplicatePost(apost: post) + editDuplicatePost(post) } func publish(_ post: AbstractPost) { publishPost(post) { - BloggingRemindersFlow.present(from: self, for: post.blog, source: .publishFlow, @@ -633,13 +320,7 @@ class PostListViewController: AbstractPostListViewController, UIViewControllerRe copyPostLink(post) } - func trash(_ post: AbstractPost) { - guard ReachabilityUtils.isInternetReachable() else { - let offlineMessage = NSLocalizedString("Unable to trash posts while offline. Please try again later.", comment: "Message that appears when a user tries to trash a post while their device is offline.") - ReachabilityUtils.showNoInternetConnectionNotice(message: offlineMessage) - return - } - + func trash(_ post: AbstractPost, completion: @escaping () -> Void) { let cancelText: String let deleteText: String let messageText: String @@ -659,23 +340,18 @@ class PostListViewController: AbstractPostListViewController, UIViewControllerRe let alertController = UIAlertController(title: titleText, message: messageText, preferredStyle: .alert) - alertController.addCancelActionWithTitle(cancelText) + alertController.addCancelActionWithTitle(cancelText) { _ in + completion() + } alertController.addDestructiveActionWithTitle(deleteText) { [weak self] action in self?.deletePost(post) + completion() } alertController.presentFromRootViewController() } - func restore(_ post: AbstractPost) { - ReachabilityUtils.onAvailableInternetConnectionDo { - restorePost(post) - } - } - func draft(_ post: AbstractPost) { - ReachabilityUtils.onAvailableInternetConnectionDo { - moveToDraft(post) - } + moveToDraft(post) } func retry(_ post: AbstractPost) { @@ -686,8 +362,8 @@ class PostListViewController: AbstractPostListViewController, UIViewControllerRe PostCoordinator.shared.cancelAutoUploadOf(post) } - func share(_ apost: AbstractPost, fromView view: UIView) { - guard let post = apost as? Post else { + func share(_ post: AbstractPost, fromView view: UIView) { + guard let post = post as? Post else { return } @@ -702,57 +378,15 @@ class PostListViewController: AbstractPostListViewController, UIViewControllerRe BlazeFlowCoordinator.presentBlaze(in: self, source: .postsList, blog: blog, post: post) } - // MARK: - Searching - - override func updateForLocalPostsMatchingSearchText() { - // If the user taps and starts to type right away, avoid doing the search - // while the tableViewHandler is not ready yet - if !_tableViewHandler.isSearching, let search = currentSearchTerm(), !search.isEmpty { - return - } - - super.updateForLocalPostsMatchingSearchText() - } - - override func willPresentSearchController(_ searchController: UISearchController) { - super.willPresentSearchController(searchController) - - self.filterTabBar.alpha = WPAlphaZero - } - - func didPresentSearchController(_ searchController: UISearchController) { - updateTableHeaderSize() - _tableViewHandler.isSearching = true - - tableView.verticalScrollIndicatorInsets.top = searchWrapperView.bounds.height - tableView.contentInset.top = 0 - } - - override func sortDescriptorsForFetchRequest() -> [NSSortDescriptor] { - if !isSearching() { - return super.sortDescriptorsForFetchRequest() - } - - let descriptor = NSSortDescriptor(key: BasePost.statusKeyPath, ascending: true) - return [descriptor] - } - - override func willDismissSearchController(_ searchController: UISearchController) { - _tableViewHandler.isSearching = false - _tableViewHandler.refreshTableView() - super.willDismissSearchController(searchController) + func comments(_ post: AbstractPost) { + WPAnalytics.track(.postListCommentsAction, properties: propertiesForAnalytics()) + let contentCoordinator = DefaultContentCoordinator(controller: self, context: ContextManager.sharedInstance().mainContext) + try? contentCoordinator.displayCommentsWithPostId(post.postID, siteID: blog.dotComID, commentID: nil, source: .postsList) } - func didDismissSearchController(_ searchController: UISearchController) { - updateTableHeaderSize() - - UIView.animate(withDuration: Animations.searchDismissDuration) { - self.filterTabBar.alpha = WPAlphaFull - } - } - - enum Animations { - static let searchDismissDuration: TimeInterval = 0.3 + func showSettings(for post: AbstractPost) { + WPAnalytics.track(.postListSettingsAction, properties: propertiesForAnalytics()) + PostSettingsViewController.showStandaloneEditor(for: post, from: self) } // MARK: - NetworkAwareUI @@ -763,10 +397,6 @@ class PostListViewController: AbstractPostListViewController, UIViewControllerRe } private enum Constants { - static let exhibitionModeKey = "showCompactPosts" - static let searchHeaderHeight: CGFloat = 40 - static let card = "card" - static let compact = "compact" static let source = "post_list" } } @@ -782,20 +412,12 @@ private extension PostListViewController { return } - if searchController.isActive { - if currentSearchTerm()?.count == 0 { - noResultsViewController.configureForNoSearchResults(title: NoResultsText.searchPosts) - } else { - noResultsViewController.configureForNoSearchResults(title: noResultsTitle()) - } - } else { - let accessoryView = syncHelper.isSyncing ? NoResultsViewController.loadingAccessoryView() : nil + let accessoryView = syncHelper.isSyncing ? NoResultsViewController.loadingAccessoryView() : nil - noResultsViewController.configure(title: noResultsTitle(), - buttonTitle: noResultsButtonTitle(), - image: noResultsImageName, - accessoryView: accessoryView) - } + noResultsViewController.configure(title: noResultsTitle(), + buttonTitle: noResultsButtonTitle(), + image: noResultsImageName, + accessoryView: accessoryView) } var noResultsImageName: String { @@ -803,7 +425,7 @@ private extension PostListViewController { } func noResultsButtonTitle() -> String? { - if syncHelper.isSyncing == true || isSearching() { + if syncHelper.isSyncing == true { return nil } @@ -815,11 +437,6 @@ private extension PostListViewController { if syncHelper.isSyncing == true { return NoResultsText.fetchingTitle } - - if isSearching() { - return NoResultsText.noMatchesTitle - } - return noResultsFilteredTitle() } @@ -842,20 +459,11 @@ private extension PostListViewController { struct NoResultsText { static let buttonTitle = NSLocalizedString("Create Post", comment: "Button title, encourages users to create post on their blog.") static let fetchingTitle = NSLocalizedString("Fetching posts...", comment: "A brief prompt shown when the reader is empty, letting the user know the app is currently fetching new posts.") - static let noMatchesTitle = NSLocalizedString("No posts matching your search", comment: "Displayed when the user is searching the posts list and there are no matching posts") static let noDraftsTitle = NSLocalizedString("You don't have any draft posts", comment: "Displayed when the user views drafts in the posts list and there are no posts") static let noScheduledTitle = NSLocalizedString("You don't have any scheduled posts", comment: "Displayed when the user views scheduled posts in the posts list and there are no posts") static let noTrashedTitle = NSLocalizedString("You don't have any trashed posts", comment: "Displayed when the user views trashed in the posts list and there are no posts") static let noPublishedTitle = NSLocalizedString("You haven't published any posts yet", comment: "Displayed when the user views published posts in the posts list and there are no posts") static let noConnectionTitle: String = NSLocalizedString("Unable to load posts right now.", comment: "Title for No results full page screen displayedfrom post list when there is no connection") static let noConnectionSubtitle: String = NSLocalizedString("Check your network connection and try again. Or draft a post.", comment: "Subtitle for No results full page screen displayed from post list when there is no connection") - static let searchPosts = NSLocalizedString("Search posts", comment: "Text displayed when the search controller will be presented") - } -} - -extension PostListViewController: PostActionSheetDelegate { - func showActionSheet(_ postCardStatusViewModel: PostCardStatusViewModel, from view: UIView) { - let isCompactOrSearching = isCompact || searchController.isActive - postActionSheet.show(for: postCardStatusViewModel, from: view, isCompactOrSearching: isCompactOrSearching) } } diff --git a/WordPress/Classes/ViewRelated/Post/PostSearchHeader.swift b/WordPress/Classes/ViewRelated/Post/PostSearchHeader.swift deleted file mode 100644 index b84f62419ef6..000000000000 --- a/WordPress/Classes/ViewRelated/Post/PostSearchHeader.swift +++ /dev/null @@ -1,21 +0,0 @@ -import Foundation - -enum PostSearchHeader { - static let published = NSLocalizedString("Published", comment: "Title of the published header in search list.") - static let drafts = NSLocalizedString("Drafts", comment: "Title of the drafts header in search list.") - - static func title(forStatus rawStatus: String) -> String { - var title: String - - switch rawStatus { - case AbstractPost.Status.publish.rawValue: - title = PostSearchHeader.published - case AbstractPost.Status.draft.rawValue: - title = PostSearchHeader.drafts - default: - title = rawStatus - } - - return title.uppercased() - } -} diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+FeaturedImageUpload.swift b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+FeaturedImageUpload.swift index 3348b3d13b6f..2e993068bcff 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+FeaturedImageUpload.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+FeaturedImageUpload.swift @@ -2,7 +2,6 @@ import Foundation import Photos import PhotosUI import WordPressFlux -import WPMediaPicker // MARK: - PostSettingsViewController (Featured Image Menu) @@ -32,7 +31,7 @@ extension PostSettingsViewController: PHPickerViewControllerDelegate, ImagePicke return UIMenu(children: [ menu.makePhotosAction(delegate: self), menu.makeCameraAction(delegate: self), - menu.makeMediaAction(blog: self.apost.blog, delegate: self) + menu.makeSiteMediaAction(blog: self.apost.blog, delegate: self) ]) } @@ -41,7 +40,9 @@ extension PostSettingsViewController: PHPickerViewControllerDelegate, ImagePicke public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { self.dismiss(animated: true) { if let result = results.first { - self.setFeaturedImage(with: result.itemProvider) + MediaHelper.advertiseImageOptimization() { [self] in + self.setFeaturedImage(with: result.itemProvider) + } } } } @@ -49,29 +50,24 @@ extension PostSettingsViewController: PHPickerViewControllerDelegate, ImagePicke func imagePicker(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { self.dismiss(animated: true) { if let image = info[.originalImage] as? UIImage { - self.setFeaturedImage(with: image) + MediaHelper.advertiseImageOptimization() { [self] in + self.setFeaturedImage(with: image) + } } } } } -extension PostSettingsViewController: MediaPickerViewControllerDelegate { - func mediaPickerController(_ picker: WPMediaPickerViewController, didFinishPicking assets: [WPMediaAsset]) { - guard !assets.isEmpty else { return } +extension PostSettingsViewController: SiteMediaPickerViewControllerDelegate { + func siteMediaPickerViewController(_ viewController: SiteMediaPickerViewController, didFinishWithSelection selection: [Media]) { + dismiss(animated: true) - WPAnalytics.track(.editorPostFeaturedImageChanged, properties: ["via": "settings", "action": "added"]) + guard let media = selection.first else { return } - if let media = assets.first as? Media { - setFeaturedImage(media: media) - } - - dismiss(animated: true) + WPAnalytics.track(.editorPostFeaturedImageChanged, properties: ["via": "settings", "action": "added"]) + setFeaturedImage(media: media) reloadFeaturedImageCell() } - - func mediaPickerControllerDidCancel(_ picker: WPMediaPickerViewController) { - dismiss(animated: true) - } } // MARK: - PostSettingsViewController (Featured Image Upload) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift new file mode 100644 index 000000000000..2705a82b27ec --- /dev/null +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift @@ -0,0 +1,72 @@ +import UIKit +import CoreData +import Combine + +extension PostSettingsViewController { + static func showStandaloneEditor(for post: AbstractPost, from presentingViewController: UIViewController) { + let viewController: PostSettingsViewController + if let post = post as? Post { + viewController = PostSettingsViewController(post: post.latest()) + } else { + viewController = PageSettingsViewController(post: post) + } + viewController.isStandalone = true + let navigation = UINavigationController(rootViewController: viewController) + navigation.navigationBar.isTranslucent = true // Reset to default + presentingViewController.present(navigation, animated: true) + } + + @objc func setupStandaloneEditor() { + guard isStandalone else { return } + + configureDefaultNavigationBarAppearance() + + refreshNavigationBarButtons() + navigationItem.rightBarButtonItem?.isEnabled = false + + var cancellables: [AnyCancellable] = [] + apost.objectWillChange.sink { [weak self] in + self?.navigationItem.rightBarButtonItem?.isEnabled = true + }.store(in: &cancellables) + objc_setAssociatedObject(self, &PostSettingsViewController.cancellablesKey, cancellables, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + + private func refreshNavigationBarButtons() { + navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(buttonCancelTapped)) + + navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(buttonDoneTapped)) + } + + @objc private func buttonCancelTapped() { + presentingViewController?.dismiss(animated: true) + } + + @objc private func buttonDoneTapped() { + navigationItem.rightBarButtonItem = .activityIndicator + + setEnabled(false) + + PostCoordinator.shared.save(apost) { [weak self] result in + switch result { + case .success: + self?.isStandaloneEditorDismissingAfterSave = true + self?.presentingViewController?.dismiss(animated: true) + case .failure: + self?.setEnabled(true) + SVProgressHUD.showError(withStatus: Strings.errorMessage) + self?.refreshNavigationBarButtons() + } + } + } + + private func setEnabled(_ isEnabled: Bool) { + tableView.tintAdjustmentMode = isEnabled ? .automatic : .dimmed + tableView.isUserInteractionEnabled = isEnabled + } + + private static var cancellablesKey: UInt8 = 0 +} + +private enum Strings { + static let errorMessage = NSLocalizedString("postSettings.updateFailedMessage", value: "Failed to update the post settings", comment: "Error message on post/page settings screen") +} diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.h b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.h index 412f4b5cebc2..82e6b9087968 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.h +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.h @@ -13,6 +13,8 @@ - (void)endEditingAction:(nullable id)sender; @property (nonnull, nonatomic, strong, readonly) AbstractPost *apost; +@property (nonatomic) BOOL isStandalone; +@property (nonatomic) BOOL isStandaloneEditorDismissingAfterSave; @property (nonnull, nonatomic, strong, readonly) NSArray *publicizeConnections; @property (nonnull, nonatomic, strong, readonly) NSArray *unsupportedConnections; diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m index 4245d8e39592..b671874d9058 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m @@ -159,6 +159,7 @@ - (void)viewDidLoad // reachability callbacks to trigger before such initial setup completes. // [self setupReachability]; + [self setupStandaloneEditor]; } - (void)viewWillAppear:(BOOL)animated @@ -177,9 +178,15 @@ - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; - [self.apost.managedObjectContext performBlock:^{ - [self.apost.managedObjectContext save:nil]; - }]; + if (self.isStandalone) { + if (!self.isStandaloneEditorDismissingAfterSave) { + [self.apost.managedObjectContext refreshObject:self.apost mergeChanges:NO]; + } + } else { + [self.apost.managedObjectContext performBlock:^{ + [self.apost.managedObjectContext save:nil]; + }]; + } } - (void)didReceiveMemoryWarning @@ -1373,7 +1380,9 @@ - (void)postCategoriesViewController:(PostCategoriesViewController *)controller // Save changes. self.post.categories = [categories mutableCopy]; - [self.post save]; + if (!self.isStandalone) { + [self.post save]; + } } #pragma mark - PostFeaturedImageCellDelegate diff --git a/WordPress/Classes/ViewRelated/Post/Posts.storyboard b/WordPress/Classes/ViewRelated/Post/Posts.storyboard deleted file mode 100644 index ea2add84b74b..000000000000 --- a/WordPress/Classes/ViewRelated/Post/Posts.storyboard +++ /dev/null @@ -1,104 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/WordPress/Classes/ViewRelated/Post/Prepublishing Nudge/PrepublishingHeaderView.swift b/WordPress/Classes/ViewRelated/Post/Prepublishing Nudge/PrepublishingHeaderView.swift index 708d15c43344..e5969af5db3e 100644 --- a/WordPress/Classes/ViewRelated/Post/Prepublishing Nudge/PrepublishingHeaderView.swift +++ b/WordPress/Classes/ViewRelated/Post/Prepublishing Nudge/PrepublishingHeaderView.swift @@ -91,7 +91,6 @@ class PrepublishingHeaderView: UITableViewHeaderFooterView, NibLoadable { static let backButtonSize = CGSize(width: 28, height: 28) static let imageRadius: CGFloat = 4 static let leftRightInset: CGFloat = 16 - static let title = NSLocalizedString("Publishing To", comment: "Label that describes in which blog the user is publishing to") static let close = NSLocalizedString("Close", comment: "Voiceover accessibility label informing the user that this button dismiss the current view") static let doubleTapToDismiss = NSLocalizedString("Double tap to dismiss", comment: "Voiceover accessibility hint informing the user they can double tap a modal alert to dismiss it") } diff --git a/WordPress/Classes/ViewRelated/Post/Prepublishing Nudge/PrepublishingNavigationController.swift b/WordPress/Classes/ViewRelated/Post/Prepublishing Nudge/PrepublishingNavigationController.swift index d4a2107e2122..4e0924d82e7d 100644 --- a/WordPress/Classes/ViewRelated/Post/Prepublishing Nudge/PrepublishingNavigationController.swift +++ b/WordPress/Classes/ViewRelated/Post/Prepublishing Nudge/PrepublishingNavigationController.swift @@ -86,10 +86,6 @@ class PrepublishingNavigationController: LightNavigationController { navigationBar.scrollEdgeAppearance = appearance navigationBar.compactAppearance = appearance } - - private enum Constants { - static let iPadPreferredContentSize = CGSize(width: 300.0, height: 300.0) - } } diff --git a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingSocialAccountsViewController.swift b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingSocialAccountsViewController.swift index 2b6b3d44bdd4..7546d2221a1e 100644 --- a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingSocialAccountsViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingSocialAccountsViewController.swift @@ -201,7 +201,7 @@ private extension PrepublishingSocialAccountsViewController { } var indexPathsForDisabledConnections: [IndexPath] { - connections.indexed().compactMap { index, _ in + connections.indices.compactMap { index in valueForConnection(at: index) ? nil : IndexPath(row: index, section: .zero) } } diff --git a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController+JetpackSocial.swift b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController+JetpackSocial.swift index 2287ac3ea503..007546ac9d71 100644 --- a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController+JetpackSocial.swift +++ b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController+JetpackSocial.swift @@ -131,12 +131,6 @@ private extension PrepublishingViewController { WPAnalytics.track(.jetpackSocialNoConnectionCardDisplayed, properties: ["source": Constants.trackingSource]) } - func makeNoConnectionView() -> UIView { - let viewModel = makeNoConnectionViewModel() - let controller = JetpackSocialNoConnectionView.createHostController(with: viewModel) - return controller.view - } - func makeNoConnectionViewModel() -> JetpackSocialNoConnectionViewModel { let context = post.managedObjectContext ?? coreDataStack.mainContext guard let services = try? PublicizeService.allSupportedServices(in: context) else { diff --git a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift index 9c618cf8bb5e..56a87acdf351 100644 --- a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift @@ -47,9 +47,9 @@ class PrepublishingViewController: UITableViewController { return PublishSettingsViewModel(post: post) }() - private lazy var presentedVC: DrawerPresentationController? = { + private var presentedVC: DrawerPresentationController? { return (navigationController as? PrepublishingNavigationController)?.presentedVC - }() + } enum CompletionResult { case completed(AbstractPost) @@ -133,13 +133,11 @@ class PrepublishingViewController: UITableViewController { /// Toggles `keyboardShown` as the keyboard notifications come in private func configureKeyboardToggle() { NotificationCenter.default.publisher(for: UIResponder.keyboardDidShowNotification) - .map { _ in return true } - .assign(to: \.keyboardShown, on: self) + .sink { [weak self] _ in self?.keyboardShown = true } .store(in: &cancellables) NotificationCenter.default.publisher(for: UIResponder.keyboardDidHideNotification) - .map { _ in return false } - .assign(to: \.keyboardShown, on: self) + .sink { [weak self] _ in self?.keyboardShown = false } .store(in: &cancellables) } @@ -375,7 +373,6 @@ class PrepublishingViewController: UITableViewController { sourceView: tableView.cellForRow(at: indexPath)?.contentView, sourceRect: nil, viewModel: publishSettingsViewModel, - transitioningDelegate: nil, updated: { [weak self] date in WPAnalytics.track(.editorPostScheduledChanged, properties: Constants.analyticsDefaultProperty) self?.publishSettingsViewModel.setDate(date) diff --git a/WordPress/Classes/ViewRelated/Post/RestorePostTableViewCell.swift b/WordPress/Classes/ViewRelated/Post/RestorePostTableViewCell.swift deleted file mode 100644 index 81ecf62681cd..000000000000 --- a/WordPress/Classes/ViewRelated/Post/RestorePostTableViewCell.swift +++ /dev/null @@ -1,88 +0,0 @@ -import UIKit -import Gridicons - -class RestorePostTableViewCell: UITableViewCell, ConfigurablePostView, InteractivePostView { - @IBOutlet var postContentView: UIView! { - didSet { - postContentView.backgroundColor = .listForeground - } - } - @IBOutlet var restoreLabel: UILabel! - @IBOutlet var restoreButton: UIButton! - @IBOutlet var topMargin: NSLayoutConstraint! - - private weak var delegate: InteractivePostViewDelegate? - - var isCompact: Bool = false { - didSet { - isCompact ? configureCompact() : configureDefault() - } - } - var post: Post? - - func configure(with post: Post) { - self.post = post - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - // Don't respond to taps in margins. - if !postContentView.frame.contains(point) { - return nil - } - return super.hitTest(point, with: event) - } - - override func awakeFromNib() { - super.awakeFromNib() - - configureView() - applyStyles() - } - - private func configureView() { - restoreLabel.text = NSLocalizedString("Post moved to trash.", comment: "A short message explaining that a post was moved to the trash bin.") - let buttonTitle = NSLocalizedString("Undo", comment: "The title of an 'undo' button. Tapping the button moves a trashed post out of the trash folder.") - restoreButton.setTitle(buttonTitle, for: .normal) - restoreButton.setImage(.gridicon(.undo, size: CGSize(width: Constants.imageSize, - height: Constants.imageSize)), for: .normal) - } - - private func configureCompact() { - topMargin.constant = Constants.compactMargin - postContentView.layer.borderWidth = 0 - } - - private func configureDefault() { - topMargin.constant = Constants.defaultMargin - postContentView.layer.borderColor = WPStyleGuide.postCardBorderColor.cgColor - postContentView.layer.borderWidth = .hairlineBorderWidth - } - - private func applyStyles() { - WPStyleGuide.applyPostCardStyle(self) - WPStyleGuide.applyRestorePostLabelStyle(restoreLabel) - WPStyleGuide.applyRestorePostButtonStyle(restoreButton) - } - - @IBAction func restore(_ sender: Any) { - guard let post = post else { - return - } - - delegate?.restore(post) - } - - func setInteractionDelegate(_ delegate: InteractivePostViewDelegate) { - self.delegate = delegate - } - - func setActionSheetDelegate(_ delegate: PostActionSheetDelegate) { - // Do nothing, since this cell doesn't have an action to present an action sheet. - } - - private enum Constants { - static let defaultMargin: CGFloat = 16 - static let compactMargin: CGFloat = 0 - static let imageSize: CGFloat = 18.0 - } -} diff --git a/WordPress/Classes/ViewRelated/Post/RestorePostTableViewCell.xib b/WordPress/Classes/ViewRelated/Post/RestorePostTableViewCell.xib deleted file mode 100644 index 90a2bf15f525..000000000000 --- a/WordPress/Classes/ViewRelated/Post/RestorePostTableViewCell.xib +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/WordPress/Classes/ViewRelated/Post/Revisions/Browser/Preview/RevisionPreviewTextViewManager.swift b/WordPress/Classes/ViewRelated/Post/Revisions/Browser/Preview/RevisionPreviewTextViewManager.swift index 9b53f309e66f..6326b5928aef 100644 --- a/WordPress/Classes/ViewRelated/Post/Revisions/Browser/Preview/RevisionPreviewTextViewManager.swift +++ b/WordPress/Classes/ViewRelated/Post/Revisions/Browser/Preview/RevisionPreviewTextViewManager.swift @@ -12,7 +12,6 @@ class RevisionPreviewTextViewManager: NSObject { static let mediaPlaceholderImageSize = CGSize(width: 128, height: 128) static let placeholderMediaLink = URL(string: "placeholder://") - static let placeholderDocumentLink = URL(string: "documentUploading://") } } diff --git a/WordPress/Classes/ViewRelated/Post/Revisions/Browser/Preview/RevisionPreviewViewController.swift b/WordPress/Classes/ViewRelated/Post/Revisions/Browser/Preview/RevisionPreviewViewController.swift index fa19665386e0..64fd61e057fd 100644 --- a/WordPress/Classes/ViewRelated/Post/Revisions/Browser/Preview/RevisionPreviewViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Revisions/Browser/Preview/RevisionPreviewViewController.swift @@ -15,7 +15,6 @@ class RevisionPreviewViewController: UIViewController, StoryboardLoadable { private let mainContext = ContextManager.sharedInstance().mainContext private let textViewManager = RevisionPreviewTextViewManager() private var titleInsets = UIEdgeInsets(top: 8.0, left: 8.0, bottom: 8.0, right: 8.0) - private var textViewInsets = UIEdgeInsets(top: 0.0, left: 6.0, bottom: 0.0, right: 6.0) private lazy var textView: TextView = { let aztext = TextView(defaultFont: WPFontManager.notoRegularFont(ofSize: 16), diff --git a/WordPress/Classes/ViewRelated/Post/Scheduling/PartScreenPresentationController.swift b/WordPress/Classes/ViewRelated/Post/Scheduling/PartScreenPresentationController.swift deleted file mode 100644 index 1fa71d36c922..000000000000 --- a/WordPress/Classes/ViewRelated/Post/Scheduling/PartScreenPresentationController.swift +++ /dev/null @@ -1,77 +0,0 @@ -import Foundation - -class PartScreenPresentationController: FancyAlertPresentationController { - - var minimumHeight: CGFloat { - return presentedViewController.preferredContentSize.height - } - - private weak var tapGestureRecognizer: UITapGestureRecognizer? - - override var frameOfPresentedViewInContainerView: CGRect { - /// If we are in compact mode, don't override the default - guard traitCollection.verticalSizeClass != .compact else { - return super.frameOfPresentedViewInContainerView - } - guard let containerView = containerView else { - return .zero - } - let height = max(containerView.bounds.height/2, minimumHeight) - let width = containerView.bounds.width - - return CGRect(x: 0, y: containerView.bounds.height - height, width: width, height: height) - } - - override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { - coordinator.animate(alongsideTransition: { _ in - self.presentedView?.frame = self.frameOfPresentedViewInContainerView - }, completion: nil) - super.viewWillTransition(to: size, with: coordinator) - } - - override func preferredContentSizeDidChange(forChildContentContainer container: UIContentContainer) { - super.preferredContentSizeDidChange(forChildContentContainer: container) - presentedViewController.view.frame = frameOfPresentedViewInContainerView - } - - override func containerViewDidLayoutSubviews() { - super.containerViewDidLayoutSubviews() - - if tapGestureRecognizer == nil { - addGestureRecognizer() - } - } - - private func addGestureRecognizer() { - let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismiss)) - gestureRecognizer.cancelsTouchesInView = false - gestureRecognizer.delegate = self - containerView?.addGestureRecognizer(gestureRecognizer) - tapGestureRecognizer = gestureRecognizer - } - - /// This may need to be added to FancyAlertPresentationController - override var shouldPresentInFullscreen: Bool { - return false - } - - @objc func dismiss() { - presentedViewController.dismiss(animated: true, completion: nil) - } -} - -extension PartScreenPresentationController: UIGestureRecognizerDelegate { - func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { - - /// Shouldn't happen; should always have container & presented view when tapped - guard let containerView = containerView, let presentedView = presentedView else { - return false - } - - let touchPoint = touch.location(in: containerView) - let isInPresentedView = presentedView.frame.contains(touchPoint) - - /// Do not accept the touch if inside of the presented view - return (gestureRecognizer == tapGestureRecognizer) && isInPresentedView == false - } -} diff --git a/WordPress/Classes/ViewRelated/Post/Scheduling/PublishSettingsViewController.swift b/WordPress/Classes/ViewRelated/Post/Scheduling/PublishSettingsViewController.swift index 83cf39bb0ac3..577e174ffc4e 100644 --- a/WordPress/Classes/ViewRelated/Post/Scheduling/PublishSettingsViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Scheduling/PublishSettingsViewController.swift @@ -49,7 +49,7 @@ struct PublishSettingsViewModel { self.post = post title = post.postTitle - timeZone = post.blog.timeZone + timeZone = post.blog.timeZone ?? TimeZone.current dateFormatter = SiteDateFormatters.dateFormatter(for: timeZone, dateStyle: .long, timeStyle: .none) dateTimeFormatter = SiteDateFormatters.dateFormatter(for: timeZone, dateStyle: .medium, timeStyle: .short) @@ -202,16 +202,16 @@ private struct DateAndTimeRow: ImmuTableRow { func dateTimeCalendarViewController(with model: PublishSettingsViewModel) -> (ImmuTableRow) -> UIViewController { return { [weak self] _ in - return PresentableSchedulingViewControllerProvider.viewController(sourceView: self?.viewController?.tableView, - sourceRect: self?.rectForSelectedRow() ?? .zero, - viewModel: model, - transitioningDelegate: self, - updated: { [weak self] date in - WPAnalytics.track(.editorPostScheduledChanged, properties: ["via": "settings"]) - self?.viewModel.setDate(date) - NotificationCenter.default.post(name: Foundation.Notification.Name(rawValue: ImmuTableViewController.modelChangedNotification), object: nil) - }, - onDismiss: nil) + return PresentableSchedulingViewControllerProvider.viewController( + sourceView: self?.viewController?.tableView, + sourceRect: self?.rectForSelectedRow() ?? .zero, + viewModel: model, + updated: { [weak self] date in + WPAnalytics.track(.editorPostScheduledChanged, properties: ["via": "settings"]) + self?.viewModel.setDate(date) + NotificationCenter.default.post(name: Foundation.Notification.Name(rawValue: ImmuTableViewController.modelChangedNotification), object: nil) + }, + onDismiss: nil) } } @@ -223,16 +223,3 @@ private struct DateAndTimeRow: ImmuTableRow { return viewController.tableView.rectForRow(at: selectedIndexPath) } } - -// The calendar sheet is shown towards the bottom half of the screen so a custom transitioning delegate is needed. -extension PublishSettingsController: UIViewControllerTransitioningDelegate, UIAdaptivePresentationControllerDelegate { - func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { - let presentationController = PartScreenPresentationController(presentedViewController: presented, presenting: presenting) - presentationController.delegate = self - return presentationController - } - - func adaptivePresentationStyle(for: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle { - return traitCollection.verticalSizeClass == .compact ? .overFullScreen : .none - } -} diff --git a/WordPress/Classes/ViewRelated/Post/Scheduling/SchedulingDatePickerViewController.swift b/WordPress/Classes/ViewRelated/Post/Scheduling/SchedulingDatePickerViewController.swift index 439148936c99..86e9b5ff0c55 100644 --- a/WordPress/Classes/ViewRelated/Post/Scheduling/SchedulingDatePickerViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Scheduling/SchedulingDatePickerViewController.swift @@ -25,7 +25,7 @@ class DateCoordinator { // MARK: - Date Picker -class SchedulingDatePickerViewController: UIViewController, DatePickerSheet, DateCoordinatorHandler, UIViewControllerTransitioningDelegate, UIAdaptivePresentationControllerDelegate { +class SchedulingDatePickerViewController: UIViewController, DatePickerSheet, DateCoordinatorHandler { var coordinator: DateCoordinator? = nil @@ -128,18 +128,6 @@ class SchedulingDatePickerViewController: UIViewController, DatePickerSheet, Dat } } -extension SchedulingDatePickerViewController { - @objc func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { - let presentationController = PartScreenPresentationController(presentedViewController: presented, presenting: presenting) - presentationController.delegate = self - return presentationController - } - - @objc func adaptivePresentationStyle(for: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle { - return traitCollection.verticalSizeClass == .compact ? .overFullScreen : .none - } -} - // MARK: Accessibility private extension SchedulingDatePickerViewController { diff --git a/WordPress/Classes/ViewRelated/Post/Scheduling/SchedulingViewControllerPresenter.swift b/WordPress/Classes/ViewRelated/Post/Scheduling/SchedulingViewControllerPresenter.swift index 2ac06408e9f4..4cc36b9ce7ca 100644 --- a/WordPress/Classes/ViewRelated/Post/Scheduling/SchedulingViewControllerPresenter.swift +++ b/WordPress/Classes/ViewRelated/Post/Scheduling/SchedulingViewControllerPresenter.swift @@ -1,49 +1,33 @@ import Foundation import UIKit -protocol PresentableSchedulingViewControllerProviding { +class PresentableSchedulingViewControllerProvider { static func viewController(sourceView: UIView?, sourceRect: CGRect?, viewModel: PublishSettingsViewModel, - transitioningDelegate: UIViewControllerTransitioningDelegate?, - updated: @escaping (Date?) -> Void, - onDismiss: (() -> Void)?) -> UINavigationController -} - -class PresentableSchedulingViewControllerProvider: PresentableSchedulingViewControllerProviding { - static func viewController(sourceView: UIView?, - sourceRect: CGRect?, - viewModel: PublishSettingsViewModel, - transitioningDelegate: UIViewControllerTransitioningDelegate?, updated: @escaping (Date?) -> Void, onDismiss: (() -> Void)?) -> UINavigationController { let schedulingViewController = schedulingViewController(with: viewModel, updated: updated) return wrappedSchedulingViewController(schedulingViewController, sourceView: sourceView, sourceRect: sourceRect, - transitioningDelegate: transitioningDelegate, onDismiss: onDismiss) } static func wrappedSchedulingViewController(_ schedulingViewController: SchedulingDatePickerViewController, sourceView: UIView?, sourceRect: CGRect?, - transitioningDelegate: UIViewControllerTransitioningDelegate?, onDismiss: (() -> Void)?) -> SchedulingLightNavigationController { let vc = SchedulingLightNavigationController(rootViewController: schedulingViewController) vc.onDismiss = onDismiss if UIDevice.isPad() { vc.modalPresentationStyle = .popover - } else { - vc.modalPresentationStyle = .custom - vc.transitioningDelegate = transitioningDelegate ?? schedulingViewController - } - - if let popoverController = vc.popoverPresentationController, - let sourceView = sourceView { - popoverController.sourceView = sourceView - popoverController.sourceRect = sourceRect ?? sourceView.frame + if let popoverController = vc.popoverPresentationController, + let sourceView = sourceView { + popoverController.sourceView = sourceView + popoverController.sourceRect = sourceRect ?? sourceView.frame + } } return vc } diff --git a/WordPress/Classes/ViewRelated/Post/Search/PostSearchService.swift b/WordPress/Classes/ViewRelated/Post/Search/PostSearchService.swift new file mode 100644 index 000000000000..797babe82197 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Post/Search/PostSearchService.swift @@ -0,0 +1,99 @@ +import Foundation +import CoreData + +protocol PostSearchServiceDelegate: AnyObject { + func service(_ service: PostSearchService, didAppendPosts page: [AbstractPost]) + func serviceDidUpdateState(_ service: PostSearchService) +} + +/// Loads post search results with pagination. +final class PostSearchService { + private(set) var isLoading = false + private(set) var error: Error? + + weak var delegate: PostSearchServiceDelegate? + + let criteria: PostSearchCriteria + private let blog: Blog + private let settings: PostListFilterSettings + private let coreDataStack: CoreDataStack + private let repository: PostRepository + + private var postIDs: Set = [] + private var offset = 0 + private var hasMore = true + + init(blog: Blog, + settings: PostListFilterSettings, + criteria: PostSearchCriteria, + coreDataStack: CoreDataStackSwift = ContextManager.shared + ) { + self.blog = blog + self.settings = settings + self.criteria = criteria + self.coreDataStack = coreDataStack + self.repository = PostRepository(coreDataStack: coreDataStack) + } + + func loadMore() { + guard !isLoading && hasMore else { + return + } + isLoading = true + error = nil + delegate?.serviceDidUpdateState(self) + + _loadMore() + } + + private func _loadMore() { + let postType = settings.postType == .post ? Post.self : Page.self + let blogID = TaggedManagedObjectID(blog) + + Task { @MainActor [weak self, offset, criteria, repository, coreDataStack] in + let result: Result<[AbstractPost], Error> + do { + let postIDs: [TaggedManagedObjectID] = try await repository.search( + type: postType, + input: criteria.searchTerm, + statuses: [], + tag: criteria.tag, + authorUserID: criteria.authorID, + offset: offset, + limit: 20, + orderBy: .byDate, + descending: true, + in: blogID + ) + result = try .success(postIDs.map { try coreDataStack.mainContext.existingObject(with: $0) }) + } catch { + result = .failure(error) + } + self?.didLoad(with: result) + } + } + + private func didLoad(with result: Result<[AbstractPost], Error>) { + assert(Thread.isMainThread) + + switch result { + case .success(let posts): + offset += posts.count + hasMore = !posts.isEmpty + + let newPosts = posts.filter { !postIDs.contains($0.objectID) } + postIDs.formUnion(newPosts.map(\.objectID)) + self.delegate?.service(self, didAppendPosts: newPosts) + case .failure(let error): + self.error = error + } + isLoading = false + delegate?.serviceDidUpdateState(self) + } +} + +struct PostSearchCriteria: Hashable { + let searchTerm: String + let authorID: NSNumber? + let tag: String? +} diff --git a/WordPress/Classes/ViewRelated/Post/Search/PostSearchSuggestionsService.swift b/WordPress/Classes/ViewRelated/Post/Search/PostSearchSuggestionsService.swift new file mode 100644 index 000000000000..ca50302c9160 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Post/Search/PostSearchSuggestionsService.swift @@ -0,0 +1,112 @@ +import UIKit + +/// Suggests search token for the given input and context. Performs all of the +/// work in the background. +actor PostSearchSuggestionsService { + private let blogID: TaggedManagedObjectID + private let isEnabled: Bool + private var cachedAuthorTokens: [PostSearchAuthorToken]? + private var cachedTags: [PostSearchTagToken]? + private let coreData: CoreDataStack + + init(blog: Blog, coreData: CoreDataStack = ContextManager.shared) { + self.blogID = TaggedManagedObjectID(blog) + self.isEnabled = blog.isAccessibleThroughWPCom() + self.coreData = coreData + } + + func getSuggestion(for searchTerm: String, selectedTokens: [any PostSearchToken]) async -> [any PostSearchToken] { + guard searchTerm.count > 1 else { + return [] // Not enough input + } + guard isEnabled else { + return [] + } + async let authors = getAuthorTokens(for: searchTerm, selectedTokens: selectedTokens) + async let tags = getTagTokens(for: searchTerm, selectedTokens: selectedTokens) + + let tokens = await [authors, tags] + let selectedTokenIDs = Set(selectedTokens.map(\.id)) + + let output = Array(tokens + .flatMap { $0 } + .filter { !selectedTokenIDs.contains($0.token.id) } + .sorted { ($0.score, $0.token.value) > ($1.score, $1.token.value) } + .map { $0.token } + .prefix(3)) + + // Remove duplicates + var encounteredIDs = Set() + return output.filter { encounteredIDs.insert($0.id).inserted } + } + + private struct RankedToken { + let token: PostSearchToken + let score: Double + } + + // MARK: - Authors + + private func getAuthorTokens(for searchTerm: String, selectedTokens: [any PostSearchToken]) async -> [RankedToken] { + guard !selectedTokens.contains(where: { $0 is PostSearchAuthorToken }) else { + return [] // Don't suggest authors anymore + } + let tokens = await getAllAuthorTokens() + guard tokens.count > 1 else { + return [] // Never show for blogs with a single author + } + let search = StringRankedSearch(searchTerm: searchTerm) + return tokens.compactMap { + let score = search.score(for: $0.displayName) + guard score > 0.7 else { return nil } + return RankedToken(token: $0, score: score) + } + } + + private func getAllAuthorTokens() async -> [PostSearchAuthorToken] { + if let tokens = cachedAuthorTokens { + return tokens + } + let tokens = try? await coreData.performQuery { [blogID] context in + let blog = try context.existingObject(with: blogID) + return (blog.authors ?? []).map(PostSearchAuthorToken.init) + } + self.cachedAuthorTokens = tokens + Task { // Invalidate cache after a few seconds + try? await Task.sleep(nanoseconds: 5_000_000_000) + self.cachedAuthorTokens = nil + } + return tokens ?? [] + } + + // MARK: - Tags + + private func getTagTokens(for searchTerm: String, selectedTokens: [any PostSearchToken]) async -> [RankedToken] { + guard !selectedTokens.contains(where: { $0 is PostSearchTagToken }) else { + return [] // Don't suggest tags anymore + } + let tokens = await getAllTagTokens() + let search = StringRankedSearch(searchTerm: searchTerm) + return tokens.compactMap { + let score = search.score(for: $0.tag) + guard score > 0.7 else { return nil } + return RankedToken(token: $0, score: score) + } + } + + private func getAllTagTokens() async -> [PostSearchTagToken] { + let tags = try? await coreData.performQuery { [blogID] context in + let blog = try context.existingObject(with: blogID) + let tags = (blog.tags as? Set) ?? [] + return tags.compactMap { + $0.name.map(PostSearchTagToken.init) + } + } + self.cachedTags = tags + Task { // Invalidate cache after a few seconds + try? await Task.sleep(nanoseconds: 5_000_000_000) + self.cachedTags = nil + } + return tags ?? [] + } +} diff --git a/WordPress/Classes/ViewRelated/Post/Search/PostSearchToken.swift b/WordPress/Classes/ViewRelated/Post/Search/PostSearchToken.swift new file mode 100644 index 000000000000..f0272ceb8512 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Post/Search/PostSearchToken.swift @@ -0,0 +1,37 @@ +import Foundation + +protocol PostSearchToken { + var icon: UIImage? { get } + var value: String { get } + var id: AnyHashable { get } +} + +extension PostSearchToken { + func asSearchToken() -> UISearchToken { + let token = UISearchToken(icon: icon, text: value) + token.representedObject = self + return token + } +} + +struct PostSearchAuthorToken: Hashable, PostSearchToken { + let authorID: NSNumber + let displayName: String? + + var icon: UIImage? { UIImage(named: "comment-author-gravatar") } + var value: String { displayName ?? "" } + var id: AnyHashable { self } + + init(author: BlogAuthor) { + self.authorID = author.userID + self.displayName = author.displayName + } +} + +struct PostSearchTagToken: Hashable, PostSearchToken { + let tag: String + + var icon: UIImage? { UIImage(named: "block-tag-cloud") } + var value: String { tag } + var id: AnyHashable { self } +} diff --git a/WordPress/Classes/ViewRelated/Post/Search/PostSearchTokenTableCell.swift b/WordPress/Classes/ViewRelated/Post/Search/PostSearchTokenTableCell.swift new file mode 100644 index 000000000000..70d104acf5d7 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Post/Search/PostSearchTokenTableCell.swift @@ -0,0 +1,53 @@ +import UIKit + +final class PostSearchTokenTableCell: UITableViewCell { + private let iconView = UIImageView() + private let titleLabel = UILabel() + private lazy var stackView = UIStackView(arrangedSubviews: [ + iconView, titleLabel, UIView() + ]) + private let separator = UIView() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + selectionStyle = .none + + iconView.tintColor = .secondaryLabel + + separator.backgroundColor = .separator + + stackView.spacing = 8 + stackView.alignment = .center + stackView.isLayoutMarginsRelativeArrangement = true + stackView.layoutMargins = UIEdgeInsets(top: 4, left: 16, bottom: 12, right: 16) + stackView.translatesAutoresizingMaskIntoConstraints = false + + contentView.addSubview(stackView) + contentView.pinSubviewToAllEdges(stackView) + + // The native one doesn't quite work the way we need if there is only one result + contentView.addSubview(separator) + separator.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + separator.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + separator.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + separator.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + separator.heightAnchor.constraint(equalToConstant: 0.5) + ]) + } + + required init?(coder: NSCoder) { + fatalError("Not implemented") + } + + func configure(with token: any PostSearchToken, isLast: Bool) { + iconView.image = token.icon + titleLabel.text = token.value + configure(isLast: isLast) + } + + func configure(isLast: Bool) { + separator.isHidden = !isLast + } +} diff --git a/WordPress/Classes/ViewRelated/Post/Search/PostSearchViewController.swift b/WordPress/Classes/ViewRelated/Post/Search/PostSearchViewController.swift new file mode 100644 index 000000000000..1e4b1aa8b95c --- /dev/null +++ b/WordPress/Classes/ViewRelated/Post/Search/PostSearchViewController.swift @@ -0,0 +1,254 @@ +import UIKit +import Combine + +final class PostSearchViewController: UIViewController, UITableViewDelegate, UISearchControllerDelegate, UISearchResultsUpdating { + private weak var searchController: UISearchController? + private weak var delegate: InteractivePostViewDelegate? + + private typealias SectionID = PostSearchViewModel.SectionID + private typealias ItemID = PostSearchViewModel.ItemID + + private let tableView = UITableView(frame: .zero, style: .plain) + + private lazy var dataSource = UITableViewDiffableDataSource (tableView: tableView) { [weak self] tableView, indexPath, itemIdentifier in + self?.tableView(tableView, cellForRowAt: indexPath) + } + + private let viewModel: PostSearchViewModel + + private var cancellables: [AnyCancellable] = [] + + init(viewModel: PostSearchViewModel) { + self.viewModel = viewModel + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func configure(_ searchController: UISearchController, _ delegate: InteractivePostViewDelegate?) { + searchController.delegate = self + searchController.searchResultsUpdater = self + searchController.showsSearchResultsController = true + + self.searchController = searchController + self.delegate = delegate + } + + override func viewDidLoad() { + super.viewDidLoad() + + configureTableView() + bindViewModel() + } + + private func configureTableView() { + view.addSubview(tableView) + tableView.translatesAutoresizingMaskIntoConstraints = false + view.pinSubviewToAllEdges(tableView) + + tableView.register(PostSearchTokenTableCell.self, forCellReuseIdentifier: Constants.tokenCellID) + tableView.register(PostListCell.self, forCellReuseIdentifier: Constants.postCellID) + tableView.register(PageListCell.self, forCellReuseIdentifier: Constants.pageCellID) + + tableView.dataSource = dataSource + tableView.delegate = self + tableView.sectionHeaderTopPadding = 0 + } + + private func bindViewModel() { + viewModel.$snapshot.sink { [weak self] in + self?.dataSource.apply($0, animatingDifferences: $0.reloadedItemIdentifiers.count == 1) + self?.updateSuggestedTokenCells() + }.store(in: &cancellables) + + viewModel.$searchTerm.removeDuplicates().sink { [weak self] in + if self?.searchController?.searchBar.text != $0 { + self?.searchController?.searchBar.text = $0 + } + self?.updateHighlightsForVisibleCells(searchTerm: $0) + }.store(in: &cancellables) + + viewModel.$selectedTokens + .removeDuplicates { $0.map(\.id) == $1.map(\.id) } + .sink { [weak self] in + self?.searchController?.searchBar.searchTextField.tokens = $0.map { + $0.asSearchToken() + } + }.store(in: &cancellables) + + viewModel.$footerState + .throttle(for: 0.33, scheduler: DispatchQueue.main, latest: true) + .removeDuplicates() + .sink { [weak self] in self?.didUpdateFooterState($0) } + .store(in: &cancellables) + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + tableView.sizeToFitFooterView() + } + + // MARK: - UITableViewDataSource + + private func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + switch SectionID(rawValue: indexPath.section)! { + case .tokens: + let cell = tableView.dequeueReusableCell(withIdentifier: Constants.tokenCellID, for: indexPath) as! PostSearchTokenTableCell + let token = viewModel.suggestedTokens[indexPath.row] + let isLast = indexPath.row == viewModel.suggestedTokens.count - 1 + cell.configure(with: token, isLast: isLast) + cell.separatorInset = UIEdgeInsets(top: 0, left: view.bounds.size.width, bottom: 0, right: 0) // Hide the native separator + return cell + case .posts: + let post = viewModel.posts[indexPath.row].latest() + switch post { + case let post as Post: + let cell = tableView.dequeueReusableCell(withIdentifier: Constants.postCellID, for: indexPath) as! PostListCell + let viewModel = PostListItemViewModel(post: post) + cell.configure(with: viewModel, delegate: delegate) + updateHighlights(for: [cell], searchTerm: self.viewModel.searchTerm) + return cell + case let page as Page: + let cell = tableView.dequeueReusableCell(withIdentifier: Constants.pageCellID, for: indexPath) as! PageListCell + let viewModel = PageListItemViewModel(page: page) + cell.configure(with: viewModel, delegate: delegate) + updateHighlights(for: [cell], searchTerm: self.viewModel.searchTerm) + return cell + default: + fatalError("Unsupported item: \(type(of: post))") + } + } + } + + // The diffable data source prevents the reloads of the existing cells + private func updateSuggestedTokenCells() { + for indexPath in tableView.indexPathsForVisibleRows ?? [] { + if let cell = tableView.cellForRow(at: indexPath) as? PostSearchTokenTableCell { + let isLast = indexPath.row == viewModel.suggestedTokens.count - 1 + cell.configure(isLast: isLast) + } + } + } + + private func didUpdateFooterState(_ state: PagingFooterView.State?) { + guard let state else { + tableView.tableFooterView = nil + return + } + switch state { + case .loading: + tableView.tableFooterView = PagingFooterView(state: .loading) + case .error: + let footerView = PagingFooterView(state: .error) + footerView.buttonRetry.addAction(UIAction { [viewModel] _ in + viewModel.didTapRefreshButton() + }, for: .touchUpInside) + tableView.tableFooterView = footerView + } + tableView.sizeToFitFooterView() + } + + // MARK: - UITableViewDelegate + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + switch SectionID(rawValue: indexPath.section)! { + case .tokens: + viewModel.didSelectToken(at: indexPath.row) + case .posts: + // TODO: Move to viewWillAppear (the way editor is displayed doesn't allow) + tableView.deselectRow(at: indexPath, animated: true) + + switch viewModel.posts[indexPath.row].latest() { + case let post as Post: + guard post.status != .trash else { return } + delegate?.edit(post) + case let page as Page: + guard page.status != .trash else { return } + delegate?.edit(page) + default: + fatalError("Unsupported post") + } + } + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if scrollView.contentOffset.y + scrollView.frame.size.height > scrollView.contentSize.height - 500 { + viewModel.didReachBottom() + } + } + + func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { + guard indexPath.section == SectionID.posts.rawValue else { return nil } + return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { [weak self] _ in + guard let self, let delegate = self.delegate else { return nil } + let post = self.viewModel.posts[indexPath.row] + let cell = self.tableView.cellForRow(at: indexPath) + return AbstractPostMenuHelper(post).makeMenu(presentingView: cell ?? UIView(), delegate: delegate) + } + } + + func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + guard let delegate, indexPath.section == SectionID.posts.rawValue else { return nil } + let actions = AbstractPostHelper.makeLeadingContextualActions(for: viewModel.posts[indexPath.row], delegate: delegate) + return UISwipeActionsConfiguration(actions: actions) + } + + func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + guard let delegate, indexPath.section == SectionID.posts.rawValue else { return nil } + let actions = AbstractPostHelper.makeTrailingContextualActions(for: viewModel.posts[indexPath.row], delegate: delegate) + return UISwipeActionsConfiguration(actions: actions) + } + + // MARK: - UISearchControllerDelegate + + func willPresentSearchController(_ searchController: UISearchController) { + viewModel.willStartSearching() + } + + // MARK: - UISearchResultsUpdating + + func updateSearchResults(for searchController: UISearchController) { + let searchBar = searchController.searchBar + viewModel.searchTerm = searchBar.text ?? "" + viewModel.selectedTokens = searchBar.searchTextField.tokens.map { + $0.representedObject as! PostSearchToken + } + } + + // MARK: - Highlighter + + private func updateHighlightsForVisibleCells(searchTerm: String) { + let cells = (tableView.indexPathsForVisibleRows ?? []) + .compactMap(tableView.cellForRow) + updateHighlights(for: cells, searchTerm: searchTerm) + } + + private func updateHighlights(for cells: [UITableViewCell], searchTerm: String) { + let terms = searchTerm + .trimmingCharacters(in: .whitespaces) + .components(separatedBy: .whitespaces) + .filter { !$0.isEmpty } + for cell in cells { + guard let cell = cell as? PostSearchResultCell else { continue } + + assert(cell.attributedText != nil) + let string = NSMutableAttributedString(attributedString: cell.attributedText ?? .init()) + PostSearchViewModel.highlight(terms: terms, in: string) + cell.attributedText = string + } + } +} + +private enum Constants { + static let postCellID = "postCellID" + static let pageCellID = "pageCellID" + static let tokenCellID = "suggestedTokenCellID" +} + +protocol PostSearchResultCell: AnyObject { + var attributedText: NSAttributedString? { get set } +} diff --git a/WordPress/Classes/ViewRelated/Post/Search/PostSearchViewModel+Highlighter.swift b/WordPress/Classes/ViewRelated/Post/Search/PostSearchViewModel+Highlighter.swift new file mode 100644 index 000000000000..36f175166a06 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Post/Search/PostSearchViewModel+Highlighter.swift @@ -0,0 +1,71 @@ +import UIKit + +extension PostSearchViewModel { + static func highlight(terms: [String], in attributedString: NSMutableAttributedString) { + attributedString.removeAttribute(.backgroundColor, range: NSRange(location: 0, length: attributedString.length)) + + let string = attributedString.string + + let ranges = terms.flatMap { + string.ranges(of: $0, options: [.caseInsensitive, .diacriticInsensitive]) + }.sorted { $0.lowerBound < $1.lowerBound } + + for range in collapseAdjacentRanges(ranges, in: string) { + attributedString.addAttributes([ + .backgroundColor: UIColor.systemYellow.withAlphaComponent(0.25) + ], range: NSRange(range, in: string)) + } + } + + // Both decoding & searching are expensive, so the service performs these + // operations in the background. + static func higlight(_ title: String, terms: [String]) -> NSAttributedString { + let title = title + .trimmingCharacters(in: .whitespaces) + .stringByDecodingXMLCharacters() + + let ranges = terms.flatMap { + title.ranges(of: $0, options: [.caseInsensitive, .diacriticInsensitive]) + }.sorted { $0.lowerBound < $1.lowerBound } + + let string = NSMutableAttributedString(string: title, attributes: [ + .font: WPStyleGuide.fontForTextStyle(.body) + ]) + for range in collapseAdjacentRanges(ranges, in: title) { + string.setAttributes([ + .backgroundColor: UIColor.systemYellow.withAlphaComponent(0.25) + ], range: NSRange(range, in: title)) + } + return string + } + + private static func collapseAdjacentRanges(_ ranges: [Range], in string: String) -> [Range] { + var output: [Range] = [] + var ranges = ranges + while let rhs = ranges.popLast() { + if let lhs = ranges.last, + rhs.lowerBound > string.startIndex, + lhs.upperBound == string.index(before: rhs.lowerBound), + string[string.index(before: rhs.lowerBound)].isWhitespace { + ranges.removeLast() + ranges.append(lhs.lowerBound.. [Range] { + var ranges: [Range] = [] + var startIndex = self.startIndex + while startIndex < endIndex, + let range = range(of: string, options: options, range: startIndex..() + + enum SectionID: Int, CaseIterable { + case tokens = 0 + case posts + } + + enum ItemID: Hashable { + case token(AnyHashable) + case post(NSManagedObjectID) + } + + private(set) var suggestedTokens: [any PostSearchToken] = [] + private(set) var posts: [AbstractPost] = [] + + private let blog: Blog + private let settings: PostListFilterSettings + private let coreData: CoreDataStack + private let entityName: String + + private var searchService: PostSearchService? + private let suggestionsService: PostSearchSuggestionsService + private var suggestionsTask: Task? + private var isRefreshing = false + private var cancellables: [AnyCancellable] = [] + + init(blog: Blog, + filters: PostListFilterSettings, + coreData: CoreDataStack = ContextManager.shared + ) { + self.blog = blog + self.settings = filters + self.coreData = coreData + self.suggestionsService = PostSearchSuggestionsService(blog: blog, coreData: coreData) + + switch settings.postType { + case .post: self.entityName = String(describing: Post.self) + case .page: self.entityName = String(describing: Page.self) + default: fatalError("Unsupported post type: \(settings.postType)") + } + + super.init() + + $searchTerm + .dropFirst() + .removeDuplicates() + .sink { [weak self] in self?.updateSuggestedTokens(for: $0) } + .store(in: &cancellables) + + $searchTerm.map { $0.trimmingCharacters(in: .whitespaces) } + .combineLatest($selectedTokens) + .dropFirst() + .throttle(for: 1.0, scheduler: DispatchQueue.main, latest: true) + .removeDuplicates { $0.0 == $1.0 && $0.1.map(\.id) == $1.1.map(\.id) } + .sink { [weak self] _ in self?.performRemoteSearch() } + .store(in: &cancellables) + + NotificationCenter.default + .publisher(for: NSManagedObjectContext.didChangeObjectsNotification, object: coreData.mainContext) + .sink { [weak self] in self?.reload(with: $0) } + .store(in: &cancellables) + + NotificationCenter.default + .publisher(for: .postCoordinatorDidUpdate, object: nil) + .sink { [weak self] in self?.reload(with: $0) } + .store(in: &cancellables) + + reload() + } + + // MARK: - Snapshot + + private func reload() { + snapshot = makeSnapshot() + } + + private func reload(with notification: Foundation.Notification) { + guard let userInfo = notification.userInfo else { return } + + // The list displays the latest versions of a post when available, + // but it uses the original posts as identifiers (because they are stable). + // This method ensures that the list updates whenever either the + // original version or the latest version changes. + let existingOriginalPosts = Set(posts) + var existingLatestPosts: [NSManagedObject: NSManagedObject] = [:] + for object in posts { + existingLatestPosts[object] = object.latest() + } + + let updatedObjects = (userInfo[NSUpdatedObjectsKey] as? Set) ?? [] + var updatedPosts = updatedObjects.intersection(existingOriginalPosts) + for (original, latest) in existingLatestPosts { + if updatedObjects.contains(latest) { + updatedPosts.insert(original) + } + } + + let deletedPosts = ((userInfo[NSDeletedObjectsKey] as? Set) ?? []) + .intersection(existingOriginalPosts) + + guard !updatedPosts.isEmpty || !deletedPosts.isEmpty else { + return + } + + var snapshot = makeSnapshot() + + snapshot.reloadItems(updatedPosts.map({ ItemID.post($0.objectID) })) + + for object in deletedPosts { + if let post = object as? AbstractPost, + let index = posts.firstIndex(of: post) { + posts.remove(at: index) + } + } + snapshot.deleteItems(deletedPosts.map({ ItemID.post($0.objectID) })) + + self.snapshot = snapshot + } + + private func makeSnapshot() -> NSDiffableDataSourceSnapshot { + var snapshot = NSDiffableDataSourceSnapshot() + + snapshot.appendSections([SectionID.tokens]) + let tokenIDs = suggestedTokens.map { ItemID.token($0.id) } + snapshot.appendItems(tokenIDs, toSection: SectionID.tokens) + + snapshot.appendSections([SectionID.posts]) + let postIDs = posts.map { ItemID.post($0.objectID) } + snapshot.appendItems(postIDs, toSection: SectionID.posts) + + return snapshot + } + + // MARK: - Events + + func didReachBottom() { + guard let searchService, searchService.error == nil else { return } + searchService.loadMore() + } + + func didTapRefreshButton() { + searchService?.loadMore() + } + + func didSelectToken(at index: Int) { + let token = suggestedTokens[index] + cancelCurrentRemoteSearch() + suggestedTokens = [] + posts = [] + selectedTokens.append(token) + searchTerm = "" + reload() + } + + func willStartSearching() { + WPAnalytics.track(.postListSearchOpened, withProperties: propertiesForAnalytics()) + + syncTags() + } + + // MARK: - Search (Remote) + + private func performRemoteSearch() { + cancelCurrentRemoteSearch() + + guard searchTerm.count > 1 || !selectedTokens.isEmpty else { + if !posts.isEmpty { + posts = [] + reload() + } + return + } + + self.isRefreshing = true // Order is important + + let criteria = PostSearchCriteria( + searchTerm: searchTerm, + authorID: getSelectedAuthorID(), + tag: selectedTokens.lazy.compactMap({ $0 as? PostSearchTagToken }).first?.tag + ) + let service = PostSearchService(blog: blog, settings: settings, criteria: criteria) + service.delegate = self + service.loadMore() + self.searchService = service + } + + private func cancelCurrentRemoteSearch() { + // Stop receiving updates from the previous service + self.searchService?.delegate = nil + } + + private func getSelectedAuthorID() -> NSNumber? { + if let token = selectedTokens.lazy.compactMap({ $0 as? PostSearchAuthorToken }).first { + return token.authorID + } + return nil + } + + // MARK: - PostSearchServiceDelegate + + func service(_ service: PostSearchService, didAppendPosts posts: [AbstractPost]) { + assert(Thread.isMainThread) + + if isRefreshing { + self.posts = posts + isRefreshing = false + } else { + self.posts += posts + } + reload() + } + + func serviceDidUpdateState(_ service: PostSearchService) { + assert(Thread.isMainThread) + + if isRefreshing && service.error != nil { + posts = [] + } + if service.isLoading && (!isRefreshing || posts.isEmpty) { + footerState = .loading + } else if service.error != nil { + footerState = .error + } else { + footerState = nil + } + } + + // MARK: - Search Tokens + + private func updateSuggestedTokens(for searchTerm: String) { + let selectedTokens = self.selectedTokens + suggestionsTask?.cancel() + suggestionsTask = Task { @MainActor in + let tokens = await suggestionsService.getSuggestion(for: searchTerm, selectedTokens: selectedTokens) + guard !Task.isCancelled else { return } + self.suggestedTokens = tokens + self.reload() + } + } + + // MARK: - Misc + + private func syncTags() { + let tagsService = PostTagService(managedObjectContext: coreData.mainContext) + tagsService.syncTags(for: blog, success: { _ in }, failure: { _ in }) + } + + private func propertiesForAnalytics() -> [String: AnyObject] { + var properties = [String: AnyObject]() + properties["type"] = settings.postType.rawValue as AnyObject? + if let dotComID = blog.dotComID { + properties[WPAppAnalyticsKeyBlogID] = dotComID + } + return properties + } +} diff --git a/WordPress/Classes/ViewRelated/Post/WPPickerView.h b/WordPress/Classes/ViewRelated/Post/WPPickerView.h deleted file mode 100644 index e0a68c5099e8..000000000000 --- a/WordPress/Classes/ViewRelated/Post/WPPickerView.h +++ /dev/null @@ -1,64 +0,0 @@ -#import - -@protocol WPPickerViewDelegate; - -@interface WPPickerView : UIView - -@property (nonatomic, weak) id delegate; - -/** - Create a WPPickerViewController with a UIDatePicker starting at the specified date. - - @param date The currently selected date. - */ -- (id)initWithDate:(NSDate *)date; - -/** - Create a WPPickerViewController with a UIPicker using the provided array as its - datasource. - - @param dataSource Accepts an NSArray of NSArrays of NSStrings. The picker displays - one compontent for each array of strings. - - @param startingIndexes Optional. An NSIndexPath corresponding the starting - selected indexes in the dataSource. - */ -- (id)initWithDataSource:(NSArray *)dataSource andStartingIndexes:(NSIndexPath *)indexPath; - -/** - Returns an array of UIBarButtonItems to show in the toolbar. Flexible spacers will - be automattically added bewteen buttons. - */ -- (NSArray *)buttonsForToolbar; - -/** - Returns the starting value assigned to the control, either an NSDate object or - an NSIndexPath. - */ -- (id)startingValue; - -/** - Returns the current value of the control, either an NSDate object or an NSIndexPath. - */ -- (id)currentValue; - -@end - -@protocol WPPickerViewDelegate - -@optional - -/** - Called on the delegate when the picker control changes its value. For UIDatePicker's - the result is an NSDate object. For UIPickers the result is an NSIndexPath. - A value of -1 is returned for components without a selection. - */ -- (void)pickerView:(WPPickerView *)pickerView didChangeValue:(id)value; - -/** - Called on the delegate when the picker's done button is tapped. Result is the same - as pickerView:didChangeValue:. - */ -- (void)pickerView:(WPPickerView *)pickerView didFinishWithValue:(id)value; - -@end diff --git a/WordPress/Classes/ViewRelated/Post/WPPickerView.m b/WordPress/Classes/ViewRelated/Post/WPPickerView.m deleted file mode 100644 index 4004f21012df..000000000000 --- a/WordPress/Classes/ViewRelated/Post/WPPickerView.m +++ /dev/null @@ -1,246 +0,0 @@ -#import "WPPickerView.h" - -static NSInteger WPPickerToolBarHeight = 44.0f; - -@interface WPPickerView () - -@property (nonatomic, strong) UIToolbar *toolbar; -@property (nonatomic, strong) UIPickerView *pickerView; -@property (nonatomic, strong) UIDatePicker *datePickerView; -@property (nonatomic, strong) NSDate *startingDate; -@property (nonatomic, strong) NSArray *dataSource; -@property (nonatomic, strong) NSIndexPath *startingIndexes; - -@end - -@implementation WPPickerView - -- (id)initWithDataSource:(NSArray *)dataSource andStartingIndexes:(NSIndexPath *)startingIndexes -{ - self = [self init]; - if (self) { - if (!startingIndexes) { - startingIndexes = [[NSIndexPath alloc] init]; - for (NSInteger i = 0; i < [dataSource count]; i++) { - startingIndexes = [startingIndexes indexPathByAddingIndex:0]; - } - } - self.startingIndexes = startingIndexes; - self.dataSource = dataSource; - [self configureView]; - } - return self; -} - -- (id)initWithDate:(NSDate *)date -{ - self = [self init]; - if (self) { - if (!date) { - date = [NSDate date]; - } - self.startingDate = date; - [self configureView]; - } - return self; -} - -- (void)configureView -{ - UIView *picker = [self viewForPicker]; - picker.frame = CGRectMake(0.0f, WPPickerToolBarHeight, CGRectGetWidth(picker.frame), CGRectGetHeight(picker.frame)); - self.frame = CGRectMake(0.0f, 0.0f, CGRectGetWidth(picker.frame), CGRectGetMaxY(picker.frame)); - - [self configureToolbar]; - - [self addSubview:picker]; - [self addSubview:self.toolbar]; -} - -- (void)configureToolbar -{ - self.toolbar = [[UIToolbar alloc] initWithFrame:CGRectMake(0.0f, 0.0f, CGRectGetWidth(self.frame), WPPickerToolBarHeight)]; - self.toolbar.autoresizingMask = UIViewAutoresizingFlexibleWidth; - - UIBarButtonItem *spacer = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil]; - UIBarButtonItem *leftSpacer = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil]; - leftSpacer.width = 0.0f; // Seems like the spacer is necessary for the right layout even if its width is 0. - UIBarButtonItem *rightSpacer = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil]; - rightSpacer.width = 6.0f; - - NSArray *buttons = [self buttonsForToolbar]; - if ([buttons count] == 0) { - return; - } - - NSMutableArray *items = [NSMutableArray array]; - [items addObject:leftSpacer]; - [items addObject:[buttons objectAtIndex:0]]; - for (NSInteger i = 1; i < [buttons count]; i++) { - [items addObject:spacer]; - [items addObject:[buttons objectAtIndex:i]]; - } - [items addObject:rightSpacer]; - - self.toolbar.items = items; -} - -#pragma mark - Instance Methods - -- (NSArray *)buttonsForToolbar -{ - NSString *title = NSLocalizedString(@"Reset", @"Title of the reset button"); - UIBarButtonItem *resetButton = [[UIBarButtonItem alloc] initWithTitle:title style:UIBarButtonItemStylePlain target:self action:@selector(reset)]; - UIBarButtonItem *doneButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(finished)]; - - return @[resetButton, doneButton]; -} - -- (UIView *)viewForPicker -{ - if ([self isDateMode]) { - return self.datePickerView; - } - return self.pickerView; -} - -- (id)currentValue -{ - if ([self isDateMode]) { - return self.datePickerView.date; - } - - NSIndexPath *path = [[NSIndexPath alloc] init]; - for (NSInteger i = 0; i < [self.pickerView numberOfComponents]; i++) { - path = [path indexPathByAddingIndex:[self.pickerView selectedRowInComponent:i]]; - } - return path; -} - -- (id)startingValue -{ - if ([self isDateMode]) { - return self.startingDate; - } - return self.startingIndexes; -} - -- (void)pickerValueChanged -{ - if (self.delegate && [self.delegate respondsToSelector:@selector(pickerView:didChangeValue:)]) { - [self.delegate pickerView:self didChangeValue:[self currentValue]]; - } -} - -- (BOOL)isDateMode -{ - return (self.startingDate != nil); -} - -- (void)reset -{ - if ([self isDateMode]) { - [self resetDatePicker]; - } else { - [self resetPicker]; - } -} - -- (void)finished -{ - if (self.delegate && [self.delegate respondsToSelector:@selector(pickerView:didFinishWithValue:)]) { - [self.delegate pickerView:self didFinishWithValue:[self currentValue]]; - } -} - -#pragma mark - Date Mode Methods - -- (UIDatePicker *)datePickerView -{ - if (!_datePickerView) { - UIDatePicker *picker = [[UIDatePicker alloc] init]; - picker.autoresizingMask = UIViewAutoresizingFlexibleWidth; - picker.datePickerMode = UIDatePickerModeDateAndTime; - picker.date = self.startingDate; - picker.minuteInterval = 5; - [picker addTarget:self action:@selector(pickerValueChanged) forControlEvents:UIControlEventValueChanged]; - self.datePickerView = picker; - } - return _datePickerView; -} - -- (void)resetDatePicker -{ - if ([self.datePickerView.date isEqualToDate:self.startingDate]) { - return; - } - self.datePickerView.date = self.startingDate; - [self pickerValueChanged]; -} - -#pragma mark - Picker Mode Methods - -- (UIPickerView *)pickerView -{ - if (!_pickerView) { - UIPickerView *picker = [[UIPickerView alloc] init]; - picker.autoresizingMask = UIViewAutoresizingFlexibleWidth; - picker.dataSource = self; - picker.delegate = self; - self.pickerView = picker; - [self setPickerStartingIndexes]; - } - return _pickerView; -} - -- (BOOL)setPickerStartingIndexes -{ - BOOL changed = NO; - - for (NSUInteger i = 0; i < [self.startingIndexes length]; i++) { - NSUInteger index = [self.startingIndexes indexAtPosition:i]; - if (index != [self.pickerView selectedRowInComponent:i]) { - changed = YES; - } - [self.pickerView selectRow:index inComponent:i animated:YES]; - } - - return changed; -} - -- (NSArray *)arrayForComponent:(NSInteger)component -{ - return [self.dataSource objectAtIndex:component]; -} - -- (void)resetPicker -{ - BOOL changed = [self setPickerStartingIndexes]; - if (changed) { - [self pickerValueChanged]; - } -} - -#pragma mark - UIPickerView Delegate Methods - -- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView -{ - return [self.dataSource count]; -} - -- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component -{ - return [[self arrayForComponent:component] count]; -} - -- (NSString *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component -{ - return [[self arrayForComponent:component] objectAtIndex:row]; -} - -- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component -{ - [self pickerValueChanged]; -} - -@end diff --git a/WordPress/Classes/ViewRelated/Post/WPStyleGuide+Posts.swift b/WordPress/Classes/ViewRelated/Post/WPStyleGuide+Posts.swift index d1801b1bd345..b95c4f8e1f88 100644 --- a/WordPress/Classes/ViewRelated/Post/WPStyleGuide+Posts.swift +++ b/WordPress/Classes/ViewRelated/Post/WPStyleGuide+Posts.swift @@ -1,24 +1,10 @@ +import UIKit +import WordPressShared + /// A WPStyleGuide extension with styles and methods specific to the Posts feature. /// extension WPStyleGuide { - // MARK: - General Posts Styles - - class func applyPostTitleStyle(_ title: String, into label: UILabel) { - label.attributedText = NSAttributedString(string: title, attributes: WPStyleGuide.postCardTitleAttributes) - label.lineBreakMode = .byTruncatingTail - } - - class func applyPostSnippetStyle(_ title: String, into label: UILabel) { - label.attributedText = NSAttributedString(string: title, attributes: WPStyleGuide.postCardSnippetAttributes) - label.lineBreakMode = .byTruncatingTail - } - - // MARK: - Card View Styles - static let postCardBorderColor: UIColor = .divider - - static let separatorHeight: CGFloat = .hairlineBorderWidth - class func applyPostCardStyle(_ cell: UITableViewCell) { cell.backgroundColor = .listBackground cell.contentView.backgroundColor = .listBackground @@ -28,83 +14,15 @@ extension WPStyleGuide { label.textColor = .text } - class func applyPostSnippetStyle(_ label: UILabel) { - label.textColor = .text - } - - class func applyPostDateStyle(_ label: UILabel) { - configureLabelForRegularFontStyle(label) - label.textColor = .textSubtle - } - - class func applyPostButtonStyle(_ button: UIButton) { - configureLabelForRegularFontStyle(button.titleLabel) - button.setTitleColor(.textSubtle, for: .normal) - } - class func applyPostProgressViewStyle(_ progressView: UIProgressView) { progressView.trackTintColor = .divider progressView.progressTintColor = .primary progressView.tintColor = .primary } - class func applyRestorePostLabelStyle(_ label: UILabel) { - configureLabelForRegularFontStyle(label) - label.textColor = .textSubtle - } - - class func applyRestorePostButtonStyle(_ button: UIButton) { - configureLabelForRegularFontStyle(button.titleLabel) - button.setTitleColor(.primary, for: .normal) - button.setTitleColor(.primaryDark, for: .highlighted) - button.tintColor = .primary - } - class func applyBorderStyle(_ view: UIView) { - view.updateConstraint(for: .height, withRelation: .equal, setConstant: separatorHeight, setActive: true) - view.backgroundColor = postCardBorderColor - } - - class func applyActionBarButtonStyle(_ button: UIButton) { - button.flipInsetsForRightToLeftLayoutDirection() - button.setImage(button.imageView?.image?.imageWithTintColor(.textSubtle), for: .normal) - button.setTitleColor(.textSubtle, for: .normal) - button.setTitleColor(.text, for: .highlighted) - button.setTitleColor(.text, for: .selected) - } - - class func insertSelectedBackgroundSubview(_ selectedBackgroundView: UIView, topMargin: CGFloat) { - let marginMask = UIView() - selectedBackgroundView.addSubview(marginMask) - marginMask.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - marginMask.leadingAnchor.constraint(equalTo: selectedBackgroundView.leadingAnchor), - marginMask.topAnchor.constraint(equalTo: selectedBackgroundView.topAnchor), - marginMask.trailingAnchor.constraint(equalTo: selectedBackgroundView.trailingAnchor), - marginMask.heightAnchor.constraint(equalToConstant: topMargin) - ]) - marginMask.backgroundColor = .neutral(.shade5) - } - - // MARK: - Attributed String Attributes - - class var postCardTitleAttributes: [NSAttributedString.Key: Any] { - let font = notoBoldFontForTextStyle(.headline) - let lineHeight = 1.4 * font.pointSize - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.minimumLineHeight = lineHeight - paragraphStyle.maximumLineHeight = lineHeight - return [.paragraphStyle: paragraphStyle, .font: font] - } - - class var postCardSnippetAttributes: [NSAttributedString.Key: Any] { - let textStyle: UIFont.TextStyle = UIDevice.isPad() ? .callout : .subheadline - let font = notoFontForTextStyle(textStyle) - let lineHeight = 1.6 * font.pointSize - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.minimumLineHeight = lineHeight - paragraphStyle.maximumLineHeight = lineHeight - return [.paragraphStyle: paragraphStyle, .font: font] + view.updateConstraint(for: .height, withRelation: .equal, setConstant: .hairlineBorderWidth, setActive: true) + view.backgroundColor = .divider } // MARK: - Font Styles diff --git a/WordPress/Classes/ViewRelated/Ratings/AppFeedbackPromptView.swift b/WordPress/Classes/ViewRelated/Ratings/AppFeedbackPromptView.swift index 7789c0ad5c6c..9ffe11e56a30 100644 --- a/WordPress/Classes/ViewRelated/Ratings/AppFeedbackPromptView.swift +++ b/WordPress/Classes/ViewRelated/Ratings/AppFeedbackPromptView.swift @@ -9,7 +9,6 @@ class AppFeedbackPromptView: UIView { private let leftButton = RoundedButton() private let rightButton = RoundedButton() private let buttonStack = UIStackView() - private var onRequestingFeedback = false // MARK: - UIView's Methods diff --git a/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.h b/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.h index 07b2c381e0d2..65b4a6d3bfa7 100644 --- a/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.h +++ b/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.h @@ -9,7 +9,8 @@ typedef NS_ENUM(NSUInteger, ReaderCommentsSource) { ReaderCommentsSourceCommentNotification, ReaderCommentsSourceCommentLikeNotification, ReaderCommentsSourceMySiteComment, - ReaderCommentsSourceActivityLogDetail + ReaderCommentsSourceActivityLogDetail, + ReaderCommentsSourcePostsList }; diff --git a/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.m b/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.m index 0682c39c58ad..385e5f7c1cd2 100644 --- a/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.m +++ b/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.m @@ -1101,7 +1101,7 @@ - (void)configureCell:(UITableViewCell *)aCell atIndexPath:(NSIndexPath *)indexP Comment *comment = [self.tableViewHandler.resultsController objectAtIndexPath:indexPath]; CommentContentTableViewCell *cell = (CommentContentTableViewCell *)aCell; - [self configureContentCell:cell comment:comment indexPath:indexPath handler:self.tableViewHandler]; + [self configureContentCell:cell comment:comment attributedText:[self cacheContentForComment:comment] indexPath:indexPath handler:self.tableViewHandler]; if (self.highlightedIndexPath) { cell.isEmphasized = (indexPath == self.highlightedIndexPath); diff --git a/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.swift b/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.swift index 3c3edacd4b8f..a70e03683da0 100644 --- a/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.swift +++ b/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.swift @@ -52,7 +52,13 @@ extension NSNotification.Name { return cell } - func configureContentCell(_ cell: UITableViewCell, comment: Comment, indexPath: IndexPath, handler: WPTableViewHandler) { + func configureContentCell( + _ cell: UITableViewCell, + comment: Comment, + attributedText: NSAttributedString, + indexPath: IndexPath, + handler: WPTableViewHandler + ) { guard let cell = cell as? CommentContentTableViewCell else { return } @@ -71,7 +77,7 @@ extension NSNotification.Name { handler: handler, sourceView: cell.accessoryButton) : nil - cell.configure(with: comment, renderMethod: .richContent) { _ in + cell.configure(with: comment, renderMethod: .richContent(attributedText)) { _ in // don't adjust cell height when it's already scrolled out of viewport. guard let visibleIndexPaths = handler.tableView.indexPathsForVisibleRows, visibleIndexPaths.contains(indexPath) else { @@ -305,6 +311,8 @@ private extension ReaderCommentsViewController { return "my_site_comment" case .activityLogDetail: return "activity_log_detail" + case .postsList: + return "posts_list" default: return "unknown" } diff --git a/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift b/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift index 6c68f9f49e12..fd081193c941 100644 --- a/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift +++ b/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift @@ -709,6 +709,17 @@ extension ReaderDetailCoordinator: ReaderDetailHeaderViewDelegate { func didSelectTopic(_ topic: String) { showTopic(topic) } + + func didTapLikes() { + showLikesList() + } + + func didTapComments() { + guard let post, let viewController else { + return + } + ReaderCommentAction().execute(post: post, origin: viewController, source: .postDetails) + } } // MARK: - ReaderDetailFeaturedImageViewDelegate diff --git a/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailViewController.swift b/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailViewController.swift index 6102039244d4..adcd80195d85 100644 --- a/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailViewController.swift +++ b/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailViewController.swift @@ -77,9 +77,6 @@ class ReaderDetailViewController: UIViewController, ReaderDetailView { /// The actual header private lazy var header: UIView & ReaderDetailHeader = { - guard RemoteFeatureFlag.readerImprovements.enabled() else { - return ReaderDetailHeaderView.loadFromNib() - } return ReaderDetailNewHeaderViewHost() }() @@ -186,6 +183,8 @@ class ReaderDetailViewController: UIViewController, ReaderDetailView { coordinator?.start() + startObservingPost() + // Fixes swipe to go back not working when leftBarButtonItem is set navigationController?.interactivePopGestureRecognizer?.delegate = self @@ -197,6 +196,7 @@ class ReaderDetailViewController: UIViewController, ReaderDetailView { super.viewWillAppear(animated) setupFeaturedImage() updateFollowButtonState() + toolbar.viewWillAppear() } override func viewWillDisappear(_ animated: Bool) { @@ -207,6 +207,7 @@ class ReaderDetailViewController: UIViewController, ReaderDetailView { } featuredImage.viewWillDisappear() + toolbar.viewWillDisappear() } override func viewDidAppear(_ animated: Bool) { @@ -253,6 +254,8 @@ class ReaderDetailViewController: UIViewController, ReaderDetailView { webView.postURL = postURL } + webView.isP2 = post.isP2Type() + coordinator?.storeAuthenticationCookies(in: webView) { [weak self] in self?.webView.loadHTMLString(post.contentForDisplay()) } @@ -533,7 +536,7 @@ class ReaderDetailViewController: UIViewController, ReaderDetailView { /// Configure the webview private func configureWebView() { - webView.usesSansSerifStyle = RemoteFeatureFlag.readerImprovements.enabled() + webView.usesSansSerifStyle = true webView.navigationDelegate = self } @@ -605,10 +608,6 @@ class ReaderDetailViewController: UIViewController, ReaderDetailView { headerContainerView.translatesAutoresizingMaskIntoConstraints = false headerContainerView.pinSubviewToAllEdges(header) - - if !RemoteFeatureFlag.readerImprovements.enabled() { - headerContainerView.heightAnchor.constraint(equalTo: header.heightAnchor).isActive = true - } } private func fetchLikes() { @@ -677,9 +676,7 @@ class ReaderDetailViewController: UIViewController, ReaderDetailView { toolbarContainerView.pinSubviewToAllEdges(toolbar) toolbarSafeAreaView.backgroundColor = toolbar.backgroundColor - if RemoteFeatureFlag.readerImprovements.enabled() { - toolbarHeightConstraint.constant = Constants.preferredToolbarHeight - } + toolbarHeightConstraint.constant = Constants.preferredToolbarHeight } private func configureDiscoverAttribution(_ post: ReaderPost) { @@ -710,9 +707,22 @@ class ReaderDetailViewController: UIViewController, ReaderDetailView { selector: #selector(siteBlocked(_:)), name: .ReaderSiteBlocked, object: nil) + + NotificationCenter.default.addObserver(self, + selector: #selector(userBlocked(_:)), + name: .ReaderUserBlockingDidEnd, + object: nil) + } + + @objc private func userBlocked(_ notification: Foundation.Notification) { + dismiss() } @objc private func siteBlocked(_ notification: Foundation.Notification) { + dismiss() + } + + private func dismiss() { navigationController?.popViewController(animated: true) dismiss(animated: true, completion: nil) } @@ -806,11 +816,33 @@ class ReaderDetailViewController: UIViewController, ReaderDetailView { private enum Constants { static let margin: CGFloat = UIDevice.isPad() ? 0 : 8 - static let bottomMargin: CGFloat = 16 - static let toolbarHeight: CGFloat = 50 - static let delay: Double = 50 static let preferredToolbarHeight: CGFloat = 58.0 } + + // MARK: - Managed object observer + + func startObservingPost() { + guard let post else { + return + } + NotificationCenter.default.addObserver(self, + selector: #selector(handleObjectsChange(_:)), + name: NSNotification.Name.NSManagedObjectContextObjectsDidChange, + object: post.managedObjectContext) + } + + + @objc func handleObjectsChange(_ notification: Foundation.Notification) { + guard let post else { + return + } + let updated = notification.userInfo?[NSUpdatedObjectsKey] as? Set ?? Set() + let refreshed = notification.userInfo?[NSRefreshedObjectsKey] as? Set ?? Set() + + if updated.contains(post) || refreshed.contains(post) { + header.configure(for: post) + } + } } // MARK: - StoryboardLoadable diff --git a/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailHeaderView.swift b/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailHeaderView.swift index 95131c3fc146..3f2602ad629a 100644 --- a/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailHeaderView.swift +++ b/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailHeaderView.swift @@ -7,6 +7,8 @@ protocol ReaderDetailHeaderViewDelegate: AnyObject { func didTapHeaderAvatar() func didTapFollowButton(completion: @escaping () -> Void) func didSelectTopic(_ topic: String) + func didTapLikes() + func didTapComments() } class ReaderDetailHeaderView: UIStackView, NibLoadable, ReaderDetailHeader { @@ -35,12 +37,6 @@ class ReaderDetailHeaderView: UIStackView, NibLoadable, ReaderDetailHeader { /// private var post: ReaderPost? - /// The user interface direction for the view's semantic content attribute. - /// - private var layoutDirection: UIUserInterfaceLayoutDirection { - return UIView.userInterfaceLayoutDirection(for: semanticContentAttribute) - } - /// Any interaction with the header is sent to the delegate /// weak var delegate: ReaderDetailHeaderViewDelegate? diff --git a/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailNewHeaderView.swift b/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailNewHeaderView.swift index 7be4411a52be..ebac019cba57 100644 --- a/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailNewHeaderView.swift +++ b/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailNewHeaderView.swift @@ -112,10 +112,26 @@ class ReaderDetailHeaderViewModel: ObservableObject { @Published var relativePostTime = String() @Published var siteName = String() @Published var postTitle: String? = nil // post title can be empty. + @Published var likeCount: Int? = nil + @Published var commentCount: Int? = nil @Published var tags: [String] = [] @Published var showsAuthorName: Bool = true + var likeCountString: String? { + guard let count = likeCount, count > 0 else { + return nil + } + return WPStyleGuide.likeCountForDisplay(count) + } + + var commentCountString: String? { + guard let count = commentCount, count > 0 else { + return nil + } + return WPStyleGuide.commentCountForDisplay(count) + } + init(coreDataStack: CoreDataStackSwift = ContextManager.shared) { self.coreDataStack = coreDataStack } @@ -150,6 +166,8 @@ class ReaderDetailHeaderViewModel: ObservableObject { self.showsAuthorName = self.authorName != self.siteName && !self.authorName.isEmpty self.postTitle = post.titleForDisplay() ?? nil + self.likeCount = post.likeCount?.intValue + self.commentCount = post.commentCount?.intValue self.tags = post.tagsForDisplay() ?? [] } @@ -187,6 +205,14 @@ class ReaderDetailHeaderViewModel: ObservableObject { self?.isFollowButtonInteractive = true } } + + func didTapLikes() { + headerDelegate?.didTapLikes() + } + + func didTapComments() { + headerDelegate?.didTapComments() + } } // MARK: - SwiftUI @@ -226,6 +252,9 @@ struct ReaderDetailNewHeaderView: View { .lineLimit(nil) .fixedSize(horizontal: false, vertical: true) // prevents the title from being truncated. } + if viewModel.likeCountString != nil || viewModel.commentCountString != nil { + postCounts + } if !viewModel.tags.isEmpty { tagsView } @@ -253,7 +282,7 @@ struct ReaderDetailNewHeaderView: View { let avatarURL = viewModel.authorAvatarURL { avatarView(with: siteIconURL, avatarURL: avatarURL) } - VStack(alignment: .leading) { + VStack(alignment: .leading, spacing: 4.0) { Text(viewModel.siteName) .font(.callout) .fontWeight(.semibold) @@ -309,6 +338,28 @@ struct ReaderDetailNewHeaderView: View { } } + var postCounts: some View { + HStack(spacing: 0) { + if let likeCount = viewModel.likeCountString { + Group { + Button(action: viewModel.didTapLikes) { + Text(likeCount) + } + if viewModel.commentCountString != nil { + Text(" • ") + } + } + } + if let commentCount = viewModel.commentCountString { + Button(action: viewModel.didTapComments) { + Text(commentCount) + } + } + } + .font(.footnote) + .foregroundStyle(.secondary) + } + var tagsView: some View { ReaderDetailTagsWrapperView(topics: viewModel.tags, delegate: viewModel.topicDelegate) .background(GeometryReader { geometry in diff --git a/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailToolbar.swift b/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailToolbar.swift index 24659ef55708..fb7015570538 100644 --- a/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailToolbar.swift +++ b/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailToolbar.swift @@ -11,12 +11,6 @@ class ReaderDetailToolbar: UIView, NibLoadable { @IBOutlet weak var commentButton: PostMetaButton! @IBOutlet weak var likeButton: PostMetaButton! - /// These are added to dynamically apply changes based on the `readerImprovements` feature flag. - /// Once the flag is removed, we should remove these and apply the changes directly on the XIB file. - @IBOutlet weak var stackView: UIStackView! - @IBOutlet weak var stackViewLeadingConstraint: NSLayoutConstraint! - @IBOutlet weak var stackViewTrailingConstraint: NSLayoutConstraint! - /// The reader post that the toolbar interacts with private var post: ReaderPost? @@ -34,19 +28,15 @@ class ReaderDetailToolbar: UIView, NibLoadable { weak var delegate: ReaderDetailToolbarDelegate? = nil - private lazy var readerImprovementsEnabled: Bool = { - return RemoteFeatureFlag.readerImprovements.enabled() - }() - private var likeButtonTitle: String { - guard let post, readerImprovementsEnabled else { + guard let post else { return likeLabel(count: likeCount) } return post.isLiked ? Constants.likedButtonTitle : Constants.likeButtonTitle } private var likeCount: Int { - post?.likeCount.intValue ?? 0 + post?.likeCount?.intValue ?? 0 } override func awakeFromNib() { @@ -59,6 +49,14 @@ class ReaderDetailToolbar: UIView, NibLoadable { prepareActionButtonsForVoiceOver() } + func viewWillAppear() { + subscribePostChanges() + } + + func viewWillDisappear() { + unsubscribePostChanges() + } + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) { @@ -71,37 +69,10 @@ class ReaderDetailToolbar: UIView, NibLoadable { self.post = post self.viewController = viewController - likeCountObserver = post.observe(\.likeCount, options: [.old, .new]) { [weak self] updatedPost, change in - // ensure that we only update the like button when there's actual change. - let oldValue = change.oldValue??.intValue ?? 0 - let newValue = change.newValue??.intValue ?? 0 - guard oldValue != newValue else { - return - } - - self?.configureLikeActionButton(true) - self?.delegate?.didTapLikeButton(isLiked: updatedPost.isLiked) - } - - commentCountObserver = post.observe(\.commentCount, options: [.old, .new]) { [weak self] _, change in - // ensure that we only update the like button when there's actual change. - let oldValue = change.oldValue??.intValue ?? 0 - let newValue = change.newValue??.intValue ?? 0 - guard oldValue != newValue else { - return - } - - self?.configureCommentActionButton() - } - + subscribePostChanges() configureActionButtons() } - deinit { - likeCountObserver?.invalidate() - commentCountObserver?.invalidate() - } - // MARK: - Actions @IBAction func didTapSaveForLater(_ sender: Any) { @@ -164,14 +135,6 @@ class ReaderDetailToolbar: UIView, NibLoadable { WPStyleGuide.applyReaderCardActionButtonStyle(commentButton) WPStyleGuide.applyReaderCardActionButtonStyle(likeButton) - - // TODO: Apply changes on the XIB directly once the `readerImprovements` flag is removed. - if readerImprovementsEnabled { - stackView.distribution = .fillEqually - stackView.spacing = 16.0 - stackViewLeadingConstraint.constant = 16.0 - stackViewTrailingConstraint.constant = 16.0 - } } // MARK: - Configuration @@ -223,16 +186,6 @@ class ReaderDetailToolbar: UIView, NibLoadable { imageColor: .textSubtle, disabledColor: disabledColor) - /// Configure the UIButton so it displays the button image on top of the title label. - /// Previously, this would be achieved through titleEdgeInsets and imageEdgeInsets, but these - /// will be deprecated soon. The new way to do this is through `UIButton.Configuration`, by setting - /// `imagePlacement` to `.top`. - /// - /// TODO: remove unused styles once the `readerImprovements` flag is removed. - guard readerImprovementsEnabled else { - return - } - var configuration = UIButton.Configuration.plain() // Vertically stack the button's image and title. @@ -294,6 +247,7 @@ class ReaderDetailToolbar: UIView, NibLoadable { WPStyleGuide.applyReaderReblogActionButtonStyle(reblogButton, showTitle: false) configureActionButtonStyle(reblogButton) + prepareReblogForVoiceOver() } private func playLikeButtonAnimation() { @@ -304,16 +258,12 @@ class ReaderDetailToolbar: UIView, NibLoadable { let animationDuration = 0.3 let imageView = UIImageView(image: WPStyleGuide.ReaderDetail.likeSelectedToolbarIcon) - if readerImprovementsEnabled { - /// When using `UIButton.Configuration`, calling `bringSubviewToFront` somehow does not work. - /// To work around this, let's add the faux image to the image view instead, so it can be - /// properly placed in front of the masking view. - imageView.translatesAutoresizingMaskIntoConstraints = false - likeImageView.addSubview(imageView) - likeImageView.pinSubviewAtCenter(imageView) - } else { - likeButton.addSubview(imageView) - } + /// When using `UIButton.Configuration`, calling `bringSubviewToFront` somehow does not work. + /// To work around this, let's add the faux image to the image view instead, so it can be + /// properly placed in front of the masking view. + imageView.translatesAutoresizingMaskIntoConstraints = false + likeImageView.addSubview(imageView) + likeImageView.pinSubviewAtCenter(imageView) if likeButton.isSelected { // Prep a mask to hide the likeButton's image, since changes to visibility and alpha are ignored @@ -322,12 +272,7 @@ class ReaderDetailToolbar: UIView, NibLoadable { likeImageView.addSubview(mask) likeImageView.pinSubviewToAllEdges(mask) mask.translatesAutoresizingMaskIntoConstraints = false - - if readerImprovementsEnabled { - likeImageView.bringSubviewToFront(imageView) - } else { - likeButton.bringSubviewToFront(imageView) - } + likeImageView.bringSubviewToFront(imageView) // Configure starting state imageView.alpha = 0.0 @@ -377,15 +322,11 @@ class ReaderDetailToolbar: UIView, NibLoadable { private func configureCommentActionButton() { commentButton.isEnabled = shouldShowCommentActionButton - if readerImprovementsEnabled { - commentButton.setImage(WPStyleGuide.ReaderDetail.commentToolbarIcon, for: .normal) - commentButton.setImage(WPStyleGuide.ReaderDetail.commentHighlightedToolbarIcon, for: .selected) - commentButton.setImage(WPStyleGuide.ReaderDetail.commentHighlightedToolbarIcon, for: .highlighted) - commentButton.setImage(WPStyleGuide.ReaderDetail.commentHighlightedToolbarIcon, for: [.highlighted, .selected]) - commentButton.setImage(WPStyleGuide.ReaderDetail.commentToolbarIcon, for: .disabled) - } else { - WPStyleGuide.applyReaderCardCommentButtonStyle(commentButton, defaultSize: true) - } + commentButton.setImage(WPStyleGuide.ReaderDetail.commentToolbarIcon, for: .normal) + commentButton.setImage(WPStyleGuide.ReaderDetail.commentHighlightedToolbarIcon, for: .selected) + commentButton.setImage(WPStyleGuide.ReaderDetail.commentHighlightedToolbarIcon, for: .highlighted) + commentButton.setImage(WPStyleGuide.ReaderDetail.commentHighlightedToolbarIcon, for: [.highlighted, .selected]) + commentButton.setImage(WPStyleGuide.ReaderDetail.commentToolbarIcon, for: .disabled) configureActionButtonStyle(commentButton) } @@ -431,13 +372,7 @@ class ReaderDetailToolbar: UIView, NibLoadable { } fileprivate func configureButtonTitles() { - guard let post = post else { - return - } - - let commentCount = post.commentCount()?.intValue ?? 0 - let commentTitle = readerImprovementsEnabled ? Constants.commentButtonTitle : commentLabel(count: commentCount) - let showTitle: Bool = readerImprovementsEnabled || traitCollection.horizontalSizeClass != .compact + let commentTitle = Constants.commentButtonTitle likeButton.setTitle(likeButtonTitle, for: .normal) likeButton.setTitle(likeButtonTitle, for: .highlighted) @@ -445,16 +380,8 @@ class ReaderDetailToolbar: UIView, NibLoadable { commentButton.setTitle(commentTitle, for: .normal) commentButton.setTitle(commentTitle, for: .highlighted) - WPStyleGuide.applyReaderSaveForLaterButtonTitles(saveForLaterButton, showTitle: showTitle) - WPStyleGuide.applyReaderReblogActionButtonTitle(reblogButton, showTitle: showTitle) - } - - private func commentLabel(count: Int) -> String { - if traitCollection.horizontalSizeClass == .compact { - return count > 0 ? String(count) : "" - } else { - return WPStyleGuide.commentCountForDisplay(count) - } + WPStyleGuide.applyReaderSaveForLaterButtonTitles(saveForLaterButton, showTitle: true) + WPStyleGuide.applyReaderReblogActionButtonTitle(reblogButton, showTitle: true) } private func likeLabel(count: Int) -> String { @@ -579,3 +506,37 @@ private extension ReaderDetailToolbar { ) } } + +// MARK: - Observe Post + +private extension ReaderDetailToolbar { + func subscribePostChanges() { + likeCountObserver = post?.observe(\.likeCount, options: [.old, .new]) { [weak self] updatedPost, change in + // ensure that we only update the like button when there's actual change. + let oldValue = change.oldValue??.intValue ?? 0 + let newValue = change.newValue??.intValue ?? 0 + guard oldValue != newValue else { + return + } + + self?.configureLikeActionButton(true) + self?.delegate?.didTapLikeButton(isLiked: updatedPost.isLiked) + } + + commentCountObserver = post?.observe(\.commentCount, options: [.old, .new]) { [weak self] _, change in + // ensure that we only update the like button when there's actual change. + let oldValue = change.oldValue??.intValue ?? 0 + let newValue = change.newValue??.intValue ?? 0 + guard oldValue != newValue else { + return + } + + self?.configureCommentActionButton() + } + } + + func unsubscribePostChanges() { + likeCountObserver?.invalidate() + commentCountObserver?.invalidate() + } +} diff --git a/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailToolbar.xib b/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailToolbar.xib index 9a032fbb29bc..e5dc86e659ff 100644 --- a/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailToolbar.xib +++ b/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailToolbar.xib @@ -19,11 +19,11 @@ - - + + - - - - - - - - - - - - - - - - - - - - - - diff --git a/WordPress/Classes/ViewRelated/Reader/ReaderTopicsCardCell.swift b/WordPress/Classes/ViewRelated/Reader/ReaderTopicsCardCell.swift index a65924491f95..18e4fd4a96d0 100644 --- a/WordPress/Classes/ViewRelated/Reader/ReaderTopicsCardCell.swift +++ b/WordPress/Classes/ViewRelated/Reader/ReaderTopicsCardCell.swift @@ -21,9 +21,7 @@ class ReaderTopicsCardCell: UITableViewCell, NibLoadable { weak var delegate: ReaderTopicsTableCardCellDelegate? - static var defaultNibName: String { - RemoteFeatureFlag.readerImprovements.enabled() ? "ReaderTopicsNewCardCell" : String(describing: self) - } + static var defaultNibName: String { "ReaderTopicsNewCardCell" } func configure(_ data: [ReaderAbstractTopic]) { self.data = data @@ -44,21 +42,18 @@ class ReaderTopicsCardCell: UITableViewCell, NibLoadable { collectionView.register(ReaderTopicCardCollectionViewCell.self, forCellWithReuseIdentifier: ReaderTopicCardCollectionViewCell.cellReuseIdentifier()) - if RemoteFeatureFlag.readerImprovements.enabled() { - configureForNewDesign() - } + configureForNewDesign() } private func applyStyles() { - let usesNewDesign = RemoteFeatureFlag.readerImprovements.enabled() - headerLabel.font = usesNewDesign ? WPStyleGuide.fontForTextStyle(.footnote) : WPStyleGuide.serifFontForTextStyle(.title2) + headerLabel.font = WPStyleGuide.fontForTextStyle(.footnote) - containerView.backgroundColor = usesNewDesign ? .secondarySystemBackground : .listForeground - headerLabel.backgroundColor = usesNewDesign ? .secondarySystemBackground : .listForeground - collectionView.backgroundColor = usesNewDesign ? .secondarySystemBackground : .listForeground + containerView.backgroundColor = .secondarySystemBackground + headerLabel.backgroundColor = .secondarySystemBackground + collectionView.backgroundColor = .secondarySystemBackground backgroundColor = .clear - contentView.backgroundColor = usesNewDesign ? .systemBackground : .listForeground + contentView.backgroundColor = .systemBackground } /// Configures the cell and the collection view for the new design. @@ -77,19 +72,6 @@ class ReaderTopicsCardCell: UITableViewCell, NibLoadable { backgroundColor = .systemBackground contentView.backgroundColor = .systemBackground - - // add manual separator view - let separatorView = UIView() - separatorView.backgroundColor = .separator - separatorView.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(separatorView) - - NSLayoutConstraint.activate([ - separatorView.heightAnchor.constraint(equalToConstant: 0.5), - separatorView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - separatorView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), - separatorView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) - ]) } private func refreshData() { @@ -100,45 +82,20 @@ class ReaderTopicsCardCell: UITableViewCell, NibLoadable { static let title = NSLocalizedString("You might like", comment: "A suggestion of topics the user might like") static let reuseIdentifier = ReaderInterestsCollectionViewCell.defaultReuseID - - static let collectionViewMinHeight: CGFloat = 40.0 } } // MARK: - Collection View: Datasource & Delegate extension ReaderTopicsCardCell: UICollectionViewDelegate, UICollectionViewDataSource { func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - - if RemoteFeatureFlag.readerImprovements.enabled() { - guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ReaderTopicCardCollectionViewCell.cellReuseIdentifier(), for: indexPath) as? ReaderTopicCardCollectionViewCell else { - return UICollectionViewCell() - } - - let title = data[indexPath.row].title - cell.titleLabel.text = title - cell.titleLabel.accessibilityIdentifier = .topicsCardCellIdentifier - cell.titleLabel.accessibilityTraits = .button - - return cell - } - - guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Constants.reuseIdentifier, - for: indexPath) as? ReaderInterestsCollectionViewCell else { - fatalError("Expected a ReaderInterestsCollectionViewCell for identifier: \(Constants.reuseIdentifier)") + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ReaderTopicCardCollectionViewCell.cellReuseIdentifier(), for: indexPath) as? ReaderTopicCardCollectionViewCell else { + return UICollectionViewCell() } let title = data[indexPath.row].title - - ReaderSuggestedTopicsStyleGuide.applySuggestedTopicStyle(label: cell.label, - with: indexPath.row) - - cell.label.text = title - cell.label.accessibilityIdentifier = .topicsCardCellIdentifier - cell.label.accessibilityTraits = .button - - // We need to use the calculated size for the height / corner radius because the cell size doesn't change until later - let size = sizeForCell(title: title) - cell.label.layer.cornerRadius = size.height * 0.5 + cell.titleLabel.text = title + cell.titleLabel.accessibilityIdentifier = .topicsCardCellIdentifier + cell.titleLabel.accessibilityTraits = .button return cell } @@ -182,18 +139,12 @@ extension ReaderTopicsCardCell: UICollectionViewDelegateFlowLayout { let title: NSString = title as NSString var size = title.size(withAttributes: attributes) - size.height += (CellConstants.marginY * 2) - if RemoteFeatureFlag.readerImprovements.enabled() { - size.height += 2 // to account for the top & bottom border width - } + size.height += (CellConstants.marginY * 2) + 2 // Prevent 1 token from being too long let maxWidth = collectionView.bounds.width * CellConstants.maxWidthMultiplier let width = min(size.width, maxWidth) - size.width = width + (CellConstants.marginX * 2) - if RemoteFeatureFlag.readerImprovements.enabled() { - size.width += 2 // to account for the leading & trailing border width - } + size.width = width + (CellConstants.marginX * 2) + 2 return size } diff --git a/WordPress/Classes/ViewRelated/Reader/Select Interests/ReaderInterestsStyleGuide.swift b/WordPress/Classes/ViewRelated/Reader/Select Interests/ReaderInterestsStyleGuide.swift index 6ddacf3cefc9..36316b6be3d4 100644 --- a/WordPress/Classes/ViewRelated/Reader/Select Interests/ReaderInterestsStyleGuide.swift +++ b/WordPress/Classes/ViewRelated/Reader/Select Interests/ReaderInterestsStyleGuide.swift @@ -12,17 +12,6 @@ class ReaderInterestsStyleGuide { let borderWidth: CGFloat let borderColor: UIColor - /// The legacy metrics for the cell style before `readerImprovements` feature - static let legacy = Metrics( - interestsLabelMargin: 8.0, - cellCornerRadius: 4.0, - cellSpacing: 6.0, - cellHeight: 26.0, - maxCellWidthMultiplier: 0.8, - borderWidth: 0, - borderColor: .clear - ) - static let latest = Metrics( interestsLabelMargin: 16.0, cellCornerRadius: 5.0, @@ -64,7 +53,7 @@ class ReaderInterestsStyleGuide { public class func applyCompactCellLabelStyle(label: UILabel) { label.font = Self.compactCellLabelTitleFont label.textColor = .text - label.backgroundColor = RemoteFeatureFlag.readerImprovements.enabled() ? .clear : .quaternaryBackground + label.backgroundColor = .clear } // MARK: - Next Button diff --git a/WordPress/Classes/ViewRelated/Reader/Tab Navigation/ReaderTabView.swift b/WordPress/Classes/ViewRelated/Reader/Tab Navigation/ReaderTabView.swift index 6cc9f6ddc96c..1b4807b593e3 100644 --- a/WordPress/Classes/ViewRelated/Reader/Tab Navigation/ReaderTabView.swift +++ b/WordPress/Classes/ViewRelated/Reader/Tab Navigation/ReaderTabView.swift @@ -47,7 +47,6 @@ class ReaderTabView: UIView { self?.tabBar.items = tabItems self?.tabBar.setSelectedIndex(index) self?.configureTabBarElements() - self?.hideGhost() self?.addContentToContainerView() } @@ -291,50 +290,6 @@ private extension ReaderTabView { } - -// MARK: - Ghost - -private extension ReaderTabView { - - /// Build the ghost tab bar - func makeGhostTabBar() -> FilterTabBar { - let ghostTabBar = FilterTabBar() - - ghostTabBar.items = Appearance.ghostTabItems - ghostTabBar.isUserInteractionEnabled = false - ghostTabBar.tabBarHeight = Appearance.barHeight - ghostTabBar.dividerColor = .clear - - return ghostTabBar - } - - /// Show the ghost tab bar - func showGhost() { - let ghostTabBar = makeGhostTabBar() - tabBar.addSubview(ghostTabBar) - tabBar.pinSubviewToAllEdges(ghostTabBar) - - loadingView = ghostTabBar - - ghostTabBar.startGhostAnimation(style: GhostStyle(beatDuration: GhostStyle.Defaults.beatDuration, - beatStartColor: .placeholderElement, - beatEndColor: .placeholderElementFaded)) - - } - - /// Hide the ghost tab bar - func hideGhost() { - loadingView?.stopGhostAnimation() - loadingView?.removeFromSuperview() - loadingView = nil - } - - struct GhostTabItem: FilterTabBarItem { - var title: String - let accessibilityIdentifier = "" - } -} - // MARK: - Appearance private extension ReaderTabView { @@ -342,10 +297,7 @@ private extension ReaderTabView { enum Appearance { static let barHeight: CGFloat = 48 - static let tabBarAnimationsDuration = 0.2 - static let defaultFilterButtonTitle = NSLocalizedString("Filter", comment: "Title of the filter button in the Reader") - static let filterButtonMaxFontSize: CGFloat = 28.0 static let filterButtonFont = WPStyleGuide.fontForTextStyle(.headline, fontWeight: .regular) static let filterButtonInsets = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 0) static let filterButtonimageInsets = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 0) @@ -356,8 +308,6 @@ private extension ReaderTabView { static let dividerWidth: CGFloat = .hairlineBorderWidth static let dividerColor: UIColor = .divider - // "ghost" titles are set to the default english titles, as they won't be visible anyway - static let ghostTabItems = [GhostTabItem(title: "Following"), GhostTabItem(title: "Discover"), GhostTabItem(title: "Likes"), GhostTabItem(title: "Saved")] } } diff --git a/WordPress/Classes/ViewRelated/Reader/Tags View/ReaderTopicCollectionViewCoordinator.swift b/WordPress/Classes/ViewRelated/Reader/Tags View/ReaderTopicCollectionViewCoordinator.swift index 4f1a350bf2fc..40d1d8d670d0 100644 --- a/WordPress/Classes/ViewRelated/Reader/Tags View/ReaderTopicCollectionViewCoordinator.swift +++ b/WordPress/Classes/ViewRelated/Reader/Tags View/ReaderTopicCollectionViewCoordinator.swift @@ -31,9 +31,7 @@ class ReaderTopicCollectionViewCoordinator: NSObject { static let accessbilityHint: String = NSLocalizedString("Tap to view posts for this tag", comment: "Accessibility hint to inform the user what action the post tag chip performs") } - private lazy var metrics: ReaderInterestsStyleGuide.Metrics = { - return RemoteFeatureFlag.readerImprovements.enabled() ? .latest : .legacy - }() + private let metrics: ReaderInterestsStyleGuide.Metrics = .latest weak var delegate: ReaderTopicCollectionViewCoordinatorDelegate? @@ -182,10 +180,7 @@ extension ReaderTopicCollectionViewCoordinator: UICollectionViewDelegateFlowLayo configure(cell: cell, with: title) - if layout.isExpanded || RemoteFeatureFlag.readerImprovements.enabled() { - cell.label.backgroundColor = .clear - } - + cell.label.backgroundColor = .clear cell.label.accessibilityHint = layout.isExpanded ? Strings.collapseButtonAccessbilityHint : Strings.expandButtonAccessbilityHint let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(toggleExpanded)) diff --git a/WordPress/Classes/ViewRelated/Reader/WPImageViewController+Swift.swift b/WordPress/Classes/ViewRelated/Reader/WPImageViewController+Swift.swift new file mode 100644 index 000000000000..7c93f8cd357f --- /dev/null +++ b/WordPress/Classes/ViewRelated/Reader/WPImageViewController+Swift.swift @@ -0,0 +1,20 @@ +import UIKit + +extension WPImageViewController { + @objc func loadOriginalImage(for media: Media, success: @escaping (UIImage) -> Void, failure: @escaping (Error) -> Void) { + Task { @MainActor in + do { + let image = try await MediaImageService.shared.image(for: media, size: .original) + success(image) + } catch { + failure(error) + } + } + } + + @objc func startAnimationIfNeeded(for image: UIImage, in imageView: CachedAnimatedImageView?) { + if let gif = image as? AnimatedImage, let data = gif.gifData { + imageView?.animate(withGIFData: data) + } + } +} diff --git a/WordPress/Classes/ViewRelated/Reader/WPImageViewController.h b/WordPress/Classes/ViewRelated/Reader/WPImageViewController.h index f7364c4c3162..09e4d8252ad7 100644 --- a/WordPress/Classes/ViewRelated/Reader/WPImageViewController.h +++ b/WordPress/Classes/ViewRelated/Reader/WPImageViewController.h @@ -1,7 +1,6 @@ #import @import Photos; -@import WPMediaPicker; @class Media; @class AbstractPost; @@ -10,7 +9,6 @@ NS_ASSUME_NONNULL_BEGIN @interface WPImageViewController : UIViewController -@property (nonatomic, readonly, nullable) id mediaAsset; @property (nonatomic, assign) BOOL shouldDismissWithGestures; @property (nonatomic, weak) AbstractPost* post; @property (nonatomic, weak) ReaderPost* readerPost; @@ -18,10 +16,9 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)initWithImage:(UIImage *)image; - (instancetype)initWithURL:(NSURL *)url; - (instancetype)initWithMedia:(Media *)media; -- (instancetype)initWithAsset:(PHAsset *)asset; + - (instancetype)initWithGifData:(NSData *)data; - (instancetype)initWithExternalMediaURL:(NSURL *)url; -- (instancetype)initWithExternalMediaURL:(NSURL *)url andAsset:(id)asset; - (instancetype)initWithImage:(nullable UIImage *)image andURL:(nullable NSURL *)url; - (instancetype)initWithImage:(nullable UIImage *)image andMedia:(nullable Media *)media; diff --git a/WordPress/Classes/ViewRelated/Reader/WPImageViewController.m b/WordPress/Classes/ViewRelated/Reader/WPImageViewController.m index 906dbcf95bf3..a6fa1e690641 100644 --- a/WordPress/Classes/ViewRelated/Reader/WPImageViewController.m +++ b/WordPress/Classes/ViewRelated/Reader/WPImageViewController.m @@ -10,8 +10,6 @@ @interface WPImageViewController () mediaAsset; @property (nonatomic, strong) NSData *data; @property (nonatomic) BOOL isExternal; @@ -48,16 +46,6 @@ - (instancetype)initWithMedia:(Media *)media return [self initWithImage:nil andMedia:media]; } -- (instancetype)initWithAsset:(PHAsset *)asset -{ - self = [super init]; - if (self) { - _asset = asset; - [self commonInit]; - } - return self; -} - - (instancetype)initWithGifData:(NSData *)data { self = [super init]; @@ -102,19 +90,6 @@ - (instancetype)initWithExternalMediaURL:(NSURL *)url return self; } -- (instancetype)initWithExternalMediaURL:(NSURL *)url andAsset:(id)asset -{ - self = [super init]; - if (self) { - _image = nil; - _url = url; - _mediaAsset = asset; - _isExternal = YES; - [self commonInit]; - } - return self; -} - - (void)commonInit { _shouldDismissWithGestures = YES; @@ -233,8 +208,6 @@ - (void)loadImage [self loadImageFromURL]; } else if (self.media) { [self loadImageFromMedia]; - } else if (self.asset) { - [self loadImageFromPHAsset]; } else if (self.data) { [self loadImageFromGifData]; } @@ -280,28 +253,15 @@ - (void)loadImageFromMedia { self.imageView.image = self.image; self.isLoadingImage = YES; - __weak __typeof__(self) weakSelf = self; - BOOL isBlogAtomic = [self.media.blog isAtomic]; - [self.imageLoader loadImageFromMedia:self.media preferredSize:CGSizeZero placeholder:self.image isBlogAtomic:isBlogAtomic success:^{ - weakSelf.isLoadingImage = NO; - weakSelf.image = weakSelf.imageView.image; - [weakSelf updateImageView]; - } error:^(NSError * _Nullable error) { - [weakSelf.activityIndicatorView showError]; - DDLogError(@"Error loading image: %@", error); - }]; -} + [self.activityIndicatorView startAnimating]; -- (void)loadImageFromPHAsset -{ - self.imageView.image = self.image; - self.isLoadingImage = YES; __weak __typeof__(self) weakSelf = self; - [self.imageLoader loadImageFromPHAsset:self.asset preferredSize:CGSizeZero placeholder:self.image success:^{ + [self loadOriginalImageFor:self.media success:^(UIImage * _Nonnull image) { weakSelf.isLoadingImage = NO; - weakSelf.image = weakSelf.imageView.image; + weakSelf.image = image; [weakSelf updateImageView]; - } error:^(NSError * _Nullable error) { + [weakSelf startAnimationIfNeededFor:image in:weakSelf.imageView]; + } failure:^(NSError * _Nonnull error) { [weakSelf.activityIndicatorView showError]; DDLogError(@"Error loading image: %@", error); }]; @@ -379,22 +339,6 @@ - (BOOL)prefersHomeIndicatorAutoHidden #pragma mark - Instance Methods -- (id)mediaAsset -{ - if (_mediaAsset) { - return _mediaAsset; - } - - if (self.asset) { - return self.asset; - } - if (self.media) { - return self.media; - } - - return nil; -} - - (void)setShouldDismissWithGestures:(BOOL)shouldDismissWithGestures { _shouldDismissWithGestures = shouldDismissWithGestures; diff --git a/WordPress/Classes/ViewRelated/Reader/WPStyleGuide+Reader.swift b/WordPress/Classes/ViewRelated/Reader/WPStyleGuide+Reader.swift index 25ccdf70fa7d..99f2eccd93d2 100644 --- a/WordPress/Classes/ViewRelated/Reader/WPStyleGuide+Reader.swift +++ b/WordPress/Classes/ViewRelated/Reader/WPStyleGuide+Reader.swift @@ -56,64 +56,27 @@ extension WPStyleGuide { // MARK: - Card Attributed Text Attributes @objc public class func readerCrossPostTitleAttributes() -> [NSAttributedString.Key: Any] { - if RemoteFeatureFlag.readerImprovements.enabled() { - let font = UIFont.preferredFont(forTextStyle: .subheadline).semibold() - return [ - .font: font, - .foregroundColor: UIColor.label - ] - } else { - let font = WPStyleGuide.serifFontForTextStyle(Cards.crossPostTitleTextStyle) - - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.lineSpacing = Cards.crossPostLineSpacing - - return [ - .paragraphStyle: paragraphStyle, - .font: font, - .foregroundColor: UIColor.text - ] - } + let font = UIFont.preferredFont(forTextStyle: .subheadline).semibold() + return [ + .font: font, + .foregroundColor: UIColor.label + ] } @objc public class func readerCrossPostBoldSubtitleAttributes() -> [NSAttributedString.Key: Any] { - if RemoteFeatureFlag.readerImprovements.enabled() { - let font = UIFont.preferredFont(forTextStyle: .footnote).semibold() - return [ - .font: font, - .foregroundColor: UIColor.secondaryLabel - ] - } else { - let font = WPStyleGuide.fontForTextStyle(Cards.crossPostSubtitleTextStyle, symbolicTraits: .traitBold) - - let paragraphStyle = NSMutableParagraphStyle() - return [ - .paragraphStyle: paragraphStyle, - .font: font, - .foregroundColor: UIColor(light: .gray(.shade40), dark: .systemGray) - ] - } + let font = UIFont.preferredFont(forTextStyle: .footnote).semibold() + return [ + .font: font, + .foregroundColor: UIColor.secondaryLabel + ] } @objc public class func readerCrossPostSubtitleAttributes() -> [NSAttributedString.Key: Any] { - if RemoteFeatureFlag.readerImprovements.enabled() { - let font = UIFont.preferredFont(forTextStyle: .footnote) - return [ - .font: font, - .foregroundColor: UIColor.secondaryLabel - ] - } else { - let font = WPStyleGuide.fontForTextStyle(Cards.crossPostSubtitleTextStyle) - - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.lineSpacing = Cards.crossPostLineSpacing - - return [ - .paragraphStyle: paragraphStyle, - .font: font, - .foregroundColor: UIColor(light: .gray(.shade40), dark: .systemGray) - ] - } + let font = UIFont.preferredFont(forTextStyle: .footnote) + return [ + .font: font, + .foregroundColor: UIColor.secondaryLabel + ] } @objc public class func readerCardTitleAttributes() -> [NSAttributedString.Key: Any] { @@ -139,12 +102,6 @@ extension WPStyleGuide { ] } - @objc public class func readerCardReadingTimeAttributes() -> [NSAttributedString.Key: Any] { - let font = WPStyleGuide.fontForTextStyle(Cards.subtextTextStyle) - - return [.font: font] - } - // MARK: - Detail styles @objc public class func readerDetailTitleAttributes() -> [NSAttributedString.Key: Any] { @@ -292,45 +249,8 @@ extension WPStyleGuide { button.setTitleColor(disabledColor, for: .disabled) } - @objc public class func applyReaderFollowButtonStyle(_ button: UIButton) { - guard !RemoteFeatureFlag.readerImprovements.enabled() else { - applyNewReaderFollowButtonStyle(button) - return - } - let side = WPStyleGuide.fontSizeForTextStyle(.callout) - let size = CGSize(width: side, height: side) - - let followIcon = UIImage.gridicon(.readerFollow, size: size) - let followingIcon = UIImage.gridicon(.readerFollowing, size: size) - - let followFont: UIFont = fontForTextStyle(.callout, fontWeight: .semibold) - let followingFont: UIFont = fontForTextStyle(.callout, fontWeight: .regular) - button.titleLabel?.font = button.isSelected ? followingFont : followFont - - button.layer.cornerRadius = 4.0 - button.layer.borderColor = UIColor.primaryButtonBorder.cgColor - - button.backgroundColor = button.isSelected ? FollowButton.Style.followingBackgroundColor : FollowButton.Style.followBackgroundColor - button.tintColor = button.isSelected ? FollowButton.Style.followingIconColor : FollowButton.Style.followTextColor - - button.setTitleColor(FollowButton.Style.followTextColor, for: .normal) - button.setTitleColor(FollowButton.Style.followingTextColor, for: .selected) - - button.imageEdgeInsets = FollowButton.Style.imageEdgeInsets - button.titleEdgeInsets = FollowButton.Style.titleEdgeInsets - button.contentEdgeInsets = FollowButton.Style.contentEdgeInsets - - let tintedFollowIcon = followIcon.imageWithTintColor(FollowButton.Style.followTextColor) - let tintedFollowingIcon = followingIcon.imageWithTintColor(FollowButton.Style.followingTextColor) - - button.setImage(tintedFollowIcon, for: .normal) - button.setImage(tintedFollowingIcon, for: .selected) - - applyCommonReaderFollowButtonStyles(button) - } - - public class func applyNewReaderFollowButtonStyle(_ button: UIButton, - contentInsets: NSDirectionalEdgeInsets = NSDirectionalEdgeInsets(top: 8.0, leading: 24.0, bottom: 8.0, trailing: 24.0)) { + @objc public class func applyReaderFollowButtonStyle(_ button: UIButton, + contentInsets: NSDirectionalEdgeInsets = NSDirectionalEdgeInsets(top: 8.0, leading: 24.0, bottom: 8.0, trailing: 24.0)) { let font: UIFont = .preferredFont(forTextStyle: .subheadline) button.setTitleColor(.invertedLabel, for: .normal) button.setTitleColor(.secondaryLabel, for: .selected) @@ -356,27 +276,6 @@ extension WPStyleGuide { button.accessibilityHint = FollowButton.Text.accessibilityHint } - @objc public class func applyReaderIconFollowButtonStyle(_ button: UIButton) { - guard !RemoteFeatureFlag.readerImprovements.enabled() else { - applyNewReaderFollowButtonStyle(button) - return - } - let followIcon = UIImage.gridicon(.readerFollow) - let followingIcon = UIImage.gridicon(.readerFollowing) - - button.backgroundColor = .clear - - let tintedFollowIcon = followIcon.imageWithTintColor(.primary(.shade40)) - let tintedFollowingIcon = followingIcon.imageWithTintColor(.gray(.shade40)) - - button.setImage(tintedFollowIcon, for: .normal) - button.setImage(tintedFollowingIcon, for: .selected) - - // Default accessibility label and hint. - button.accessibilityLabel = button.isSelected ? FollowButton.Text.followingStringForDisplay : FollowButton.Text.followStringForDisplay - button.accessibilityHint = FollowButton.Text.accessibilityHint - } - // Reader Detail Toolbar Save button @objc public class func applyReaderSaveForLaterButtonStyle(_ button: UIButton) { button.setImage(ReaderDetail.saveToolbarIcon, for: .normal) @@ -488,9 +387,7 @@ extension WPStyleGuide { let likeStr = NSLocalizedString("Like", comment: "Text for the 'like' button. Tapping marks a post in the reader as 'liked'.") let likesStr = NSLocalizedString("Likes", comment: "Text for the 'like' button. Tapping removes the 'liked' status from a post.") - if count == 0 && !RemoteFeatureFlag.readerImprovements.enabled() { - return likeStr - } else if count == 1 { + if count == 1 { return "\(count) \(likeStr)" } else { return "\(count) \(likesStr)" @@ -501,9 +398,7 @@ extension WPStyleGuide { let commentStr = NSLocalizedString("Comment", comment: "Text for the 'comment' when there is 1 or 0 comments") let commentsStr = NSLocalizedString("Comments", comment: "Text for the 'comment' button when there are multiple comments") - if count == 0 && !RemoteFeatureFlag.readerImprovements.enabled() { - return commentStr - } else if count == 1 { + if count == 1 { return "\(count) \(commentStr)" } else { return "\(count) \(commentsStr)" @@ -537,13 +432,6 @@ extension WPStyleGuide { button.setImage(icon, for: .normal) applyReaderActionButtonStyle(button, imageColor: UIColor(light: .black, dark: .white)) } - /// Applies the settings button style to the button passed as an argument - class func applyReaderSettingsButtonStyle(_ button: UIButton) { - let icon = UIImage.gridicon(.cog) - - button.setImage(icon, for: .normal) - applyReaderActionButtonStyle(button) - } // MARK: - Gap Marker Styles @@ -578,101 +466,47 @@ extension WPStyleGuide { public static let subtextTextStyle: UIFont.TextStyle = .caption1 public static let loadMoreButtonTextStyle: UIFont.TextStyle = .subheadline - public static let crossPostTitleTextStyle: UIFont.TextStyle = .body - public static let crossPostSubtitleTextStyle: UIFont.TextStyle = .caption1 - public static let crossPostLineSpacing: CGFloat = 2.0 - public static let actionButtonSize: CGSize = CGSize(width: 20, height: 20) } - public struct Detail { - public static let titleTextStyle: UIFont.TextStyle = .title2 - public static let contentTextStyle: UIFont.TextStyle = .callout - } - public struct ReaderDetail { - private static var readerImprovements: Bool { - return RemoteFeatureFlag.readerImprovements.enabled() - } - public static var reblogToolbarIcon: UIImage? { - if readerImprovements { - return UIImage(named: "icon-reader-reblog")?.withRenderingMode(.alwaysTemplate) - } - return UIImage.gridicon(.reblog, size: Gridicon.defaultSize) + return UIImage(named: "icon-reader-reblog")?.withRenderingMode(.alwaysTemplate) } static var commentToolbarIcon: UIImage? { - let imageName = readerImprovements ? "icon-reader-post-comment" : "icon-reader-comment-outline" - return UIImage(named: imageName)? + return UIImage(named: "icon-reader-post-comment")? .imageFlippedForRightToLeftLayoutDirection() .withRenderingMode(.alwaysTemplate) } static var commentHighlightedToolbarIcon: UIImage? { - if readerImprovements { - // note: we don't have a highlighted variant in the new version. - return commentToolbarIcon - } - return UIImage(named: "icon-reader-comment-outline-highlighted")? - .imageFlippedForRightToLeftLayoutDirection() - .withRenderingMode(.alwaysTemplate) + // note: we don't have a highlighted variant in the new version. + return commentToolbarIcon } public static var saveToolbarIcon: UIImage? { - if readerImprovements { - return UIImage(named: "icon-reader-save-outline") - } - return UIImage.gridicon(.bookmarkOutline, size: Gridicon.defaultSize) + return UIImage(named: "icon-reader-save-outline") } public static var saveSelectedToolbarIcon: UIImage? { - if readerImprovements { - return UIImage(named: "icon-reader-save-fill") - } - return UIImage.gridicon(.bookmark, size: Gridicon.defaultSize) + return UIImage(named: "icon-reader-save-fill") } static var likeToolbarIcon: UIImage? { - if readerImprovements { - return UIImage(named: "icon-reader-star-outline")?.withRenderingMode(.alwaysTemplate) - } - return UIImage(named: "icon-reader-like") + return UIImage(named: "icon-reader-star-outline")?.withRenderingMode(.alwaysTemplate) } static var likeSelectedToolbarIcon: UIImage? { - if readerImprovements { - return UIImage(named: "icon-reader-star-fill")?.withRenderingMode(.alwaysTemplate) - } - return UIImage(named: "icon-reader-liked") + return UIImage(named: "icon-reader-star-fill")?.withRenderingMode(.alwaysTemplate) } } public struct FollowButton { - struct Style { - static let followBackgroundColor: UIColor = .primaryButtonBackground - static let followTextColor: UIColor = .white - static let followingBackgroundColor: UIColor = .clear - static let followingIconColor: UIColor = .buttonIcon - static let followingTextColor: UIColor = .textSubtle - - static let imageTitleSpace: CGFloat = 2.0 - static let imageEdgeInsets = UIEdgeInsets(top: 0, left: -imageTitleSpace, bottom: 0, right: imageTitleSpace) - static let titleEdgeInsets = UIEdgeInsets(top: 0, left: imageTitleSpace, bottom: 0, right: -imageTitleSpace) - static let contentEdgeInsets = UIEdgeInsets(top: 6.0, left: 12.0, bottom: 6.0, right: 12.0) - } - struct Text { static let accessibilityHint = NSLocalizedString("Follows the tag.", comment: "VoiceOver accessibility hint, informing the user the button can be used to follow a tag.") static let followStringForDisplay = NSLocalizedString("Follow", comment: "Verb. Button title. Follow a new blog.") static let followingStringForDisplay = NSLocalizedString("Following", comment: "Verb. Button title. The user is following a blog.") } } - - public struct FollowConversationButton { - struct Style { - static let imageEdgeInsets = UIEdgeInsets(top: 1.0, left: -4.0, bottom: 0.0, right: -4.0) - static let contentEdgeInsets = UIEdgeInsets(top: 0.0, left: 4.0, bottom: 0.0, right: 0.0) - } - } } diff --git a/WordPress/Classes/ViewRelated/Sharing/ShareAppContentPresenter+TableView.swift b/WordPress/Classes/ViewRelated/Sharing/ShareAppContentPresenter+TableView.swift index 9ec22c1eb556..320a9d0ca279 100644 --- a/WordPress/Classes/ViewRelated/Sharing/ShareAppContentPresenter+TableView.swift +++ b/WordPress/Classes/ViewRelated/Sharing/ShareAppContentPresenter+TableView.swift @@ -4,6 +4,5 @@ extension ShareAppContentPresenter { struct RowConstants { static let buttonTitle = AppConstants.Settings.shareButtonTitle static let buttonIconImage: UIImage? = .init(systemName: "square.and.arrow.up") - static let buttonTintColor: UIColor = .primary } } diff --git a/WordPress/Classes/ViewRelated/Site Creation/Design Selection/Preview/TemplatePreviewViewController.swift b/WordPress/Classes/ViewRelated/Site Creation/Design Selection/Preview/TemplatePreviewViewController.swift index 174da4386d9a..431cae36c141 100644 --- a/WordPress/Classes/ViewRelated/Site Creation/Design Selection/Preview/TemplatePreviewViewController.swift +++ b/WordPress/Classes/ViewRelated/Site Creation/Design Selection/Preview/TemplatePreviewViewController.swift @@ -37,13 +37,6 @@ class TemplatePreviewViewController: UIViewController, NoResultsViewHost, UIPopo } private var onDismissWithDeviceSelected: ((PreviewDevice) -> ())? - lazy var ghostView: GutenGhostView = { - let ghost = GutenGhostView() - ghost.hidesToolbar = true - ghost.translatesAutoresizingMaskIntoConstraints = false - return ghost - }() - private var accentColor: UIColor { return UIColor { (traitCollection: UITraitCollection) -> UIColor in if traitCollection.userInterfaceStyle == .dark { diff --git a/WordPress/Classes/ViewRelated/Site Creation/Final Assembly/SiteAssemblyContentView.swift b/WordPress/Classes/ViewRelated/Site Creation/Final Assembly/SiteAssemblyContentView.swift index 7aeb4924451a..ffe86966769a 100644 --- a/WordPress/Classes/ViewRelated/Site Creation/Final Assembly/SiteAssemblyContentView.swift +++ b/WordPress/Classes/ViewRelated/Site Creation/Final Assembly/SiteAssemblyContentView.swift @@ -36,28 +36,26 @@ final class SiteAssemblyContentView: UIView { return $0 }(UILabel()) + private let noticeView: UIView = { + let noticeText = NSLocalizedString( + "domain.purchase.preview.footer", + value: "It may take up to 30 minutes for your custom domain to start working.", + comment: "Domain Purchase Completion footer" + ) + let noticeView = DomainSetupNoticeView(noticeText: noticeText) + let embeddedView = UIView.embedSwiftUIView(noticeView) + embeddedView.translatesAutoresizingMaskIntoConstraints = false + return embeddedView + }() + private lazy var completionLabelsStack: UIStackView = { - $0.addArrangedSubviews([completionLabel, completionDescription]) + $0.addArrangedSubviews([completionLabel, completionDescription, noticeView]) $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .vertical $0.spacing = 24 return $0 }(UIStackView()) - private let footnoteLabel: UILabel = { - $0.translatesAutoresizingMaskIntoConstraints = false - $0.numberOfLines = 0 - $0.font = WPStyleGuide.fontForTextStyle(.footnote) - $0.textColor = .text - let footerText = NSLocalizedString( - "domain.purchase.preview.footer", - value: "It may take up to 30 minutes for your custom domain to start working.", - comment: "Domain Purchase Completion footer" - ) - $0.text = footerText - return $0 - }(UILabel()) - /// This provides the user with some playful words while their site is being assembled private let statusTitleLabel: UILabel @@ -315,7 +313,7 @@ final class SiteAssemblyContentView: UIView { backgroundColor = .listBackground statusStackView.addArrangedSubviews([ statusTitleLabel, statusSubtitleLabel, statusImageView, statusMessageRotatingView, activityIndicator ]) - addSubviews([completionLabelsStack, statusStackView, footnoteLabel]) + addSubviews([completionLabelsStack, statusStackView]) // Increase the spacing around the illustration statusStackView.setCustomSpacing(Parameters.verticalSpacing, after: statusSubtitleLabel) @@ -334,8 +332,6 @@ final class SiteAssemblyContentView: UIView { prevailingLayoutGuide.trailingAnchor.constraint(equalTo: statusStackView.trailingAnchor, constant: Parameters.horizontalMargin), statusStackView.centerXAnchor.constraint(equalTo: centerXAnchor), statusStackView.centerYAnchor.constraint(equalTo: centerYAnchor), - footnoteLabel.leadingAnchor.constraint(equalTo: completionLabelsStack.leadingAnchor), - completionLabelsStack.trailingAnchor.constraint(equalTo: footnoteLabel.trailingAnchor) ]) } @@ -369,7 +365,7 @@ final class SiteAssemblyContentView: UIView { if shouldShowDomainPurchase() { assembledSiteView.layer.cornerRadius = 12 assembledSiteView.layer.masksToBounds = true - assembledSiteViewBottomConstraint = footnoteLabel.topAnchor.constraint( + assembledSiteViewBottomConstraint = (buttonContainerView?.topAnchor ?? bottomAnchor).constraint( equalTo: assembledSiteView.bottomAnchor, constant: 24 ) @@ -390,7 +386,7 @@ final class SiteAssemblyContentView: UIView { assembledSiteViewBottomConstraint, assembledSiteView.centerXAnchor.constraint(equalTo: centerXAnchor), assembledSiteWidthConstraint, - (buttonContainerView?.topAnchor ?? bottomAnchor).constraint(equalTo: footnoteLabel.bottomAnchor, constant: 15) + (buttonContainerView?.topAnchor ?? bottomAnchor).constraint(equalTo: assembledSiteView.bottomAnchor, constant: 15) ]) self.assembledSiteView = assembledSiteView @@ -446,7 +442,7 @@ final class SiteAssemblyContentView: UIView { private func layoutIdle() { completionLabel.isHidden = true completionDescription.isHidden = true - footnoteLabel.isHidden = true + noticeView.isHidden = true statusStackView.alpha = 0 errorStateView?.alpha = 0 } @@ -512,7 +508,7 @@ final class SiteAssemblyContentView: UIView { self.completionDescription.isHidden = false self.completionLabel.text = self.shouldShowDomainPurchase() ? Strings.Paid.completionTitle : Strings.Free.completionTitle self.completionDescription.text = self.shouldShowDomainPurchase() ? Strings.Paid.description : Strings.Free.description - self.footnoteLabel.isHidden = !self.shouldShowDomainPurchase() + self.noticeView.isHidden = !self.shouldShowDomainPurchase() if let buttonView = self.buttonContainerView { diff --git a/WordPress/Classes/ViewRelated/Site Creation/Plan/PlanStep.swift b/WordPress/Classes/ViewRelated/Site Creation/Plan/PlanStep.swift index c741eb555734..0b9245375c71 100644 --- a/WordPress/Classes/ViewRelated/Site Creation/Plan/PlanStep.swift +++ b/WordPress/Classes/ViewRelated/Site Creation/Plan/PlanStep.swift @@ -5,8 +5,9 @@ final class PlanStep: WizardStep { internal var content: UIViewController { let viewModel = PlanWizardContentViewModel(siteCreator: creator) - return PlanWizardContent(viewModel: viewModel) { [weak self] planId in + return PlanWizardContent(viewModel: viewModel) { [weak self] planId, domainName in self?.creator.planId = planId + self?.creator.addressFromPlanSelection = domainName self?.delegate?.nextStep() } } diff --git a/WordPress/Classes/ViewRelated/Site Creation/Plan/PlanWizardContent.swift b/WordPress/Classes/ViewRelated/Site Creation/Plan/PlanWizardContent.swift index 7002e632116a..ddf7e083ba7d 100644 --- a/WordPress/Classes/ViewRelated/Site Creation/Plan/PlanWizardContent.swift +++ b/WordPress/Classes/ViewRelated/Site Creation/Plan/PlanWizardContent.swift @@ -1,7 +1,9 @@ import UIKit final class PlanWizardContent: WebKitViewController { - typealias PlanSelectionCallback = (Int?) -> Void + typealias PlanId = Int + typealias DomainName = String + typealias PlanSelectionCallback = (PlanId?, DomainName?) -> Void private let viewModel: PlanWizardContentViewModel private let completion: PlanSelectionCallback @@ -29,7 +31,7 @@ final class PlanWizardContent: WebKitViewController { } if viewModel.isPlanSelected(url) { - completion(viewModel.selectedPlanId(from: url)) + completion(viewModel.selectedPlanId(from: url), viewModel.selectedDomainName(from: url)) decisionHandler(.cancel) return } diff --git a/WordPress/Classes/ViewRelated/Site Creation/Plan/PlanWizardContentViewModel.swift b/WordPress/Classes/ViewRelated/Site Creation/Plan/PlanWizardContentViewModel.swift index 7db013c8e709..4151c5e33d9a 100644 --- a/WordPress/Classes/ViewRelated/Site Creation/Plan/PlanWizardContentViewModel.swift +++ b/WordPress/Classes/ViewRelated/Site Creation/Plan/PlanWizardContentViewModel.swift @@ -8,7 +8,7 @@ struct PlanWizardContentViewModel { var queryItems: [URLQueryItem] = [] if let domainSuggestion = siteCreator.address, !domainSuggestion.isFree { - queryItems.append(.init(name: Constants.paidDomainNameParameter, value: domainSuggestion.domainName)) + queryItems.append(.init(name: Constants.InputParameter.paidDomainName, value: domainSuggestion.domainName)) } components.queryItems = queryItems @@ -25,7 +25,7 @@ struct PlanWizardContentViewModel { } func selectedPlanId(from url: URL) -> Int? { - guard let planId = parameterValue(from: url, key: Constants.planIdParameter) else { + guard let planId = parameterValue(from: url, key: Constants.OutputParameter.planId) else { return nil } @@ -33,14 +33,25 @@ struct PlanWizardContentViewModel { } func selectedPlanSlug(from url: URL) -> String? { - return parameterValue(from: url, key: Constants.planSlugParameter) + return parameterValue(from: url, key: Constants.OutputParameter.planSlug) } - enum Constants { + func selectedDomainName(from url: URL) -> String? { + return parameterValue(from: url, key: Constants.OutputParameter.domainName) + } + + struct Constants { static let plansWebAddress = "https://wordpress.com/jetpack-app/plans" - static let planIdParameter = "plan_id" - static let planSlugParameter = "plan_slug" - static let paidDomainNameParameter = "paid_domain_name" + + struct InputParameter { + static let paidDomainName = "paid_domain_name" + } + + struct OutputParameter { + static let planId = "plan_id" + static let planSlug = "plan_slug" + static let domainName = "domain_name" + } } } diff --git a/WordPress/Classes/ViewRelated/Site Creation/Shared/KeyboardInfo.swift b/WordPress/Classes/ViewRelated/Site Creation/Shared/KeyboardInfo.swift deleted file mode 100644 index 1c55b9f13a8c..000000000000 --- a/WordPress/Classes/ViewRelated/Site Creation/Shared/KeyboardInfo.swift +++ /dev/null @@ -1,31 +0,0 @@ -struct KeyboardInfo { - let animationCurve: UIView.AnimationCurve - let animationDuration: Double - let isLocal: Bool - let frameBegin: CGRect - let frameEnd: CGRect -} - -extension KeyboardInfo { - init?(_ notification: Foundation.Notification) { - guard notification.name == UIResponder.keyboardWillShowNotification || notification.name == UIResponder.keyboardWillHideNotification else { - return nil - } - - guard let u = notification.userInfo, - let curve = u[UIWindow.keyboardAnimationCurveUserInfoKey] as? Int, - let aCurve = UIView.AnimationCurve(rawValue: curve), - let duration = u[UIWindow.keyboardAnimationDurationUserInfoKey] as? Double, - let local = u[UIWindow.keyboardIsLocalUserInfoKey] as? Bool, - let begin = u[UIWindow.keyboardFrameBeginUserInfoKey] as? CGRect, - let end = u[UIWindow.keyboardFrameEndUserInfoKey] as? CGRect else { - return nil - } - - animationCurve = aCurve - animationDuration = duration - isLocal = local - frameBegin = begin - frameEnd = end - } -} diff --git a/WordPress/Classes/ViewRelated/Site Creation/Shared/ShadowView.swift b/WordPress/Classes/ViewRelated/Site Creation/Shared/ShadowView.swift index a216439d5972..89778d84d519 100644 --- a/WordPress/Classes/ViewRelated/Site Creation/Shared/ShadowView.swift +++ b/WordPress/Classes/ViewRelated/Site Creation/Shared/ShadowView.swift @@ -33,8 +33,4 @@ final class ShadowView: UIView { maskPath.addPath(shadowPath) shadowMaskLayer.path = maskPath } - - func clearShadow() { - shadowLayer.removeFromSuperlayer() - } } diff --git a/WordPress/Classes/ViewRelated/Site Creation/Shared/SiteCreationAnalyticsHelper.swift b/WordPress/Classes/ViewRelated/Site Creation/Shared/SiteCreationAnalyticsHelper.swift index 2938b938b943..dbd3dbda0155 100644 --- a/WordPress/Classes/ViewRelated/Site Creation/Shared/SiteCreationAnalyticsHelper.swift +++ b/WordPress/Classes/ViewRelated/Site Creation/Shared/SiteCreationAnalyticsHelper.swift @@ -21,11 +21,9 @@ class SiteCreationAnalyticsHelper { private static let siteDesignKey = "template" private static let previewModeKey = "preview_mode" private static let verticalSlugKey = "vertical_slug" - private static let verticalSearchTerm = "search_term" private static let variationKey = "variation" private static let siteNameKey = "site_name" private static let recommendedKey = "recommended" - private static let customTreatmentNameKey = "custom_treatment_variation_name" // MARK: - Lifecycle static func trackSiteCreationAccessed(source: String) { @@ -58,24 +56,6 @@ class SiteCreationAnalyticsHelper { WPAnalytics.track(.enhancedSiteCreationIntentQuestionCanceled) } - // MARK: - Site Name - static func trackSiteNameViewed() { - WPAnalytics.track(.enhancedSiteCreationSiteNameViewed) - } - - static func trackSiteNameEntered(_ name: String) { - let properties = [siteNameKey: name] - WPAnalytics.track(.enhancedSiteCreationSiteNameEntered, properties: properties) - } - - static func trackSiteNameSkipped() { - WPAnalytics.track(.enhancedSiteCreationSiteNameSkipped) - } - - static func trackSiteNameCanceled() { - WPAnalytics.track(.enhancedSiteCreationSiteNameCanceled) - } - // MARK: - Site Design static func trackSiteDesignViewed(previewMode: PreviewDevice) { WPAnalytics.track(.enhancedSiteCreationSiteDesignViewed, withProperties: commonProperties(previewMode)) diff --git a/WordPress/Classes/ViewRelated/Site Creation/Shared/TableDataCoordinator.swift b/WordPress/Classes/ViewRelated/Site Creation/Shared/TableDataCoordinator.swift index cffcf7980d84..c6c28144ab41 100644 --- a/WordPress/Classes/ViewRelated/Site Creation/Shared/TableDataCoordinator.swift +++ b/WordPress/Classes/ViewRelated/Site Creation/Shared/TableDataCoordinator.swift @@ -1,7 +1,5 @@ import UIKit -typealias TableViewProvider = UITableViewDataSource & UITableViewDelegate - /// Generic-based implementation of the UITableViewDataSource and UITableViewDelegate protocol. /// final class TableDataCoordinator: NSObject, UITableViewDataSource, UITableViewDelegate where Cell: ModelSettableCell, Cell: UITableViewCell, Model == Cell.DataType { diff --git a/WordPress/Classes/ViewRelated/Site Creation/Shared/TableViewOffsetCoordinator.swift b/WordPress/Classes/ViewRelated/Site Creation/Shared/TableViewOffsetCoordinator.swift deleted file mode 100644 index 043d447d4b8f..000000000000 --- a/WordPress/Classes/ViewRelated/Site Creation/Shared/TableViewOffsetCoordinator.swift +++ /dev/null @@ -1,202 +0,0 @@ - -import UIKit - -/// In Site Creation, both Verticals & Domains coordinate table view header appearance, keyboard behavior, & offsets. -/// This class manages that shared behavior. -/// -final class TableViewOffsetCoordinator { - - // MARK: Properties - private struct Constants { - static let headerAnimationDuration = Double(0.25) // matches current system keyboard transition duration - static let topMargin = CGFloat(36) - static let domainHeaderSection = 0 - } - - /// The table view to coordinate - private weak var tableView: UITableView? - - //// The view containing the toolbar - private weak var footerControlContainer: UIView? - - //// The toolbar - private weak var footerControl: UIView? - - //// The constraint linking the bottom of the footerControl to its container - private weak var toolbarBottomConstraint: NSLayoutConstraint? - - /// Tracks the content offset introduced by the keyboard being presented - private var keyboardContentOffset = CGFloat(0) - - /// To avoid wasted animations, we track whether or not we have already adjusted the table view - private var tableViewHasBeenAdjusted = false - - /// Track the status of the toolbar, wether we have adjusted its position or remains at its initial location - private var toolbarHasBeenAdjusted = false - - // MARK: TableViewOffsetCoordinator - - /// Initializes a table view offset coordinator with the specified table view. - /// - /// - Parameter tableView: the table view to manage - /// - Parameter footerControlContainer: the view containing the toolbar - /// - Parameter toolbar: a view that needs to be offset in coordination with the table view - /// - Parameter toolbarBottomConstraint: the constraint linking the bottom if footerControlContainer and toolbar - /// - init(coordinated tableView: UITableView, footerControlContainer: UIView? = nil, footerControl: UIView? = nil, toolbarBottomConstraint: NSLayoutConstraint? = nil) { - self.tableView = tableView - self.footerControlContainer = footerControlContainer - self.footerControl = footerControl - self.toolbarBottomConstraint = toolbarBottomConstraint - } - - // MARK: Internal behavior - - /// This method hides the table view header and adjusts the content offset so that the input text field is visible. - /// - func adjustTableOffsetIfNeeded() { - guard let tableView = tableView, keyboardContentOffset > 0, tableViewHasBeenAdjusted == false else { - return - } - - let topInset: CGFloat - if WPDeviceIdentification.isiPhone(), let header = tableView.tableHeaderView as? TitleSubtitleTextfieldHeader { - let textfieldFrame = header.textField.frame - topInset = textfieldFrame.origin.y - Constants.topMargin - } else { - topInset = 0 - } - - let bottomInset: CGFloat - if WPDeviceIdentification.isiPad() && UIDevice.current.orientation.isPortrait { - bottomInset = 0 - } else { - bottomInset = keyboardContentOffset - } - - let targetInsets = UIEdgeInsets(top: -topInset, left: 0, bottom: bottomInset, right: 0) - - UIView.animate(withDuration: Constants.headerAnimationDuration, delay: 0, options: .beginFromCurrentState, animations: { [weak self] in - guard let self = self, let tableView = self.tableView else { - return - } - - tableView.contentInset = targetInsets - tableView.scrollIndicatorInsets = targetInsets - if WPDeviceIdentification.isiPhone(), let header = tableView.tableHeaderView as? TitleSubtitleTextfieldHeader { - header.titleSubtitle.alpha = 0.0 - tableView.headerView(forSection: Constants.domainHeaderSection)?.isHidden = true - } - }, completion: { [weak self] _ in - self?.tableViewHasBeenAdjusted = true - }) - } - - /// This method resets the table view header and the content offset to the default state. - /// - func resetTableOffsetIfNeeded() { - guard WPDeviceIdentification.isiPhone(), tableViewHasBeenAdjusted == true else { - return - } - - UIView.animate(withDuration: Constants.headerAnimationDuration, delay: 0, options: .beginFromCurrentState, animations: { [weak self] in - guard let self = self, let tableView = self.tableView else { - return - } - - let finalOffset: UIEdgeInsets - if let footerControl = self.footerControl, self.toolbarHasBeenAdjusted == true { - let toolbarHeight = footerControl.frame.size.height - finalOffset = UIEdgeInsets(top: -1 * toolbarHeight, - left: 0, bottom: toolbarHeight, right: 0) - } else { - finalOffset = .zero - } - tableView.contentInset = finalOffset - tableView.scrollIndicatorInsets = finalOffset - if WPDeviceIdentification.isiPhone(), let header = tableView.tableHeaderView as? TitleSubtitleTextfieldHeader { - header.titleSubtitle.alpha = 1.0 - } - }, completion: { [weak self] _ in - self?.tableViewHasBeenAdjusted = false - }) - } - - @objc - func keyboardWillShow(_ notification: Foundation.Notification) { - guard let payload = KeyboardInfo(notification) else { - return - } - - let keyboardScreenFrame = payload.frameEnd - keyboardContentOffset = keyboardScreenFrame.height - - adjustToolbarOffsetIfNeeded() - } - - @objc - private func keyboardWillHide(_ notification: Foundation.Notification) { - keyboardContentOffset = 0 - toolbarHasBeenAdjusted = false - toolbarBottomConstraint?.constant = 0 - } - - private func adjustToolbarOffsetIfNeeded() { - guard let footerControl = footerControl, let footerControlContainer = footerControlContainer else { - return - } - - var constraintConstant = keyboardContentOffset - - let bottomInset = footerControlContainer.safeAreaInsets.bottom - constraintConstant -= bottomInset - - if let header = tableView?.tableHeaderView as? TitleSubtitleTextfieldHeader { - let textFieldFrame = header.textField.frame - - let newToolbarFrame = footerControl.frame.offsetBy(dx: 0.0, dy: -1 * constraintConstant) - - toolbarBottomConstraint?.constant = constraintConstant - footerControlContainer.setNeedsUpdateConstraints() - - UIView.animate(withDuration: Constants.headerAnimationDuration, delay: 0, options: .beginFromCurrentState, animations: { [weak self] in - guard let self = self, let tableView = self.tableView else { - return - } - - if textFieldFrame.intersects(newToolbarFrame) { - let contentInsets = UIEdgeInsets(top: -1 * footerControl.frame.height, left: 0.0, bottom: constraintConstant + footerControl.frame.height, right: 0.0) - self.toolbarHasBeenAdjusted = true - tableView.contentInset = contentInsets - tableView.scrollIndicatorInsets = contentInsets - } - footerControlContainer.layoutIfNeeded() - }, completion: { [weak self] _ in - self?.tableViewHasBeenAdjusted = false - }) - } - } - - func startListeningToKeyboardNotifications() { - NotificationCenter.default.addObserver(self, - selector: #selector(keyboardWillShow), - name: UIResponder.keyboardWillShowNotification, - object: nil) - NotificationCenter.default.addObserver(self, - selector: #selector(keyboardWillHide), - name: UIResponder.keyboardWillHideNotification, - object: nil) - } - - func stopListeningToKeyboardNotifications() { - NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil) - } - - func showBottomToolbar() { - footerControl?.isHidden = false - } - - func hideBottomToolbar() { - footerControl?.isHidden = true - } -} diff --git a/WordPress/Classes/ViewRelated/Site Creation/Shared/TitleSubtitleTextfieldHeader.swift b/WordPress/Classes/ViewRelated/Site Creation/Shared/TitleSubtitleTextfieldHeader.swift index c55287847eb0..363113425eec 100644 --- a/WordPress/Classes/ViewRelated/Site Creation/Shared/TitleSubtitleTextfieldHeader.swift +++ b/WordPress/Classes/ViewRelated/Site Creation/Shared/TitleSubtitleTextfieldHeader.swift @@ -112,89 +112,3 @@ final class SearchTextField: UITextField { leftViewMode = .always } } - -// MARK: - TitleSubtitleTextfieldHeader - -final class TitleSubtitleTextfieldHeader: UIView { - - // MARK: Properties - - private struct Constants { - static let spacing = CGFloat(10) - static let bottomMargin = CGFloat(16) - } - - private(set) lazy var titleSubtitle: TitleSubtitleHeader = { - let returnValue = TitleSubtitleHeader(frame: .zero) - returnValue.translatesAutoresizingMaskIntoConstraints = false - - return returnValue - }() - - private(set) var textField = SearchTextField() - - private lazy var stackView: UIStackView = { - - let returnValue = UIStackView(arrangedSubviews: [self.titleSubtitle, self.textField]) - returnValue.translatesAutoresizingMaskIntoConstraints = false - returnValue.axis = .vertical - returnValue.spacing = Constants.spacing - NSLayoutConstraint.activate([ - textField.leadingAnchor.constraint(equalTo: returnValue.leadingAnchor), - textField.trailingAnchor.constraint(equalTo: returnValue.trailingAnchor) - ]) - - return returnValue - }() - - // MARK: UIView - - override init(frame: CGRect) { - super.init(frame: frame) - setupView() - } - - required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - setupView() - } - - // MARK: Private behavior - - private func setupView() { - translatesAutoresizingMaskIntoConstraints = false - addSubview(stackView) - NSLayoutConstraint.activate([ - stackView.leadingAnchor.constraint(equalTo: leadingAnchor), - stackView.trailingAnchor.constraint(equalTo: trailingAnchor), - stackView.topAnchor.constraint(equalTo: topAnchor), - stackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -Constants.bottomMargin) - ]) - - setStyles() - - prepareForVoiceOver() - } - - private func setStyles() { - backgroundColor = .clear - } - - func setTitle(_ text: String) { - titleSubtitle.setTitle(text) - } - - func setSubtitle(_ text: String) { - titleSubtitle.setSubtitle(text) - } -} - -extension TitleSubtitleTextfieldHeader: Accessible { - func prepareForVoiceOver() { - prepareSearchFieldForVoiceOver() - } - - private func prepareSearchFieldForVoiceOver() { - textField.accessibilityTraits = .searchField - } -} diff --git a/WordPress/Classes/ViewRelated/Site Creation/Site Intent/SiteIntentViewController.swift b/WordPress/Classes/ViewRelated/Site Creation/Site Intent/SiteIntentViewController.swift index 8c367ee609fe..8f55c0937952 100644 --- a/WordPress/Classes/ViewRelated/Site Creation/Site Intent/SiteIntentViewController.swift +++ b/WordPress/Classes/ViewRelated/Site Creation/Site Intent/SiteIntentViewController.swift @@ -138,9 +138,6 @@ extension SiteIntentViewController { private enum Metrics { static let largeTitleLines = 2 - static let continueButtonPadding: CGFloat = 16 - static let continueButtonBottomOffset: CGFloat = 12 - static let continueButtonHeight: CGFloat = 44 } } diff --git a/WordPress/Classes/ViewRelated/Site Creation/Site Segments/SiteSegmentsCell.swift b/WordPress/Classes/ViewRelated/Site Creation/Site Segments/SiteSegmentsCell.swift index e1a0f5dee260..6eb627ff20f7 100644 --- a/WordPress/Classes/ViewRelated/Site Creation/Site Segments/SiteSegmentsCell.swift +++ b/WordPress/Classes/ViewRelated/Site Creation/Site Segments/SiteSegmentsCell.swift @@ -10,12 +10,6 @@ private extension String { } } -private extension SiteSegment { - var iconTintColor: UIColor? { - return self.iconColor?.hexAsColor() - } -} - final class SiteSegmentsCell: UITableViewCell, ModelSettableCell { @IBOutlet weak var icon: UIImageView! @IBOutlet weak var title: UILabel! diff --git a/WordPress/Classes/ViewRelated/Site Creation/Web Address/AddressTableViewCell+ViewModel.swift b/WordPress/Classes/ViewRelated/Site Creation/Web Address/AddressTableViewCell+ViewModel.swift index 051cd2d4c261..ba60b80415bf 100644 --- a/WordPress/Classes/ViewRelated/Site Creation/Web Address/AddressTableViewCell+ViewModel.swift +++ b/WordPress/Classes/ViewRelated/Site Creation/Web Address/AddressTableViewCell+ViewModel.swift @@ -73,7 +73,7 @@ extension AddressTableViewCell.ViewModel { extension AddressTableViewCell.ViewModel { - init(model: DomainSuggestion, tags: [Tag] = []) { + init(model: DomainSuggestion, type: DomainSelectionType, tags: [Tag] = []) { // Declare variables var tags = tags let cost: Cost @@ -81,12 +81,29 @@ extension AddressTableViewCell.ViewModel { // Format cost and sale cost if model.isFree { cost = .free - } else if let formatter = Self.currencyFormatter(code: model.currencyCode), - let costValue = model.cost, - let formattedCost = formatter.string(from: .init(value: costValue)) { - cost = .freeWithPaidPlan(cost: formattedCost) } else { - cost = .freeWithPaidPlan(cost: model.costString) + switch type { + case .purchaseSeparately: + if let formatter = Self.currencyFormatter(code: model.currencyCode), + let costValue = model.cost, + let formattedCost = formatter.string(from: .init(value: costValue)) { + if let saleCost = model.saleCost, let formattedSaleCost = formatter.string(from: .init(value: saleCost)) { + cost = .onSale(cost: formattedCost, sale: formattedSaleCost) + } else { + cost = .regular(cost: formattedCost) + } + } else { + cost = .regular(cost: model.costString) + } + default: + if let formatter = Self.currencyFormatter(code: model.currencyCode), + let costValue = model.cost, + let formattedCost = formatter.string(from: .init(value: costValue)) { + cost = .freeWithPaidPlan(cost: formattedCost) + } else { + cost = .freeWithPaidPlan(cost: model.costString) + } + } } // Configure tags diff --git a/WordPress/Classes/ViewRelated/Site Creation/Web Address/AddressTableViewCell.swift b/WordPress/Classes/ViewRelated/Site Creation/Web Address/AddressTableViewCell.swift index b28652ff09c9..53bbfb264963 100644 --- a/WordPress/Classes/ViewRelated/Site Creation/Web Address/AddressTableViewCell.swift +++ b/WordPress/Classes/ViewRelated/Site Creation/Web Address/AddressTableViewCell.swift @@ -5,10 +5,6 @@ final class AddressTableViewCell: UITableViewCell { // MARK: - Dependencies - private var domainPurchasingEnabled: Bool { - RemoteFeatureFlag.plansInSiteCreation.enabled() - } - override var accessibilityLabel: String? { get { return [domainLabel.text, @@ -77,9 +73,7 @@ final class AddressTableViewCell: UITableViewCell { override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) commonInit() - if domainPurchasingEnabled { - setupSubviews() - } + setupSubviews() } required init?(coder aDecoder: NSCoder) { @@ -93,9 +87,6 @@ final class AddressTableViewCell: UITableViewCell { "Selects this domain to use for your site.", comment: "Accessibility hint for a domain in the Site Creation domains list." ) - if !domainPurchasingEnabled { - self.selectedBackgroundView?.backgroundColor = .clear - } } private func setupSubviews() { @@ -146,7 +137,6 @@ final class AddressTableViewCell: UITableViewCell { // MARK: - Updating UI - /// This is the new update method and it's called when `domainPurchasing` feature flag is enabled. func update(with viewModel: ViewModel) { self.domainLabel.text = viewModel.domain self.leadingLabel.attributedText = Self.leadingAttributedString(tags: viewModel.tags, cost: viewModel.cost) @@ -292,13 +282,9 @@ extension AddressTableViewCell { } override func setSelected(_ selected: Bool, animated: Bool) { - if domainPurchasingEnabled { - super.setSelected(selected, animated: animated) - self.checkmarkImageView.isHidden = !selected - self.dotView.isHidden = !checkmarkImageView.isHidden - } else { - accessoryType = selected ? .checkmark : .none - } + super.setSelected(selected, animated: animated) + self.checkmarkImageView.isHidden = !selected + self.dotView.isHidden = !checkmarkImageView.isHidden } private func styleCheckmark() { diff --git a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Color+DesignSystem.swift b/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Color+DesignSystem.swift deleted file mode 100644 index 37578a0f93aa..000000000000 --- a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Color+DesignSystem.swift +++ /dev/null @@ -1,70 +0,0 @@ -import SwiftUI - -/// Design System Color extensions. Keep it in sync with its sibling file `UIColor+DesignSystem` -/// to support borth API's equally. -public extension Color { - enum DS { - public enum Foreground { - public static let primary = Color(DesignSystemColorNames.Foreground.primary) - public static let secondary = Color(DesignSystemColorNames.Foreground.secondary) - public static let tertiary = Color(DesignSystemColorNames.Foreground.tertiary) - public static let quaternary = Color(DesignSystemColorNames.Foreground.quaternary) - public static let success = Color(DesignSystemColorNames.Foreground.success) - public static let warning = Color(DesignSystemColorNames.Foreground.warning) - public static let error = Color(DesignSystemColorNames.Foreground.error) - } - - public enum Background { - public static let primary = Color(DesignSystemColorNames.Background.primary) - public static let secondary = Color(DesignSystemColorNames.Background.secondary) - public static let tertiary = Color(DesignSystemColorNames.Background.tertiary) - public static let quaternary = Color(DesignSystemColorNames.Background.quaternary) - - public static var brand: Color { - if AppConfiguration.isJetpack { - return jetpack - } else { - return wordPress - } - } - - private static let jetpack = Color(DesignSystemColorNames.Background.jetpack) - private static let wordPress = Color(DesignSystemColorNames.Background.wordPress) - } - - public enum Border { - public static let primary = Color(DesignSystemColorNames.Border.primary) - public static let secondary = Color(DesignSystemColorNames.Border.secondary) - public static let divider = Color(DesignSystemColorNames.Border.divider) - } - } -} - -/// Once we move Design System to its own module, we should keep this `internal` -/// as we don't need to expose it to the application module -internal enum DesignSystemColorNames { - internal enum Foreground { - internal static let primary = "foregroundPrimary" - internal static let secondary = "foregroundSecondary" - internal static let tertiary = "foregroundTertiary" - internal static let quaternary = "foregroundQuaternary" - internal static let success = "success" - internal static let warning = "warning" - internal static let error = "error" - } - - internal enum Background { - internal static let primary = "backgroundPrimary" - internal static let secondary = "backgroundSecondary" - internal static let tertiary = "backgroundTertiary" - internal static let quaternary = "backgroundQuaternary" - internal static let jetpack = "brandJetpack" - internal static let wordPress = "brandWordPress" - } - - internal enum Border { - internal static let primary = "borderPrimary" - internal static let secondary = "borderSecondary" - internal static let divider = "borderDivider" - } -} diff --git a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Background/backgroundPrimary.colorset/Contents.json b/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Background/backgroundPrimary.colorset/Contents.json deleted file mode 100644 index 7175aa037611..000000000000 --- a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Background/backgroundPrimary.colorset/Contents.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "platform" : "ios", - "reference" : "systemBackgroundColor" - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "platform" : "ios", - "reference" : "systemBackgroundColor" - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Background/backgroundQuaternary.colorset/Contents.json b/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Background/backgroundQuaternary.colorset/Contents.json deleted file mode 100644 index 77ab6c6cdeff..000000000000 --- a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Background/backgroundQuaternary.colorset/Contents.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "platform" : "ios", - "reference" : "quaternarySystemFillColor" - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "platform" : "ios", - "reference" : "quaternarySystemFillColor" - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Background/backgroundTertiary.colorset/Contents.json b/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Background/backgroundTertiary.colorset/Contents.json deleted file mode 100644 index 5d761c058695..000000000000 --- a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Background/backgroundTertiary.colorset/Contents.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "extended-gray", - "components" : { - "alpha" : "1.000", - "white" : "1.000" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "platform" : "ios", - "reference" : "tertiarySystemBackgroundColor" - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Foreground/foregroundSecondary.colorset/Contents.json b/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Foreground/foregroundSecondary.colorset/Contents.json deleted file mode 100644 index 92d12ef42b40..000000000000 --- a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Foreground/foregroundSecondary.colorset/Contents.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "platform" : "universal", - "reference" : "secondaryLabelColor" - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "platform" : "universal", - "reference" : "secondaryLabelColor" - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Foreground/foregroundTertiary.colorset/Contents.json b/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Foreground/foregroundTertiary.colorset/Contents.json deleted file mode 100644 index e04eff94d4f5..000000000000 --- a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Foreground/foregroundTertiary.colorset/Contents.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "platform" : "universal", - "reference" : "tertiaryLabelColor" - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "platform" : "universal", - "reference" : "tertiaryLabelColor" - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Theme/brandWordPress.colorset/Contents.json b/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Theme/brandWordPress.colorset/Contents.json deleted file mode 100644 index ca9f64fd910a..000000000000 --- a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Theme/brandWordPress.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.533", - "green" : "0.376", - "red" : "0.000" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.635", - "green" : "0.478", - "red" : "0.094" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Spacing.swift b/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Spacing.swift deleted file mode 100644 index 1308b3ffa0e1..000000000000 --- a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Spacing.swift +++ /dev/null @@ -1,21 +0,0 @@ -public enum Length { - public enum Padding { - public static let single: CGFloat = 8 - public static let double: CGFloat = 16 - public static let small: CGFloat = 24 - public static let medium: CGFloat = 32 - public static let large: CGFloat = 40 - } - - public enum Hitbox { - public static let minTapDimension: CGFloat = 44 - } - - public enum Radius { - public static let minHeightButton: CGFloat = 8 - } - - public enum Border { - public static let thin: CGFloat = 0.5 - } -} diff --git a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/UIColor+DesignSystem.swift b/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/UIColor+DesignSystem.swift deleted file mode 100644 index ab5da1ad789b..000000000000 --- a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/UIColor+DesignSystem.swift +++ /dev/null @@ -1,42 +0,0 @@ -import UIKit - -/// Replica of the `Color` structure -/// The reason for not using the `Color` intializer of `UIColor` is that -/// it has dubious effects. Also the doc advises against it. -/// Even though `UIColor(SwiftUI.Color)` keeps the adaptability for color theme, -/// accessing to light or dark variants specifically via trait collection does not return the right values -/// if the color is initialized as such. Probably one of the reasons why they advise against it. -/// To make these values non-optional, we use `Color` versions as fallback. -public extension UIColor { - enum DS { - public enum Foreground { - public static let primary = UIColor(named: DesignSystemColorNames.Foreground.primary) - public static let secondary = UIColor(named: DesignSystemColorNames.Foreground.secondary) - public static let tertiary = UIColor(named: DesignSystemColorNames.Foreground.tertiary) - public static let quaternary = UIColor(named: DesignSystemColorNames.Foreground.quaternary) - } - - public enum Background { - public static let primary = UIColor(named: DesignSystemColorNames.Background.primary) - public static let secondary = UIColor(named: DesignSystemColorNames.Background.secondary) - public static let tertiary = UIColor(named: DesignSystemColorNames.Background.tertiary) - public static let quaternary = UIColor(named: DesignSystemColorNames.Background.quaternary) - - public static var brand: UIColor? { - if AppConfiguration.isJetpack { - return jetpack - } else { - return jetpack // FIXME: WordPress colors - } - } - - private static let jetpack = UIColor(named: DesignSystemColorNames.Background.jetpack) - } - - public enum Border { - public static let primary = UIColor(named: DesignSystemColorNames.Border.primary) - public static let secondary = UIColor(named: DesignSystemColorNames.Border.secondary) - public static let divider = UIColor(named: DesignSystemColorNames.Border.divider) - } - } -} diff --git a/WordPress/Classes/ViewRelated/Site Creation/Web Address/SiteCreationEmptySiteTemplate.swift b/WordPress/Classes/ViewRelated/Site Creation/Web Address/SiteCreationEmptySiteTemplate.swift index a79c483b5c17..cee4b492d848 100644 --- a/WordPress/Classes/ViewRelated/Site Creation/Web Address/SiteCreationEmptySiteTemplate.swift +++ b/WordPress/Classes/ViewRelated/Site Creation/Web Address/SiteCreationEmptySiteTemplate.swift @@ -1,4 +1,5 @@ import SwiftUI +import DesignSystem struct SiteCreationEmptySiteTemplate: View { private enum Constants { @@ -11,20 +12,26 @@ struct SiteCreationEmptySiteTemplate: View { } var body: some View { - VStack(spacing: Constants.containerStackSpacing) { - siteBarVStack - tooltip - } - .background( - LinearGradient( - gradient: Gradient( - colors: [Color.emptySiteGradientInitial, Color.emptySiteBackgroundPrimary] - ), - startPoint: .top, - endPoint: .center + VStack { + Spacer() + VStack(spacing: Constants.containerStackSpacing) { + siteBarVStack + tooltip + } + .background( + LinearGradient( + gradient: Gradient( + colors: [Color.emptySiteGradientInitial, Color.emptySiteBackgroundPrimary] + ), + startPoint: .top, + endPoint: .center + ) ) - ) - .cornerRadius(Constants.containerCornerRadius) + .cornerRadius(Constants.containerCornerRadius) + Spacer() + Spacer() + } + } private var siteBarVStack: some View { @@ -135,12 +142,12 @@ private extension SiteCreationEmptySiteTemplate { } private extension Color { - static let emptySiteBackgroundPrimary = Color("emptySiteBackgroundPrimary") - static let emptySiteBackgroundSecondary = Color("emptySiteBackgroundSecondary") - static let emptySiteForegroundPrimary = Color("emptySiteForegroundPrimary") - static let emptySiteForegroundSecondary = Color("emptySiteForegroundSecondary") - static let emptySiteGradientInitial = Color("emptySiteGradientInitial") - static let emptySiteTooltipGradientInitial = Color("emptySiteTooltipGradientInitial") - static let emptySiteTooltipBackground = Color("emptySiteTooltipBackground") - static let emptySiteTooltipBorder = Color("emptySiteTooltipBorder") + static let emptySiteBackgroundPrimary = Color.DS.custom("emptySiteBackgroundPrimary") + static let emptySiteBackgroundSecondary = Color.DS.custom("emptySiteBackgroundSecondary") + static let emptySiteForegroundPrimary = Color.DS.custom("emptySiteForegroundPrimary") + static let emptySiteForegroundSecondary = Color.DS.custom("emptySiteForegroundSecondary") + static let emptySiteGradientInitial = Color.DS.custom("emptySiteGradientInitial") + static let emptySiteTooltipGradientInitial = Color.DS.custom("emptySiteTooltipGradientInitial") + static let emptySiteTooltipBackground = Color.DS.custom("emptySiteTooltipBackground") + static let emptySiteTooltipBorder = Color.DS.custom("emptySiteTooltipBorder") } diff --git a/WordPress/Classes/ViewRelated/Site Creation/Web Address/SitePromptView.swift b/WordPress/Classes/ViewRelated/Site Creation/Web Address/SitePromptView.swift deleted file mode 100644 index c244bcdd401f..000000000000 --- a/WordPress/Classes/ViewRelated/Site Creation/Web Address/SitePromptView.swift +++ /dev/null @@ -1,61 +0,0 @@ -import UIKit -import Gridicons - -class SitePromptView: UIView { - - private struct Parameters { - static let cornerRadius = CGFloat(8) - static let borderWidth = CGFloat(1) - static let borderColor = UIColor.primaryButtonBorder - } - - @IBOutlet weak var sitePrompt: UILabel! { - didSet { - sitePrompt.text = NSLocalizedString("example.com", comment: "Provides a sample of what a domain name looks like.") - } - } - - @IBOutlet weak var lockIcon: UIImageView! { - didSet { - lockIcon.image = UIImage.gridicon(.lock) - } - } - var contentView: UIView! - - override init(frame: CGRect) { - super.init(frame: frame) - commonInit() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - commonInit() - } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) { - contentView.layer.borderColor = Parameters.borderColor.cgColor - } - } - - private func commonInit() { - let bundle = Bundle(for: SitePromptView.self) - guard - let nibViews = bundle.loadNibNamed("SitePromptView", owner: self, options: nil), - let loadedView = nibViews.first as? UIView - else { - return - } - - contentView = loadedView - addSubview(contentView) - contentView.translatesAutoresizingMaskIntoConstraints = false - - NSLayoutConstraint.activate( - contentView.constrainToSuperViewEdges() - ) - contentView.layer.cornerRadius = Parameters.cornerRadius - contentView.layer.borderColor = Parameters.borderColor.cgColor - contentView.layer.borderWidth = Parameters.borderWidth - } -} diff --git a/WordPress/Classes/ViewRelated/Site Creation/Web Address/SitePromptView.xib b/WordPress/Classes/ViewRelated/Site Creation/Web Address/SitePromptView.xib deleted file mode 100644 index 58b072a131f5..000000000000 --- a/WordPress/Classes/ViewRelated/Site Creation/Web Address/SitePromptView.xib +++ /dev/null @@ -1,123 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/WordPress/Classes/ViewRelated/Site Creation/Web Address/WebAddressStep.swift b/WordPress/Classes/ViewRelated/Site Creation/Web Address/WebAddressStep.swift index 581b5f6d52d2..b38e339245bc 100644 --- a/WordPress/Classes/ViewRelated/Site Creation/Web Address/WebAddressStep.swift +++ b/WordPress/Classes/ViewRelated/Site Creation/Web Address/WebAddressStep.swift @@ -5,7 +5,16 @@ final class WebAddressStep: WizardStep { private let service: SiteAddressService private(set) lazy var content: UIViewController = { - return WebAddressWizardContent(creator: creator, service: self.service) { [weak self] (address) in + let primaryActionTitle = creator.domainPurchasingEnabled ? + DomainSelectionViewController.Strings.selectDomain : + DomainSelectionViewController.Strings.createSite + + return DomainSelectionViewController( + service: service, + domainSelectionType: .siteCreation, + primaryActionTitle: primaryActionTitle, + includeSupportButton: false + ) { [weak self] (address) in self?.didSelect(address) } }() diff --git a/WordPress/Classes/ViewRelated/Site Creation/Wizard/SiteCreationWizard.swift b/WordPress/Classes/ViewRelated/Site Creation/Wizard/SiteCreationWizard.swift index 158b60e8a2b0..002e7070a1d6 100644 --- a/WordPress/Classes/ViewRelated/Site Creation/Wizard/SiteCreationWizard.swift +++ b/WordPress/Classes/ViewRelated/Site Creation/Wizard/SiteCreationWizard.swift @@ -1,12 +1,5 @@ /// Coordinates the UI flow for creating a new site final class SiteCreationWizard: Wizard { - private lazy var firstContentViewController: UIViewController? = { - guard let firstStep = self.steps.first else { - return nil - } - return firstStep.content - }() - private lazy var navigation: WizardNavigation? = { return WizardNavigation(steps: self.steps) }() diff --git a/WordPress/Classes/ViewRelated/Site Creation/Wizard/SiteCreator.swift b/WordPress/Classes/ViewRelated/Site Creation/Wizard/SiteCreator.swift index 28388c637619..7bbd21a85008 100644 --- a/WordPress/Classes/ViewRelated/Site Creation/Wizard/SiteCreator.swift +++ b/WordPress/Classes/ViewRelated/Site Creation/Wizard/SiteCreator.swift @@ -2,23 +2,6 @@ import Foundation import WordPressKit -extension DomainSuggestion { - var subdomain: String { - return domainName.components(separatedBy: ".").first ?? "" - } - - var isWordPress: Bool { - return domainName.contains("wordpress.com") - } -} - -// MARK: - SiteCreationRequestAssemblyError - -enum SiteCreationRequestAssemblyError: Error { - case invalidSegmentIdentifier - case invalidVerticalIdentifier -} - // MARK: - SiteCreator // Tracks data state shared between Site Creation Wizard Steps. I am not too fond of the name, but it kind of works for now. @@ -31,6 +14,8 @@ final class SiteCreator { var information: SiteInformation? var address: DomainSuggestion? var planId: Int? + /// Users can opt for a free domain name in Plans selection + var addressFromPlanSelection: String? /// Generates the final object that will be posted to the backend /// @@ -66,15 +51,17 @@ final class SiteCreator { domainPurchasingEnabled && planId != nil } - /// Returns the domain suggestion if there's one, + /// Returns domain name selected in plan seletion + /// - otherwise the domain suggestion selected in domain view if there's one, /// - otherwise a site name if there's one, /// - otherwise an empty string. private var siteURLString: String { - - guard let domainSuggestion = address else { + guard let domainName = addressFromPlanSelection ?? address?.domainName else { return information?.title ?? "" } - return domainSuggestion.isWordPress ? domainSuggestion.subdomain : domainSuggestion.domainName + + + return domainName.isWordPress ? domainName.subdomain : domainName } private enum Strings { @@ -82,3 +69,25 @@ final class SiteCreator { static let siteCreationFlowForNoAddress = "with-design-picker" } } + +// MARK: - Helper Extensions + +extension String { + var subdomain: String { + return components(separatedBy: ".").first ?? "" + } + + var isWordPress: Bool { + return contains("wordpress.com") + } +} + +extension DomainSuggestion { + var subdomain: String { + return domainName.subdomain + } + + var isWordPress: Bool { + return domainName.isWordPress + } +} diff --git a/WordPress/Classes/ViewRelated/Site Creation/Wizard/WizardNavigation.swift b/WordPress/Classes/ViewRelated/Site Creation/Wizard/WizardNavigation.swift index ab39f2898ccc..772f566ad404 100644 --- a/WordPress/Classes/ViewRelated/Site Creation/Wizard/WizardNavigation.swift +++ b/WordPress/Classes/ViewRelated/Site Creation/Wizard/WizardNavigation.swift @@ -5,13 +5,6 @@ final class WizardNavigation: UINavigationController { private let steps: [WizardStep] private let pointer: WizardNavigationPointer - private lazy var firstContentViewController: UIViewController? = { - guard let firstStep = self.steps.first else { - return nil - } - return firstStep.content - }() - init(steps: [WizardStep]) { self.steps = steps self.pointer = WizardNavigationPointer(capacity: steps.count) diff --git a/WordPress/Classes/ViewRelated/Stats/Charts/Charts+AxisFormatters.swift b/WordPress/Classes/ViewRelated/Stats/Charts/Charts+AxisFormatters.swift index d350205f1009..1c3ecb9ce2d3 100644 --- a/WordPress/Classes/ViewRelated/Stats/Charts/Charts+AxisFormatters.swift +++ b/WordPress/Classes/ViewRelated/Stats/Charts/Charts+AxisFormatters.swift @@ -1,7 +1,7 @@ import Foundation -import Charts +import DGCharts // MARK: - HorizontalAxisFormatter diff --git a/WordPress/Classes/ViewRelated/Stats/Charts/Charts+LargeValueFormatter.swift b/WordPress/Classes/ViewRelated/Stats/Charts/Charts+LargeValueFormatter.swift index 50f4b9ee7f3d..9a22564f6f35 100644 --- a/WordPress/Classes/ViewRelated/Stats/Charts/Charts+LargeValueFormatter.swift +++ b/WordPress/Classes/ViewRelated/Stats/Charts/Charts+LargeValueFormatter.swift @@ -1,9 +1,7 @@ // Ported from https://github.com/danielgindi/Charts/ChartsDemo-iOS/Swift/Formatters/LargeValueFormatter.swift import Foundation -import Charts - -private let MAX_LENGTH = 5 +import DGCharts @objc protocol Testing123 { } diff --git a/WordPress/Classes/ViewRelated/Stats/Charts/Charts+Support.swift b/WordPress/Classes/ViewRelated/Stats/Charts/Charts+Support.swift index d9803fd4d956..1341bbb99451 100644 --- a/WordPress/Classes/ViewRelated/Stats/Charts/Charts+Support.swift +++ b/WordPress/Classes/ViewRelated/Stats/Charts/Charts+Support.swift @@ -1,5 +1,5 @@ -import Charts +import DGCharts // MARK: - Charts extensions diff --git a/WordPress/Classes/ViewRelated/Stats/Charts/InsightsLineChart.swift b/WordPress/Classes/ViewRelated/Stats/Charts/InsightsLineChart.swift index c0fbc3219a64..4d8aea514d3c 100644 --- a/WordPress/Classes/ViewRelated/Stats/Charts/InsightsLineChart.swift +++ b/WordPress/Classes/ViewRelated/Stats/Charts/InsightsLineChart.swift @@ -1,5 +1,5 @@ import Foundation -import Charts +import DGCharts import Kanvas // MARK: - StatsInsightsFilterDimension @@ -129,15 +129,6 @@ class InsightsLineChart { return (thisWeekEntries: thisWeekEntries, prevWeekEntries: prevWeekEntries) } - - func primaryLineColor(forFilterDimension filterDimension: StatsInsightsFilterDimension) -> UIColor { - switch filterDimension { - case .views: - return UIColor(light: .muriel(name: .blue, .shade50), dark: .muriel(name: .blue, .shade50)) - case .visitors: - return UIColor(light: .muriel(name: .purple, .shade50), dark: .muriel(name: .purple, .shade50)) - } - } } private extension InsightsLineChart { diff --git a/WordPress/Classes/ViewRelated/Stats/Charts/PeriodChart.swift b/WordPress/Classes/ViewRelated/Stats/Charts/PeriodChart.swift index c81e33987c4f..759c8ee60b50 100644 --- a/WordPress/Classes/ViewRelated/Stats/Charts/PeriodChart.swift +++ b/WordPress/Classes/ViewRelated/Stats/Charts/PeriodChart.swift @@ -1,5 +1,5 @@ import Foundation -import Charts +import DGCharts // MARK: - StatsPeriodFilterDimension diff --git a/WordPress/Classes/ViewRelated/Stats/Charts/PostChart.swift b/WordPress/Classes/ViewRelated/Stats/Charts/PostChart.swift index 711eebf8ea44..0291f49a2a6b 100644 --- a/WordPress/Classes/ViewRelated/Stats/Charts/PostChart.swift +++ b/WordPress/Classes/ViewRelated/Stats/Charts/PostChart.swift @@ -1,5 +1,5 @@ import Foundation -import Charts +import DGCharts // MARK: - PostChartType diff --git a/WordPress/Classes/ViewRelated/Stats/Charts/StatsBarChartView.swift b/WordPress/Classes/ViewRelated/Stats/Charts/StatsBarChartView.swift index aa33583a3894..bb422b6c8392 100644 --- a/WordPress/Classes/ViewRelated/Stats/Charts/StatsBarChartView.swift +++ b/WordPress/Classes/ViewRelated/Stats/Charts/StatsBarChartView.swift @@ -1,5 +1,5 @@ import UIKit -import Charts +import DGCharts // MARK: - StatsBarChartViewDelegate diff --git a/WordPress/Classes/ViewRelated/Stats/Charts/StatsLineChartView.swift b/WordPress/Classes/ViewRelated/Stats/Charts/StatsLineChartView.swift index d3273b84949b..1138bdda561d 100644 --- a/WordPress/Classes/ViewRelated/Stats/Charts/StatsLineChartView.swift +++ b/WordPress/Classes/ViewRelated/Stats/Charts/StatsLineChartView.swift @@ -1,5 +1,5 @@ import UIKit -import Charts +import DGCharts // MARK: - StatsLineChartViewDelegate @@ -18,14 +18,10 @@ class StatsLineChartView: LineChartView { private struct Constants { static let intrinsicHeight = CGFloat(190) - static let highlightAlpha = CGFloat(1) static let highlightLineWidth = 1.0 static let highlightLineDashLengths = 4.4 static let horizontalAxisLabelCount = 3 - static let presentationDelay = TimeInterval(0.5) - static let primaryDataSetIndex = 0 static let rotationDelay = TimeInterval(0.35) - static let secondaryDataSetIndex = 1 static let topOffset = CGFloat(16) static let trailingOffset = CGFloat(8) static let verticalAxisLabelCount = 5 @@ -57,13 +53,6 @@ class StatsLineChartView: LineChartView { private var statsInsightsFilterDimension: StatsInsightsFilterDimension - private var isHighlightNeeded: Bool { - guard let primaryDataSet = primaryDataSet, primaryDataSet.isHighlightEnabled else { - return false - } - return styling.primaryHighlightColor != nil - } - private var primaryDataSet: ChartDataSetProtocol? { return data?.dataSets.first } @@ -308,8 +297,6 @@ private extension StatsLineChartView { // MARK: - ChartViewDelegate -private typealias StatsLineChartMarker = MarkerView - extension StatsLineChartView: ChartViewDelegate { func chartValueSelected(_ chartView: ChartViewBase, entry: ChartDataEntry, highlight: Highlight) { captureAnalyticsEvent() diff --git a/WordPress/Classes/ViewRelated/Stats/Extensions/WPStyleGuide+Stats.swift b/WordPress/Classes/ViewRelated/Stats/Extensions/WPStyleGuide+Stats.swift index 0481c0315762..50cbfb96b9cb 100644 --- a/WordPress/Classes/ViewRelated/Stats/Extensions/WPStyleGuide+Stats.swift +++ b/WordPress/Classes/ViewRelated/Stats/Extensions/WPStyleGuide+Stats.swift @@ -245,8 +245,6 @@ extension WPStyleGuide { static let customizeInsightsDismissButtonFont = WPStyleGuide.fontForTextStyle(.body, fontWeight: .regular) static let customizeInsightsTryButtonFont = UIFont.systemFont(ofSize: UIFont.preferredFont(forTextStyle: .body).pointSize, weight: .medium) - static let manageInsightsButtonTintColor = UIColor.textSubtle - static let positiveColor = UIColor.success static let negativeColor = UIColor.error static let neutralColor = UIColor.muriel(color: MurielColor(name: .blue)) diff --git a/WordPress/Classes/ViewRelated/Stats/Helpers/StatsPeriodHelper.swift b/WordPress/Classes/ViewRelated/Stats/Helpers/StatsPeriodHelper.swift index 090a4590177b..554b4a51b408 100644 --- a/WordPress/Classes/ViewRelated/Stats/Helpers/StatsPeriodHelper.swift +++ b/WordPress/Classes/ViewRelated/Stats/Helpers/StatsPeriodHelper.swift @@ -168,14 +168,6 @@ class StatsPeriodHelper { } private extension Date { - func adjusted(for period: StatsPeriodUnit, in calendar: Calendar, value: Int) -> Date { - guard let adjustedDate = calendar.date(byAdding: period.calendarComponent, value: value, to: self) else { - DDLogError("[Stats] Couldn't do basic math on Calendars in Stats. Returning original value.") - return self - } - return adjustedDate - } - func lastDayOfTheWeek(in calendar: Calendar, with offset: Int) -> Date? { let components = DateComponents(day: 7 * offset) diff --git a/WordPress/Classes/ViewRelated/Stats/Insights/SiteStatsInsightsTableViewController.swift b/WordPress/Classes/ViewRelated/Stats/Insights/SiteStatsInsightsTableViewController.swift index 52612b562741..238ecda83305 100644 --- a/WordPress/Classes/ViewRelated/Stats/Insights/SiteStatsInsightsTableViewController.swift +++ b/WordPress/Classes/ViewRelated/Stats/Insights/SiteStatsInsightsTableViewController.swift @@ -389,10 +389,6 @@ extension SiteStatsInsightsTableViewController: SiteStatsInsightsDelegate { } func showPostingActivityDetails() { - guard let viewModel = viewModel else { - return - } - let postingActivityViewModel = PostingActivityViewModel(insightsStore: insightsStore) let postingActivityViewController = PostingActivityViewController.loadFromStoryboard { coder in return PostingActivityViewController(coder: coder, viewModel: postingActivityViewModel) diff --git a/WordPress/Classes/ViewRelated/Stats/Insights/SiteStatsInsightsViewModel.swift b/WordPress/Classes/ViewRelated/Stats/Insights/SiteStatsInsightsViewModel.swift index f6b44a0c49ba..90f4341b4a25 100644 --- a/WordPress/Classes/ViewRelated/Stats/Insights/SiteStatsInsightsViewModel.swift +++ b/WordPress/Classes/ViewRelated/Stats/Insights/SiteStatsInsightsViewModel.swift @@ -383,10 +383,6 @@ class SiteStatsInsightsViewModel: Observable { ]) } - func isFetchingOverview() -> Bool { - return insightsStore.isFetchingOverview - } - func fetchingFailed() -> Bool { return insightsStore.fetchingFailed(for: .insights) } @@ -395,10 +391,6 @@ class SiteStatsInsightsViewModel: Observable { return insightsStore.containsCachedData(for: insightsToShow) } - func yearlyPostingActivity(from date: Date = Date()) -> [[PostingStreakEvent]] { - return insightsStore.getYearlyPostingActivity(from: date) - } - func annualInsightsYear() -> Int? { return insightsStore.getAnnualAndMostPopularTime()?.annualInsightsYear } @@ -830,13 +822,6 @@ private extension SiteStatsInsightsViewModel { dataRows: followersData ?? []) } - func createAddInsightRow() -> StatsTotalRowData { - return StatsTotalRowData(name: StatSection.insightsAddInsight.title, - data: "", - icon: Style.imageForGridiconType(.plus, withTint: .darkGrey), - statSection: .insightsAddInsight) - } - func updateMostRecentChartData(_ periodSummary: StatsSummaryTimeIntervalData?) { if mostRecentChartData == nil, let periodSummary = periodSummary, diff --git a/WordPress/Classes/ViewRelated/Stats/Insights/StatsLatestPostSummaryInsightsCell.swift b/WordPress/Classes/ViewRelated/Stats/Insights/StatsLatestPostSummaryInsightsCell.swift index c7f0e08a0a74..886bdb5cb297 100644 --- a/WordPress/Classes/ViewRelated/Stats/Insights/StatsLatestPostSummaryInsightsCell.swift +++ b/WordPress/Classes/ViewRelated/Stats/Insights/StatsLatestPostSummaryInsightsCell.swift @@ -7,7 +7,6 @@ class StatsLatestPostSummaryInsightsCell: StatsBaseCell, LatestPostSummaryConfig private typealias Style = WPStyleGuide.Stats private var lastPostInsight: StatsLastPostInsight? private var lastPostDetails: StatsPostDetails? - private var postTitle = StatSection.noPostTitle private let outerStackView = UIStackView() private let postStackView = UIStackView() diff --git a/WordPress/Classes/ViewRelated/Stats/Insights/ViewsVisitors/ViewsVisitorsChartMarker.swift b/WordPress/Classes/ViewRelated/Stats/Insights/ViewsVisitors/ViewsVisitorsChartMarker.swift index 0878b63afbb5..e588c5a1af96 100644 --- a/WordPress/Classes/ViewRelated/Stats/Insights/ViewsVisitors/ViewsVisitorsChartMarker.swift +++ b/WordPress/Classes/ViewRelated/Stats/Insights/ViewsVisitors/ViewsVisitorsChartMarker.swift @@ -1,5 +1,5 @@ import Foundation -import Charts +import DGCharts import UIKit final class ViewsVisitorsChartMarker: MarkerView { diff --git a/WordPress/Classes/ViewRelated/Stats/Period Stats/SiteStatsPeriodTableViewController.swift b/WordPress/Classes/ViewRelated/Stats/Period Stats/SiteStatsPeriodTableViewController.swift index e9b77bb13b31..251de2fe39ab 100644 --- a/WordPress/Classes/ViewRelated/Stats/Period Stats/SiteStatsPeriodTableViewController.swift +++ b/WordPress/Classes/ViewRelated/Stats/Period Stats/SiteStatsPeriodTableViewController.swift @@ -40,10 +40,7 @@ class SiteStatsPeriodTableViewController: UITableViewController, StoryboardLoada if oldValue == nil { initViewModel() } else { - // If the oldValue is equal to the new value the overview cache is cleaned - // This might happen only when a new date has been injected from the dashboard, - // and the app will enter the foreground state. - refreshData(resetOverviewCache: oldValue == selectedPeriod) + refreshData() } } } @@ -81,7 +78,7 @@ class SiteStatsPeriodTableViewController: UITableViewController, StoryboardLoada return } addViewModelListeners() - viewModel?.refreshPeriodOverviewData(withDate: date, forPeriod: period, resetOverviewCache: false) + viewModel?.refreshPeriodOverviewData(withDate: date, forPeriod: period) } } @@ -179,7 +176,7 @@ private extension SiteStatsPeriodTableViewController { refreshData() } - func refreshData(resetOverviewCache: Bool = false) { + func refreshData() { guard let selectedDate = selectedDate, let selectedPeriod = selectedPeriod, viewIsVisible() else { @@ -187,7 +184,7 @@ private extension SiteStatsPeriodTableViewController { return } addViewModelListeners() - viewModel?.refreshPeriodOverviewData(withDate: selectedDate, forPeriod: selectedPeriod, resetOverviewCache: resetOverviewCache) + viewModel?.refreshPeriodOverviewData(withDate: selectedDate, forPeriod: selectedPeriod) } func applyTableUpdates() { diff --git a/WordPress/Classes/ViewRelated/Stats/Period Stats/SiteStatsPeriodViewModel.swift b/WordPress/Classes/ViewRelated/Stats/Period Stats/SiteStatsPeriodViewModel.swift index b1db0511d9fc..217c50c555d6 100644 --- a/WordPress/Classes/ViewRelated/Stats/Period Stats/SiteStatsPeriodViewModel.swift +++ b/WordPress/Classes/ViewRelated/Stats/Period Stats/SiteStatsPeriodViewModel.swift @@ -218,11 +218,7 @@ class SiteStatsPeriodViewModel: Observable { // MARK: - Refresh Data - func refreshPeriodOverviewData(withDate date: Date, forPeriod period: StatsPeriodUnit, resetOverviewCache: Bool = false) { - if resetOverviewCache { - mostRecentChartData = nil - } - + func refreshPeriodOverviewData(withDate date: Date, forPeriod period: StatsPeriodUnit) { selectedDate = date lastRequestedPeriod = period ActionDispatcher.dispatch(PeriodAction.refreshPeriodOverviewData(date: date, period: period, forceRefresh: true)) diff --git a/WordPress/Classes/ViewRelated/Stats/Shared Views/Stats Detail/SiteStatsDetailTableViewController.swift b/WordPress/Classes/ViewRelated/Stats/Shared Views/Stats Detail/SiteStatsDetailTableViewController.swift index c73afd119f67..267a576da1ab 100644 --- a/WordPress/Classes/ViewRelated/Stats/Shared Views/Stats Detail/SiteStatsDetailTableViewController.swift +++ b/WordPress/Classes/ViewRelated/Stats/Shared Views/Stats Detail/SiteStatsDetailTableViewController.swift @@ -29,9 +29,7 @@ class SiteStatsDetailTableViewController: UITableViewController, StoryboardLoada private var receipt: Receipt? private let insightsStore = StoreContainer.shared.statsInsights - private var insightsChangeReceipt: Receipt? private let periodStore = StoreContainer.shared.statsPeriod - private var periodChangeReceipt: Receipt? private lazy var tableHandler: ImmuTableViewHandler = { return ImmuTableViewHandler(takeOver: self) @@ -228,12 +226,6 @@ private extension SiteStatsDetailTableViewController { } } - func applyTableUpdates() { - tableView.performBatchUpdates({ - updateStatSectionForFilterChange() - }) - } - func clearExpandedRows() { StatsDataHelper.clearExpandedDetails() } diff --git a/WordPress/Classes/ViewRelated/Stats/Shared Views/Stats Detail/SiteStatsDetailsViewModel.swift b/WordPress/Classes/ViewRelated/Stats/Shared Views/Stats Detail/SiteStatsDetailsViewModel.swift index a99d3568debf..cada7b2dd936 100644 --- a/WordPress/Classes/ViewRelated/Stats/Shared Views/Stats Detail/SiteStatsDetailsViewModel.swift +++ b/WordPress/Classes/ViewRelated/Stats/Shared Views/Stats Detail/SiteStatsDetailsViewModel.swift @@ -681,7 +681,7 @@ private extension SiteStatsDetailsViewModel { StatsTotalRowData(name: $0.title, data: $0.clicksCount.abbreviatedString(), showDisclosure: true, - disclosureURL: $0.iconURL, + disclosureURL: $0.clickedURL, childRows: $0.children.map { StatsTotalRowData(name: $0.title, data: $0.clicksCount.abbreviatedString(), showDisclosure: true, diff --git a/WordPress/Classes/ViewRelated/Stats/Shared Views/Stats Detail/SiteStatsInsightsDetailsTableViewController.swift b/WordPress/Classes/ViewRelated/Stats/Shared Views/Stats Detail/SiteStatsInsightsDetailsTableViewController.swift index de42e4c968cb..0a672b7a893b 100644 --- a/WordPress/Classes/ViewRelated/Stats/Shared Views/Stats Detail/SiteStatsInsightsDetailsTableViewController.swift +++ b/WordPress/Classes/ViewRelated/Stats/Shared Views/Stats Detail/SiteStatsInsightsDetailsTableViewController.swift @@ -17,9 +17,7 @@ class SiteStatsInsightsDetailsTableViewController: SiteStatsBaseTableViewControl private var receipt: Receipt? private let insightsStore = StoreContainer.shared.statsInsights - private var insightsChangeReceipt: Receipt? private let periodStore = StoreContainer.shared.statsPeriod - private var periodChangeReceipt: Receipt? private lazy var tableHandler: ImmuTableViewHandler = { return ImmuTableViewHandler(takeOver: self) @@ -246,28 +244,6 @@ private extension SiteStatsInsightsDetailsTableViewController { func clearExpandedRows() { StatsDataHelper.clearExpandedDetails() } - - func updateStatSectionForFilterChange() { - guard let oldStatSection = statSection else { - return - } - - switch oldStatSection { - case .insightsFollowersWordPress: - statSection = .insightsFollowersEmail - case .insightsFollowersEmail: - statSection = .insightsFollowersWordPress - case .insightsCommentsAuthors: - statSection = .insightsCommentsPosts - case .insightsCommentsPosts: - statSection = .insightsCommentsAuthors - default: - // Return here as `initViewModel` is only needed for filtered cards. - return - } - - initViewModel() - } } // MARK: - SiteStatsDetailsDelegate Methods diff --git a/WordPress/Classes/ViewRelated/Stats/Shared Views/Stats Detail/SiteStatsInsightsDetailsViewModel.swift b/WordPress/Classes/ViewRelated/Stats/Shared Views/Stats Detail/SiteStatsInsightsDetailsViewModel.swift index 38b6816955f8..1f32b412b1fd 100644 --- a/WordPress/Classes/ViewRelated/Stats/Shared Views/Stats Detail/SiteStatsInsightsDetailsViewModel.swift +++ b/WordPress/Classes/ViewRelated/Stats/Shared Views/Stats Detail/SiteStatsInsightsDetailsViewModel.swift @@ -704,10 +704,6 @@ private extension SiteStatsInsightsDetailsViewModel { // MARK: - Tabbed Cards - func tabbedRowsFrom(_ commentsRowData: [StatsTotalRowData]) -> [DetailDataRow] { - return dataRowsFor(commentsRowData) - } - func tabDataForFollowerType(_ followerType: StatSection) -> TabData { let tabTitle = followerType.tabTitle var followers: [StatsFollower] = [] diff --git a/WordPress/Classes/ViewRelated/Stats/Shared Views/Stats Detail/StatsFollowersChartViewModel.swift b/WordPress/Classes/ViewRelated/Stats/Shared Views/Stats Detail/StatsFollowersChartViewModel.swift index eb7d42acf9c8..636054d415d4 100644 --- a/WordPress/Classes/ViewRelated/Stats/Shared Views/Stats Detail/StatsFollowersChartViewModel.swift +++ b/WordPress/Classes/ViewRelated/Stats/Shared Views/Stats Detail/StatsFollowersChartViewModel.swift @@ -44,8 +44,6 @@ struct StatsFollowersChartViewModel { static let emailGroupTitle = NSLocalizedString("Email", comment: "Title of Stats section that shows email followers.") static let socialGroupTitle = NSLocalizedString("Social", comment: "Title of Stats section that shows social followers.") - static let followersMaxGroupCount = 3 - static let wpComColor: UIColor = .muriel(name: .blue, .shade50) static let emailColor: UIColor = .muriel(name: .blue, .shade5) static let socialColor: UIColor = .muriel(name: .orange, .shade30) diff --git a/WordPress/Classes/ViewRelated/Stats/Shared Views/StatsTotalRow.swift b/WordPress/Classes/ViewRelated/Stats/Shared Views/StatsTotalRow.swift index 6332a498df9f..300f32c49c73 100644 --- a/WordPress/Classes/ViewRelated/Stats/Shared Views/StatsTotalRow.swift +++ b/WordPress/Classes/ViewRelated/Stats/Shared Views/StatsTotalRow.swift @@ -104,7 +104,6 @@ class StatsTotalRow: UIView, NibLoadable, Accessible { @IBOutlet weak var disclosureButton: UIButton! private(set) var rowData: StatsTotalRowData? - private var dataBarMaxTrailing: Float = 0.0 private typealias Style = WPStyleGuide.Stats private weak var delegate: StatsTotalRowDelegate? private weak var referrerDelegate: StatsTotalRowReferrerDelegate? diff --git a/WordPress/Classes/ViewRelated/Stats/SiteStatsDashboardViewController.swift b/WordPress/Classes/ViewRelated/Stats/SiteStatsDashboardViewController.swift index d1ae45dde5f8..1291b51bdc1f 100644 --- a/WordPress/Classes/ViewRelated/Stats/SiteStatsDashboardViewController.swift +++ b/WordPress/Classes/ViewRelated/Stats/SiteStatsDashboardViewController.swift @@ -158,13 +158,6 @@ extension SiteStatsDashboardViewController: StatsForegroundObservable { // MARK: - Private Extension private extension SiteStatsDashboardViewController { - - struct Constants { - static let progressViewInitialProgress = Float(0.03) - static let progressViewHideDelay = 1 - static let progressViewHideDuration = 0.15 - } - var currentSelectedPeriod: StatsPeriodType { get { let selectedIndex = filterTabBar?.selectedIndex ?? StatsPeriodType.insights.rawValue diff --git a/WordPress/Classes/ViewRelated/Support/SupportTableViewController.swift b/WordPress/Classes/ViewRelated/Support/SupportTableViewController.swift index 132d155080e8..290527fb88bf 100644 --- a/WordPress/Classes/ViewRelated/Support/SupportTableViewController.swift +++ b/WordPress/Classes/ViewRelated/Support/SupportTableViewController.swift @@ -495,7 +495,6 @@ private extension SupportTableViewController { static let closeButton = NSLocalizedString("support.button.close.title", value: "Done", comment: "Dismiss the current view") static let wpHelpCenter = NSLocalizedString("support.row.helpCenter.title", value: "WordPress Help Center", comment: "Option in Support view to launch the Help Center.") static let contactUs = NSLocalizedString("support.row.contactUs.title", value: "Contact support", comment: "Option in Support view to contact the support team.") - static let wpForums = NSLocalizedString("support.row.forums.title", value: "WordPress Forums", comment: "Option in Support view to view the Forums.") static let prioritySupportSectionHeader = NSLocalizedString("support.sectionHeader.prioritySupport.title", value: "Priority Support", comment: "Section header in Support view for priority support.") static let wpForumsSectionHeader = NSLocalizedString("support.sectionHeader.forum.title", value: "Community Forums", comment: "Section header in Support view for the Forums.") static let advancedSectionHeader = NSLocalizedString("support.sectionHeader.advanced.title", value: "Advanced", comment: "Section header in Support view for advanced information.") diff --git a/WordPress/Classes/ViewRelated/System/Coordinators/MySitesCoordinator.swift b/WordPress/Classes/ViewRelated/System/Coordinators/MySitesCoordinator.swift index fbbe005465c2..66d668f0e523 100644 --- a/WordPress/Classes/ViewRelated/System/Coordinators/MySitesCoordinator.swift +++ b/WordPress/Classes/ViewRelated/System/Coordinators/MySitesCoordinator.swift @@ -69,9 +69,14 @@ class MySitesCoordinator: NSObject { navigationController.restorationIdentifier = MySitesCoordinator.navigationControllerRestorationID navigationController.navigationBar.isTranslucent = false - let tabBarImage = AppStyleGuide.mySiteTabIcon - navigationController.tabBarItem.image = tabBarImage - navigationController.tabBarItem.selectedImage = tabBarImage + if FeatureFlag.newTabIcons.enabled { + navigationController.tabBarItem.image = UIImage(named: "tab-bar-home-unselected")?.withRenderingMode(.alwaysOriginal) + navigationController.tabBarItem.selectedImage = UIImage(named: "tab-bar-home-selected") + } else { + let tabBarImage = AppStyleGuide.mySiteTabIcon + navigationController.tabBarItem.image = tabBarImage + navigationController.tabBarItem.selectedImage = tabBarImage + } navigationController.tabBarItem.accessibilityLabel = NSLocalizedString("My Site", comment: "The accessibility value of the my site tab.") navigationController.tabBarItem.accessibilityIdentifier = "mySitesTabButton" navigationController.tabBarItem.title = NSLocalizedString("My Site", comment: "The accessibility value of the my site tab.") @@ -82,7 +87,7 @@ class MySitesCoordinator: NSObject { @objc private(set) lazy var blogListViewController: BlogListViewController = { - BlogListViewController(meScenePresenter: self.meScenePresenter) + BlogListViewController(configuration: .defaultConfig, meScenePresenter: self.meScenePresenter) }() private lazy var mySiteViewController: MySiteViewController = { @@ -101,16 +106,6 @@ class MySitesCoordinator: NSObject { navigationController.viewControllers = [rootContentViewController] } - // MARK: - Sites List - - private func showSitesList() { - showRootViewController() - - let navigationController = UINavigationController(rootViewController: blogListViewController) - navigationController.modalPresentationStyle = .formSheet - mySiteViewController.present(navigationController, animated: true) - } - // MARK: - Blog Details @objc @@ -125,11 +120,11 @@ class MySitesCoordinator: NSObject { } } - func showBlogDetails(for blog: Blog, then subsection: BlogDetailsSubsection) { + func showBlogDetails(for blog: Blog, then subsection: BlogDetailsSubsection, userInfo: [AnyHashable: Any] = [:]) { showBlogDetails(for: blog) if let mySiteViewController = navigationController.topViewController as? MySiteViewController { - mySiteViewController.showBlogDetailsSubsection(subsection) + mySiteViewController.showBlogDetailsSubsection(subsection, userInfo: userInfo) } } @@ -219,6 +214,10 @@ class MySitesCoordinator: NSObject { showBlogDetails(for: blog, then: .media) } + func showMediaPicker(for blog: Blog) { + showBlogDetails(for: blog, then: .media, userInfo: [BlogDetailsViewController.userInfoShowPickerKey(): true]) + } + func showComments(for blog: Blog) { showBlogDetails(for: blog, then: .comments) } diff --git a/WordPress/Classes/ViewRelated/System/FilterTabBar.swift b/WordPress/Classes/ViewRelated/System/FilterTabBar.swift index 7cd9dd8b8253..e28abe838f06 100644 --- a/WordPress/Classes/ViewRelated/System/FilterTabBar.swift +++ b/WordPress/Classes/ViewRelated/System/FilterTabBar.swift @@ -33,6 +33,7 @@ class FilterTabBar: UIControl { scrollView.translatesAutoresizingMaskIntoConstraints = false scrollView.showsHorizontalScrollIndicator = false scrollView.showsVerticalScrollIndicator = false + scrollView.scrollsToTop = false return scrollView }() @@ -148,21 +149,6 @@ class FilterTabBar: UIControl { } } - /// Accessory view displayed on the leading end of the tab bar. - /// - var accessoryView: UIView? = nil { - didSet { - if let oldValue = oldValue { - oldValue.removeFromSuperview() - } - - if let accessoryView = accessoryView { - accessoryView.setContentCompressionResistancePriority(.required, for: .horizontal) - stackView.insertArrangedSubview(accessoryView, at: 0) - } - } - } - // MARK: - Tab Sizing private var stackViewEdgeConstraints: [NSLayoutConstraint]! { @@ -502,8 +488,18 @@ private class TabBarButton: UIButton { setFont() } + override var isSelected: Bool { + didSet { + setFont() + } + } + private func setFont() { - titleLabel?.font = WPStyleGuide.fontForTextStyle(.subheadline, symbolicTraits: .traitBold, maximumPointSize: TabFont.maxSize) + if isSelected { + titleLabel?.font = WPStyleGuide.fontForTextStyle(.subheadline, symbolicTraits: .traitBold, maximumPointSize: TabFont.maxSize) + } else { + titleLabel?.font = WPStyleGuide.fontForTextStyle(.subheadline, maximumPointSize: TabFont.maxSize) + } } override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { diff --git a/WordPress/Classes/ViewRelated/System/Floating Create Button/CreateButtonCoordinator.swift b/WordPress/Classes/ViewRelated/System/Floating Create Button/CreateButtonCoordinator.swift index 5b4bed599cb6..ca8d879a1c03 100644 --- a/WordPress/Classes/ViewRelated/System/Floating Create Button/CreateButtonCoordinator.swift +++ b/WordPress/Classes/ViewRelated/System/Floating Create Button/CreateButtonCoordinator.swift @@ -153,10 +153,6 @@ import WordPressFlux } } - private func isShowingStoryOption() -> Bool { - actions.contains(where: { $0 is StoryAction }) - } - private func actionSheetController(with traitCollection: UITraitCollection) -> UIViewController { let actionSheetVC = CreateButtonActionSheet(headerView: createPromptHeaderView(), actions: actions) setupPresentation(on: actionSheetVC, for: traitCollection) diff --git a/WordPress/Classes/ViewRelated/System/Floating Create Button/FloatingActionButton.swift b/WordPress/Classes/ViewRelated/System/Floating Create Button/FloatingActionButton.swift index e8d1181720bb..1dd08a8949ea 100644 --- a/WordPress/Classes/ViewRelated/System/Floating Create Button/FloatingActionButton.swift +++ b/WordPress/Classes/ViewRelated/System/Floating Create Button/FloatingActionButton.swift @@ -1,8 +1,6 @@ /// A rounded button with a shadow intended for use as a "Floating Action Button" class FloatingActionButton: UIButton { - private var shadowLayer: CALayer? - private enum Constants { static let shadowColor: UIColor = UIColor.gray(.shade20) static let shadowRadius: CGFloat = 3 diff --git a/WordPress/Classes/ViewRelated/System/Floating Create Button/SheetActions.swift b/WordPress/Classes/ViewRelated/System/Floating Create Button/SheetActions.swift index ba7c16a2cd88..e45852afde2a 100644 --- a/WordPress/Classes/ViewRelated/System/Floating Create Button/SheetActions.swift +++ b/WordPress/Classes/ViewRelated/System/Floating Create Button/SheetActions.swift @@ -39,16 +39,6 @@ struct PageAction: ActionSheetItem { } struct StoryAction: ActionSheetItem { - - private enum Constants { - enum Badge { - static let font = UIFont.preferredFont(forTextStyle: .caption1) - static let insets = UIEdgeInsets(top: 2, left: 8, bottom: 2, right: 8) - static let cornerRadius: CGFloat = 2 - static let backgroundColor = UIColor.muriel(color: MurielColor(name: .red, shade: .shade50)) - } - } - let handler: () -> Void let source: String diff --git a/WordPress/Classes/ViewRelated/System/WPTabBarController+MeTab.swift b/WordPress/Classes/ViewRelated/System/WPTabBarController+MeTab.swift index 1a53ed46d0f9..82fdacb20be1 100644 --- a/WordPress/Classes/ViewRelated/System/WPTabBarController+MeTab.swift +++ b/WordPress/Classes/ViewRelated/System/WPTabBarController+MeTab.swift @@ -8,10 +8,19 @@ extension WPTabBarController { @objc func observeGravatarImageUpdate() { NotificationCenter.default.addObserver(self, selector: #selector(updateGravatarImage(_:)), name: .GravatarImageUpdateNotification, object: nil) + + NotificationCenter.default.addObserver(self, selector: #selector(accountDidChange), name: .WPAccountDefaultWordPressComAccountChanged, object: nil) + + NotificationCenter.default.addObserver(self, selector: #selector(accountDidChange), name: .WPAccountEmailAndDefaultBlogUpdated, object: nil) } - @objc func configureMeTabImage(placeholderImage: UIImage) { - meNavigationController?.tabBarItem.image = placeholderImage + @objc func configureMeTabImage(placeholderImage: UIImage?) { + configureMeTabImage(unselectedPlaceholderImage: placeholderImage, selectedPlaceholderImage: placeholderImage) + } + + @objc func configureMeTabImage(unselectedPlaceholderImage: UIImage?, selectedPlaceholderImage: UIImage?) { + meNavigationController?.tabBarItem.image = unselectedPlaceholderImage + meNavigationController?.tabBarItem.selectedImage = selectedPlaceholderImage guard let account = defaultAccount(), let email = account.email else { @@ -38,12 +47,22 @@ extension WPTabBarController { ImageCache.shared.setImage(image, forKey: url.absoluteString) meNavigationController?.tabBarItem.configureGravatarImage(image) } + + @objc private func accountDidChange() { + guard FeatureFlag.newTabIcons.enabled else { + configureMeTabImage(placeholderImage: UIImage(named: "icon-tab-me")) + return + } + + configureMeTabImage(unselectedPlaceholderImage: UIImage(named: "tab-bar-me-unselected"), + selectedPlaceholderImage: UIImage(named: "tab-bar-me-selected")) + } } extension UITabBarItem { func configureGravatarImage(_ image: UIImage) { - let gravatarIcon = image.gravatarIcon(size: 28.0) + let gravatarIcon = image.gravatarIcon(size: 26.0) self.image = gravatarIcon?.blackAndWhite?.withAlpha(0.36) self.selectedImage = gravatarIcon } diff --git a/WordPress/Classes/ViewRelated/System/WPTabBarController+Swift.swift b/WordPress/Classes/ViewRelated/System/WPTabBarController+Swift.swift index 0ab38e7acbff..a6becc8df5a2 100644 --- a/WordPress/Classes/ViewRelated/System/WPTabBarController+Swift.swift +++ b/WordPress/Classes/ViewRelated/System/WPTabBarController+Swift.swift @@ -106,4 +106,18 @@ extension WPTabBarController { return selectedViewController.supportedInterfaceOrientations } + + @objc func animateSelectedItem(_ item: UITabBarItem, for tabBar: UITabBar) { + + guard let index = tabBar.items?.firstIndex(of: item), tabBar.subviews.count > index + 1, + let imageView = tabBar.subviews[index + 1].subviews.last as? UIImageView else { + return + } + + let bounceAnimation = CAKeyframeAnimation(keyPath: "transform.scale") + bounceAnimation.values = [0.8, 1.02, 1.0] + bounceAnimation.duration = TimeInterval(0.2) + bounceAnimation.calculationMode = CAAnimationCalculationMode.cubic + imageView.layer.add(bounceAnimation, forKey: nil) + } } diff --git a/WordPress/Classes/ViewRelated/System/WPTabBarController.m b/WordPress/Classes/ViewRelated/System/WPTabBarController.m index 390af0d56201..cf722e609c93 100644 --- a/WordPress/Classes/ViewRelated/System/WPTabBarController.m +++ b/WordPress/Classes/ViewRelated/System/WPTabBarController.m @@ -181,9 +181,14 @@ - (UINavigationController *)readerNavigationController _readerNavigationController.navigationBar.translucent = NO; _readerNavigationController.view.backgroundColor = [UIColor murielBasicBackground]; - UIImage *readerTabBarImage = [UIImage imageNamed:@"icon-tab-reader"]; - _readerNavigationController.tabBarItem.image = readerTabBarImage; - _readerNavigationController.tabBarItem.selectedImage = readerTabBarImage; + if ([Feature enabled:FeatureFlagNewTabIcons]) { + _readerNavigationController.tabBarItem.image = [[UIImage imageNamed:@"tab-bar-reader-unselected"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]; + _readerNavigationController.tabBarItem.selectedImage = [UIImage imageNamed:@"tab-bar-reader-selected"]; + } else { + UIImage *readerTabBarImage = [UIImage imageNamed:@"icon-tab-reader"]; + _readerNavigationController.tabBarItem.image = readerTabBarImage; + _readerNavigationController.tabBarItem.selectedImage = readerTabBarImage; + } _readerNavigationController.restorationIdentifier = WPReaderNavigationRestorationID; _readerNavigationController.tabBarItem.accessibilityIdentifier = @"readerTabButton"; _readerNavigationController.tabBarItem.title = NSLocalizedString(@"Reader", @"The accessibility value of the Reader tab."); @@ -207,11 +212,19 @@ - (UINavigationController *)notificationsNavigationController } _notificationsNavigationController = [[UINavigationController alloc] initWithRootViewController:rootViewController]; _notificationsNavigationController.navigationBar.translucent = NO; - self.notificationsTabBarImage = [UIImage imageNamed:@"icon-tab-notifications"]; - NSString *unreadImageName = [AppConfiguration isJetpack] ? @"icon-tab-notifications-unread-jetpack" : @"icon-tab-notifications-unread"; - self.notificationsTabBarImageUnread = [[UIImage imageNamed:unreadImageName] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]; - _notificationsNavigationController.tabBarItem.image = self.notificationsTabBarImage; - _notificationsNavigationController.tabBarItem.selectedImage = self.notificationsTabBarImage; + if ([Feature enabled:FeatureFlagNewTabIcons]) { + self.notificationsTabBarImage = [[UIImage imageNamed:@"tab-bar-notifications-unselected"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]; + NSString *unreadImageName = [AppConfiguration isJetpack] ? @"tab-bar-notifications-unread-jp" : @"tab-bar-notifications-unread-wp"; + self.notificationsTabBarImageUnread = [[UIImage imageNamed:unreadImageName] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]; + _notificationsNavigationController.tabBarItem.image = self.notificationsTabBarImage; + _notificationsNavigationController.tabBarItem.selectedImage = [UIImage imageNamed:@"tab-bar-notifications-selected"]; + } else { + self.notificationsTabBarImage = [UIImage imageNamed:@"icon-tab-notifications"]; + NSString *unreadImageName = [AppConfiguration isJetpack] ? @"icon-tab-notifications-unread-jetpack" : @"icon-tab-notifications-unread"; + self.notificationsTabBarImageUnread = [[UIImage imageNamed:unreadImageName] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]; + _notificationsNavigationController.tabBarItem.image = self.notificationsTabBarImage; + _notificationsNavigationController.tabBarItem.selectedImage = self.notificationsTabBarImage; + } _notificationsNavigationController.restorationIdentifier = WPNotificationsNavigationRestorationID; _notificationsNavigationController.tabBarItem.accessibilityIdentifier = @"notificationsTabButton"; _notificationsNavigationController.tabBarItem.accessibilityLabel = NSLocalizedString(@"Notifications", @"Notifications tab bar item accessibility label"); @@ -224,7 +237,12 @@ - (UINavigationController *)meNavigationController { if (!_meNavigationController) { _meNavigationController = [[UINavigationController alloc] initWithRootViewController:self.meViewController]; - [self configureMeTabImageWithPlaceholderImage:[UIImage imageNamed:@"icon-tab-me"]]; + if ([Feature enabled:FeatureFlagNewTabIcons]) { + [self configureMeTabImageWithUnselectedPlaceholderImage:[[UIImage imageNamed:@"tab-bar-me-unselected"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal] + selectedPlaceholderImage:[UIImage imageNamed:@"tab-bar-me-selected"]]; + } else { + [self configureMeTabImageWithPlaceholderImage:[UIImage imageNamed:@"icon-tab-me"]]; + } _meNavigationController.restorationIdentifier = WPMeNavigationRestorationID; _meNavigationController.tabBarItem.accessibilityLabel = NSLocalizedString(@"Me", @"The accessibility value of the me tab."); _meNavigationController.tabBarItem.accessibilityIdentifier = @"meTabButton"; @@ -472,6 +490,16 @@ - (void)showNotificationsTabForNoteWithID:(NSString *)notificationID [self.notificationsViewController showDetailsForNotificationWithID:notificationID]; } +#pragma mark - UITabBarDelegate + +- (void)tabBar:(UITabBar *)tabBar didSelectItem:(UITabBarItem *)item +{ + UIImpactFeedbackGenerator *generator = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleMedium]; + [generator impactOccurred]; + + [self animateSelectedItem:item for:tabBar]; +} + #pragma mark - Zendesk Notifications - (void)updateIconIndicators:(NSNotification *)notification diff --git a/WordPress/Classes/ViewRelated/Themes/WPStyleGuide+Themes.swift b/WordPress/Classes/ViewRelated/Themes/WPStyleGuide+Themes.swift index 18617f6635ca..660c676c763d 100644 --- a/WordPress/Classes/ViewRelated/Themes/WPStyleGuide+Themes.swift +++ b/WordPress/Classes/ViewRelated/Themes/WPStyleGuide+Themes.swift @@ -98,19 +98,18 @@ extension WPStyleGuide { let columnWidth = trunc(columnsWidth / numberOfColumns) return columnWidth } + public static func cellHeightForCellWidth(_ width: CGFloat) -> CGFloat { let imageHeight = (width - cellImageInset) * cellImageRatio return imageHeight + cellInfoBarHeight } - public static func cellHeightForFrameWidth(_ width: CGFloat) -> CGFloat { - let cellWidth = cellWidthForFrameWidth(width) - return cellHeightForCellWidth(cellWidth) - } + public static func cellSizeForFrameWidth(_ width: CGFloat) -> CGSize { let cellWidth = cellWidthForFrameWidth(width) let cellHeight = cellHeightForCellWidth(cellWidth) return CGSize(width: cellWidth.zeroIfNaN(), height: cellHeight.zeroIfNaN()) } + public static func imageWidthForFrameWidth(_ width: CGFloat) -> CGFloat { let cellWidth = cellWidthForFrameWidth(width) return cellWidth - cellImageInset @@ -121,7 +120,6 @@ extension WPStyleGuide { public static let themeMargins = UIEdgeInsets(top: rowMargin, left: columnMargin, bottom: rowMargin, right: columnMargin) public static let infoMargins = UIEdgeInsets() - public static let minimumSearchHeight: CGFloat = 44 public static let searchAnimationDuration: TimeInterval = 0.2 } diff --git a/WordPress/Classes/ViewRelated/Tools/Confirmable.h b/WordPress/Classes/ViewRelated/Tools/Confirmable.h deleted file mode 100644 index cfb6fd153807..000000000000 --- a/WordPress/Classes/ViewRelated/Tools/Confirmable.h +++ /dev/null @@ -1,9 +0,0 @@ -#import - -@protocol Confirmable - -- (void)cancel; -- (void)confirm; - -@end - diff --git a/WordPress/Classes/ViewRelated/Tools/PromptViewController.swift b/WordPress/Classes/ViewRelated/Tools/PromptViewController.swift deleted file mode 100644 index 50229548d20d..000000000000 --- a/WordPress/Classes/ViewRelated/Tools/PromptViewController.swift +++ /dev/null @@ -1,125 +0,0 @@ -import Foundation -import UIKit - - -/// Wraps a given UIViewController, that conforms to the Confirmable Protocol, into: -/// - A PromptViewController instance, which deals with the NavigationItem buttons -/// - (And verything) inside a UINavigationController instance. -/// -public func PromptViewController(_ viewController: T) -> UINavigationController where T: Confirmable { - let viewController = PromptContainerViewController(viewController: viewController) - return UINavigationController(rootViewController: viewController) -} - - -/// ViewController container, that presents a Done / Cancel button, and forwards their events to -/// the childrenViewController (which *must* implement the Confirmable protocol). -/// -private class PromptContainerViewController: UIViewController { - // MARK: - Initializers / Deinitializers - - deinit { - stopListeningToProperties(childViewController) - } - - @objc init(viewController: UIViewController) { - // You stay with us, sir - childViewController = viewController - - super.init(nibName: nil, bundle: nil) - precondition(viewController.conforms(to: Confirmable.self)) - - setupNavigationButtons() - attachChildViewController(viewController) - setupChildViewConstraints(viewController.view) - startListeningToProperties(viewController) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("Not meant for Nib usage!") - } - - - - // MARK: - Private Helpers - - fileprivate func attachChildViewController(_ viewController: UIViewController) { - // Attach! - viewController.willMove(toParent: self) - view.addSubview(viewController.view) - addChild(viewController) - viewController.didMove(toParent: self) - } - - fileprivate func setupChildViewConstraints(_ childrenView: UIView) { - // We grow, you grow. We shrink, you shrink. Capicci? - childrenView.translatesAutoresizingMaskIntoConstraints = false - childrenView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true - childrenView.heightAnchor.constraint(equalTo: view.heightAnchor).isActive = true - childrenView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true - childrenView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true - } - - - // MARK: - KVO Rocks! - - fileprivate func startListeningToProperties(_ viewController: UIViewController) { - for key in Properties.all where viewController.responds(to: NSSelectorFromString(key.rawValue)) { - viewController.addObserver(self, forKeyPath: key.rawValue, options: [.initial, .new], context: nil) - } - } - - fileprivate func stopListeningToProperties(_ viewController: UIViewController) { - for key in Properties.all where viewController.responds(to: NSSelectorFromString(key.rawValue)) { - viewController.removeObserver(self, forKeyPath: key.rawValue) - } - } - - override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) { - guard let unwrappedKeyPath = keyPath, let property = Properties(rawValue: unwrappedKeyPath) else { - return - } - - switch property { - case .title: - title = change?[NSKeyValueChangeKey.newKey] as? String ?? String() - case .doneButtonEnabled: - navigationItem.rightBarButtonItem?.isEnabled = change?[NSKeyValueChangeKey.newKey] as? Bool ?? true - } - } - - - // MARK: - Navigation Buttons - - fileprivate func setupNavigationButtons() { - navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, - target: self, - action: #selector(cancelButtonWasPressed)) - - navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, - target: self, - action: #selector(doneButtonWasPressed)) - } - - @objc - @IBAction func cancelButtonWasPressed(_ sender: AnyObject) { - (childViewController as? Confirmable)?.cancel() - } - - @objc - @IBAction func doneButtonWasPressed(_ sender: AnyObject) { - (childViewController as? Confirmable)?.confirm() - } - - - - // MARK: - Private Constants - fileprivate enum Properties: String { - case title = "title" - case doneButtonEnabled = "doneButtonEnabled" - static let all = [title, doneButtonEnabled] - } - - // MARK: - Private Properties - fileprivate let childViewController: UIViewController -} diff --git a/WordPress/Classes/ViewRelated/Tools/SettingsTextViewController.h b/WordPress/Classes/ViewRelated/Tools/SettingsTextViewController.h index 5f8f0e8dcc27..df27e0e18b50 100644 --- a/WordPress/Classes/ViewRelated/Tools/SettingsTextViewController.h +++ b/WordPress/Classes/ViewRelated/Tools/SettingsTextViewController.h @@ -1,5 +1,4 @@ #import -#import "Confirmable.h" // Typedef's typedef NS_ENUM(NSInteger, SettingsTextModes) { @@ -18,7 +17,7 @@ typedef void (^SettingsTextOnDismiss)(void); /// Reusable component that renders a UITextField + Hint onscreen. Useful for Text / Password / Email data entry. /// -@interface SettingsTextViewController : UITableViewController +@interface SettingsTextViewController : UITableViewController /// Block to be executed on dismiss, if the value was effectively updated. /// diff --git a/WordPress/Classes/ViewRelated/Views/LinearGradientView.swift b/WordPress/Classes/ViewRelated/Views/LinearGradientView.swift index 975c5da3e3fb..fa9eee077c17 100644 --- a/WordPress/Classes/ViewRelated/Views/LinearGradientView.swift +++ b/WordPress/Classes/ViewRelated/Views/LinearGradientView.swift @@ -19,10 +19,6 @@ class LinearGradientView: UIView { contentMode = .redraw } - private func configure() { - contentMode = .redraw - } - override func draw(_ rect: CGRect) { guard let context = UIGraphicsGetCurrentContext(), diff --git a/WordPress/Classes/ViewRelated/Views/VerticallyStackedButton.m b/WordPress/Classes/ViewRelated/Views/VerticallyStackedButton.m index 8a559b76bafb..d560c15288be 100644 --- a/WordPress/Classes/ViewRelated/Views/VerticallyStackedButton.m +++ b/WordPress/Classes/ViewRelated/Views/VerticallyStackedButton.m @@ -1,5 +1,6 @@ #import "VerticallyStackedButton.h" #import +#import static const CGFloat ImageLabelSeparation = 2.f; diff --git a/WordPress/Classes/ViewRelated/Views/WPRichText/WPRichContentView.swift b/WordPress/Classes/ViewRelated/Views/WPRichText/WPRichContentView.swift index c85024959db4..b6cf9c9c16e6 100644 --- a/WordPress/Classes/ViewRelated/Views/WPRichText/WPRichContentView.swift +++ b/WordPress/Classes/ViewRelated/Views/WPRichText/WPRichContentView.swift @@ -255,7 +255,7 @@ extension WPRichContentView: WPTextAttachmentManagerDelegate { /// - Parameter size: The proposed size for the gif image. /// - Returns: The most efficient size with good quality. fileprivate func efficientImageSize(with url: URL, proposedSize size: CGSize) -> CGSize { - guard url.isGif else { + guard url.isGif, !url.isWPComEmoji else { return size } @@ -443,19 +443,9 @@ private extension WPRichContentView { struct Constants { static let textContainerInset = UIEdgeInsets.init(top: 0.0, left: 0.0, bottom: 16.0, right: 0.0) static let defaultAttachmentHeight = CGFloat(50.0) - static let photonQuality = 65 } } -// MARK: - Rich Media Struct - -/// A simple struct used to keep references to a rich text image and its associated attachment. -/// -struct RichMedia { - let image: WPRichTextImage - let attachment: WPTextAttachment -} - // This is very much based on Aztec.LayoutManager — most of this code is pretty much copy-pasted // from there and trimmed to only contain the relevant parts. @objc fileprivate class BlockquoteBackgroundLayoutManager: NSLayoutManager { diff --git a/WordPress/Classes/ViewRelated/Views/WPRichText/WPRichTextFormatter.swift b/WordPress/Classes/ViewRelated/Views/WPRichText/WPRichTextFormatter.swift index accbb0321511..61210129d08e 100644 --- a/WordPress/Classes/ViewRelated/Views/WPRichText/WPRichTextFormatter.swift +++ b/WordPress/Classes/ViewRelated/Views/WPRichText/WPRichTextFormatter.swift @@ -30,15 +30,6 @@ class WPRichTextFormatter { ] }() - /// An array of tag names that the formatter can process. - /// - lazy var tagNames: [String] = { - return self.tags.map { tag -> String in - return tag.tagName - } - }() - - /// Converts the specified HTML formatted string to an NSAttributedString. /// /// - Parameters: @@ -272,21 +263,6 @@ class WPRichTextFormatter { } return (processedString, attachments) } - - - /// Returns the html processor that handles the specified tag. - /// - /// - Parameters: - /// - tagName: The name of an HTML tag. - /// - /// - Returns: An HtmlTagProcessor optional. - /// - func processorForTagName(_ tagName: String) -> HtmlTagProcessor? { - return tags.filter({ (item) -> Bool in - item.tagName == tagName - }).first - } - } diff --git a/WordPress/Classes/ViewRelated/WhatsNew/Views/FindOutMoreCell.swift b/WordPress/Classes/ViewRelated/WhatsNew/Views/FindOutMoreCell.swift index 2e8ba5699da4..56ae59925add 100644 --- a/WordPress/Classes/ViewRelated/WhatsNew/Views/FindOutMoreCell.swift +++ b/WordPress/Classes/ViewRelated/WhatsNew/Views/FindOutMoreCell.swift @@ -41,7 +41,6 @@ class FindOutMoreCell: UITableViewCell, Reusable { private extension FindOutMoreCell { enum Appearance { static let buttonTitle = NSLocalizedString("Find out more", comment: "Title for the find out more button in the What's New page.") - static let buttonFont = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .callout), size: 16) static let topMargin: CGFloat = -8 } diff --git a/WordPress/Classes/ViewRelated/WhatsNew/Views/GridCell.swift b/WordPress/Classes/ViewRelated/WhatsNew/Views/GridCell.swift index 96481f9ee480..1eaa7c3b9a87 100644 --- a/WordPress/Classes/ViewRelated/WhatsNew/Views/GridCell.swift +++ b/WordPress/Classes/ViewRelated/WhatsNew/Views/GridCell.swift @@ -139,9 +139,6 @@ private extension GridCell { // heading static let headingFont = UIFont(descriptor: UIFontDescriptor.preferredFontDescriptor(withTextStyle: .subheadline), size: 17) - // sub-heading - static let subHeadingFont = UIFont(descriptor: UIFontDescriptor.preferredFontDescriptor(withTextStyle: .subheadline), size: 15) - // main stack view static let mainStackViewInsets = UIEdgeInsets(top: 0, left: 0, bottom: 24, right: 0) diff --git a/WordPress/Classes/WordPress.xcdatamodeld/.xccurrentversion b/WordPress/Classes/WordPress.xcdatamodeld/.xccurrentversion index 3806e1ca265f..72eae1f85f5d 100644 --- a/WordPress/Classes/WordPress.xcdatamodeld/.xccurrentversion +++ b/WordPress/Classes/WordPress.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - WordPress 152.xcdatamodel + WordPress 153.xcdatamodel diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 153.xcdatamodel/contents b/WordPress/Classes/WordPress.xcdatamodeld/WordPress 153.xcdatamodel/contents new file mode 100644 index 000000000000..e724a3442ad2 --- /dev/null +++ b/WordPress/Classes/WordPress.xcdatamodeld/WordPress 153.xcdatamodel/contents @@ -0,0 +1,879 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/WordPress/Jetpack/AppImages.xcassets/wp-jp-circular-lockup.imageset/Contents.json b/WordPress/Jetpack/AppImages.xcassets/wp-jp-circular-lockup.imageset/Contents.json new file mode 100644 index 000000000000..228e98858de7 --- /dev/null +++ b/WordPress/Jetpack/AppImages.xcassets/wp-jp-circular-lockup.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "wp-jp-circular-lockup.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WordPress/Jetpack/AppImages.xcassets/wp-jp-circular-lockup.imageset/wp-jp-circular-lockup.pdf b/WordPress/Jetpack/AppImages.xcassets/wp-jp-circular-lockup.imageset/wp-jp-circular-lockup.pdf new file mode 100644 index 000000000000..52552a1b7745 Binary files /dev/null and b/WordPress/Jetpack/AppImages.xcassets/wp-jp-circular-lockup.imageset/wp-jp-circular-lockup.pdf differ diff --git a/WordPress/Jetpack/AppStyleGuide.swift b/WordPress/Jetpack/AppStyleGuide.swift index cfb471116e9e..3fd7f60ef1b2 100644 --- a/WordPress/Jetpack/AppStyleGuide.swift +++ b/WordPress/Jetpack/AppStyleGuide.swift @@ -30,7 +30,6 @@ extension AppStyleGuide { // MARK: - Images extension AppStyleGuide { static let mySiteTabIcon = UIImage(named: "jetpack-icon-tab-mysites") - static let aboutAppIcon = UIImage(named: "jetpack-install-logo") static let quickStartExistingSite = UIImage(named: "wp-illustration-quickstart-existing-site-jetpack") } diff --git a/WordPress/Jetpack/Classes/JetpackAuthenticationManager.swift b/WordPress/Jetpack/Classes/JetpackAuthenticationManager.swift index 2598d4c0edb8..39f2bd0ea05f 100644 --- a/WordPress/Jetpack/Classes/JetpackAuthenticationManager.swift +++ b/WordPress/Jetpack/Classes/JetpackAuthenticationManager.swift @@ -5,7 +5,7 @@ struct JetpackAuthenticationManager: AuthenticationHandler { let statusBarStyle: UIStatusBarStyle = .default let prologueViewController: UIViewController? = JetpackPrologueViewController() let buttonViewTopShadowImage: UIImage? = UIImage() - let prologueButtonsBackgroundColor: UIColor? = JetpackPrologueStyleGuide.backgroundColor + let prologueButtonsBackgroundColor: UIColor? = JetpackPrologueStyleGuide.gradientColor let prologueButtonsBlurEffect: UIBlurEffect? = JetpackPrologueStyleGuide.prologueButtonsBlurEffect let prologuePrimaryButtonStyle: NUXButtonStyle? = JetpackPrologueStyleGuide.continueButtonStyle let prologueSecondaryButtonStyle: NUXButtonStyle? = JetpackPrologueStyleGuide.siteAddressButtonStyle diff --git a/WordPress/Jetpack/Classes/NUX/JetpackPrologueStyleGuide.swift b/WordPress/Jetpack/Classes/NUX/JetpackPrologueStyleGuide.swift index e71de459117b..3b6d10b5daa8 100644 --- a/WordPress/Jetpack/Classes/NUX/JetpackPrologueStyleGuide.swift +++ b/WordPress/Jetpack/Classes/NUX/JetpackPrologueStyleGuide.swift @@ -2,19 +2,20 @@ import UIKit import WordPressAuthenticator -/// The colors in here intentionally do not support light or dark modes since they're the same on both. -/// struct JetpackPrologueStyleGuide { // Background color static let backgroundColor = UIColor.clear // Gradient overlay color - static let gradientColor = UIColor(light: .muriel(color: .jetpackGreen, .shade0), dark: .muriel(color: .jetpackGreen, .shade100)) + static let gradientColor = UIColor( + light: .white, + dark: UIColor(hexString: "050A21") + ) // Continue with WordPress button colors - static let continueFillColor = UIColor(light: .muriel(color: .jetpackGreen, .shade50), dark: .white) - static let continueHighlightedFillColor = UIColor(light: .muriel(color: .jetpackGreen, .shade90), dark: whiteWithAlpha07) - static let continueTextColor = UIColor(light: .white, dark: .muriel(color: .jetpackGreen, .shade80)) + static let continueFillColor = JetpackPromptsConfiguration.Constants.evenColor ?? .systemBlue // This is just to satisfy the compiler + static let continueHighlightedFillColor = continueFillColor.withAlphaComponent(0.9) + static let continueTextColor = UIColor.white static let continueHighlightedTextColor = whiteWithAlpha07 @@ -22,8 +23,8 @@ struct JetpackPrologueStyleGuide { static let siteFillColor = UIColor.clear static let siteBorderColor = UIColor.clear static let siteTextColor = UIColor(light: .muriel(color: .jetpackGreen, .shade90), dark: .white) - static let siteHighlightedFillColor = whiteWithAlpha07 - static let siteHighlightedBorderColor = whiteWithAlpha07 + static let siteHighlightedFillColor = UIColor.clear + static let siteHighlightedBorderColor = UIColor.clear static let siteHighlightedTextColor = UIColor(light: .muriel(color: .jetpackGreen, .shade50), dark: whiteWithAlpha07) // Color used in both old and versions @@ -34,9 +35,6 @@ struct JetpackPrologueStyleGuide { // Blur effect for the prologue buttons static let prologueButtonsBlurEffect: UIBlurEffect? = UIBlurEffect(style: .regular) - - - struct Title { static let font: UIFont = WPStyleGuide.fontForTextStyle(.title3, fontWeight: .semibold) static let textColor: UIColor = .white diff --git a/WordPress/Jetpack/Classes/NUX/JetpackPrologueViewController.swift b/WordPress/Jetpack/Classes/NUX/JetpackPrologueViewController.swift index 9a6d2ef857b6..0c239f463fd7 100644 --- a/WordPress/Jetpack/Classes/NUX/JetpackPrologueViewController.swift +++ b/WordPress/Jetpack/Classes/NUX/JetpackPrologueViewController.swift @@ -5,14 +5,6 @@ class JetpackPrologueViewController: UIViewController { @IBOutlet weak var stackView: UIStackView! @IBOutlet weak var titleLabel: UILabel! - var starFieldView: StarFieldView = { - let config = StarFieldViewConfig(particleImage: JetpackPrologueStyleGuide.Stars.particleImage, - starColors: JetpackPrologueStyleGuide.Stars.colors) - let view = StarFieldView(with: config) - view.layer.masksToBounds = true - return view - }() - private let motion: CMMotionManager? = { let motion = CMMotionManager() motion.deviceMotionUpdateInterval = Constants.deviceMotionUpdateInterval @@ -27,7 +19,7 @@ class JetpackPrologueViewController: UIViewController { }() private lazy var logoImageView: UIImageView = { - let imageView = UIImageView(image: UIImage(named: "jetpack-logo")) + let imageView = UIImageView(image: UIImage(named: "wp-jp-circular-lockup")) imageView.translatesAutoresizingMaskIntoConstraints = false return imageView }() @@ -36,11 +28,6 @@ class JetpackPrologueViewController: UIViewController { makeGradientLayer() }() - private lazy var logoWidthConstraint: NSLayoutConstraint = { - let width = Constants.logoWidth(for: traitCollection.horizontalSizeClass) - return logoImageView.widthAnchor.constraint(equalToConstant: width) - }() - private func makeGradientLayer() -> CAGradientLayer { let gradientLayer = CAGradientLayer() @@ -94,26 +81,13 @@ class JetpackPrologueViewController: UIViewController { view.layer.insertSublayer(gradientLayer, above: jetpackAnimatedView.layer) // constraints NSLayoutConstraint.activate([ - logoWidthConstraint, - logoImageView.heightAnchor.constraint(equalTo: logoImageView.widthAnchor), + logoImageView.widthAnchor.constraint(equalToConstant: 132.35), + logoImageView.heightAnchor.constraint(equalToConstant: 80), logoImageView.centerXAnchor.constraint(equalTo: view.centerXAnchor), - logoImageView.topAnchor.constraint(equalTo: view.topAnchor, constant: 68) + logoImageView.topAnchor.constraint(equalTo: view.topAnchor, constant: 135) ]) } - private func loadOldPrologueView() { - view.addSubview(starFieldView) - view.layer.addSublayer(gradientLayer) - titleLabel.text = NSLocalizedString("Site security and performance\nfrom your pocket", comment: "Prologue title label, the \n force splits it into 2 lines.") - titleLabel.textColor = JetpackPrologueStyleGuide.Title.textColor - titleLabel.font = JetpackPrologueStyleGuide.Title.font - // Move the layers to appear below everything else - starFieldView.layer.zPosition = Constants.starLayerPosition - gradientLayer.zPosition = Constants.gradientLayerPosition - addParallax(to: stackView) - updateLabel(for: traitCollection) - } - func updateLabel(for traitCollection: UITraitCollection) { let contentSize = traitCollection.preferredContentSizeCategory @@ -125,8 +99,6 @@ class JetpackPrologueViewController: UIViewController { override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) - logoWidthConstraint.constant = Constants.logoWidth(for: traitCollection.horizontalSizeClass) - guard previousTraitCollection?.userInterfaceStyle != traitCollection.userInterfaceStyle else { updateLabel(for: traitCollection) return @@ -161,8 +133,6 @@ class JetpackPrologueViewController: UIViewController { private struct Constants { static let parallaxAmount: CGFloat = 30 - static let starLayerPosition: CGFloat = -100 - static let gradientLayerPosition: CGFloat = -99 /// New landing screen @@ -172,10 +142,6 @@ class JetpackPrologueViewController: UIViewController { static let defaultAngleDegrees: Double = 30.0 /// Uniform multiplier used to tweak the rate generated from an angle static let angleRateMultiplier: CGFloat = 1.3 - /// Returns the Jetpack logo width depending on the given size class - static func logoWidth(for sizeClass: UIUserInterfaceSizeClass) -> CGFloat { - return sizeClass == .compact ? 68 : 78 - } } } diff --git a/WordPress/Jetpack/Classes/NUX/New Landing Screen/Model/JetpackPrompt.swift b/WordPress/Jetpack/Classes/NUX/New Landing Screen/Model/JetpackPrompt.swift deleted file mode 100644 index f41675bf6502..000000000000 --- a/WordPress/Jetpack/Classes/NUX/New Landing Screen/Model/JetpackPrompt.swift +++ /dev/null @@ -1,10 +0,0 @@ -import SwiftUI - -struct JetpackPrompt: Identifiable { - var id = UUID() - let index: Int - let text: String - let color: Color - var frameHeight: CGFloat - var initialOffset: CGFloat -} diff --git a/WordPress/Jetpack/Classes/NUX/New Landing Screen/ViewModel/JetpackPromptsConfiguration.swift b/WordPress/Jetpack/Classes/NUX/New Landing Screen/ViewModel/JetpackPromptsConfiguration.swift index c8d22eae7ba4..bfba55814066 100644 --- a/WordPress/Jetpack/Classes/NUX/New Landing Screen/ViewModel/JetpackPromptsConfiguration.swift +++ b/WordPress/Jetpack/Classes/NUX/New Landing Screen/ViewModel/JetpackPromptsConfiguration.swift @@ -1,13 +1,13 @@ import SwiftUI +import DesignSystem /// Text and constants for animated prompts in the Jetpack prologue screen struct JetpackPromptsConfiguration { enum Constants { // alternate colors in rows - static let evenColor = UIColor.muriel(color: .jetpackGreen, .shade50) - static let oddColor = UIColor(light: .muriel(color: .jetpackGreen, .shade50).withAlphaComponent(0.5), - dark: .muriel(color: .jetpackGreen, .shade20)) + static let evenColor = UIColor.DS.Background.brand(isJetpack: false) + static let oddColor = UIColor.muriel(color: .jetpackGreen, .shade40) static let basePrompts = [ NSLocalizedString("jetpack.prologue.prompt.updatePlugin", diff --git a/WordPress/Jetpack/Classes/NUX/StarFieldView.swift b/WordPress/Jetpack/Classes/NUX/StarFieldView.swift deleted file mode 100644 index dd9f246c1e10..000000000000 --- a/WordPress/Jetpack/Classes/NUX/StarFieldView.swift +++ /dev/null @@ -1,194 +0,0 @@ -import UIKit - -struct StarFieldViewConfig { - var particleImage: UIImage? - var starColors: [UIColor] -} - -class StarFieldView: UIView { - struct Particle { - let image: UIImage - let tintColor: UIColor - } - - let config: StarFieldViewConfig - - /// The base emitter layer that fills the background - var emitterLayer: StarFieldEmitterLayer? - - /// A special layer that moves when the user touches - var interactiveEmitterLayer: InteractiveStarFieldEmitterLayer? - - // MARK: - Config - init(with config: StarFieldViewConfig) { - self.config = config - super.init(frame: .zero) - - configure() - } - - required init?(coder: NSCoder) { - self.config = StarFieldViewConfig(particleImage: nil, starColors: []) - super.init(coder: coder) - - configure() - } - - private func configure() { - backgroundColor = .clear - - makeEmitterLayer() - } - - private func makeEmitterLayer() { - guard emitterLayer == nil, let image = config.particleImage else { - return - } - - let particles = config.starColors.map { Particle(image: image, tintColor: $0) } - - // Background layer - self.emitterLayer = { - let layer = StarFieldEmitterLayer(with: particles) - self.layer.addSublayer(layer) - return layer - }() - } - - override func layoutSubviews() { - super.layoutSubviews() - - emitterLayer?.frame = bounds - interactiveEmitterLayer?.frame = bounds - } - - // MARK: - Touches - override func touchesBegan(_ touches: Set, with event: UIEvent?) { - if interactiveEmitterLayer == nil { - interactiveEmitterLayer = { - let particles = config.starColors.map { Particle(image: config.particleImage!, tintColor: $0) } - let layer = InteractiveStarFieldEmitterLayer(with: particles) - self.layer.addSublayer(layer) - - return layer - }() - } - - touchesMoved(touches, with: event) - } - - override func touchesMoved(_ touches: Set, with event: UIEvent?) { - guard let firstTouch = touches.first else { - return - } - - let location = firstTouch.location(in: self) - let radius = firstTouch.majorRadius - - interactiveEmitterLayer?.touchesMoved(to: location, with: radius) - } - - override func touchesEnded(_ touches: Set, with event: UIEvent?) { - interactiveEmitterLayer?.touchesEnded() - - } - - override func touchesCancelled(_ touches: Set, with event: UIEvent?) { - touchesEnded(touches, with: event) - } -} - -class StarFieldEmitterLayer: CAEmitterLayer { - init(with particles: [StarFieldView.Particle]) { - super.init() - - needsDisplayOnBoundsChange = true - emitterCells = particles.map { ParticleCell(with: $0) } - } - - override func layoutSublayers() { - super.layoutSublayers() - - emitterMode = .outline - emitterShape = .sphere - emitterSize = bounds.insetBy(dx: -50, dy: -50).size - emitterPosition = CGPoint(x: bounds.midX, y: bounds.maxY) - speed = 0.5 - } - - override init(layer: Any) { - super.init(layer: layer) - } - - private class ParticleCell: CAEmitterCell { - init(with particle: StarFieldView.Particle) { - super.init() - - let randomAlpha = CGFloat.random(in: 0.3...0.5) - color = particle.tintColor.withAlphaComponent(randomAlpha).cgColor - contents = particle.image.cgImage - - birthRate = 5 - lifetime = Float.infinity - lifetimeRange = 0 - velocity = 5 - velocityRange = velocity * 0.5 - yAcceleration = -0.01 - - scale = WPDeviceIdentification.isiPad() ? 0.07 : 0.04 - scaleRange = 0.05 - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - -class InteractiveStarFieldEmitterLayer: StarFieldEmitterLayer { - override init(with particles: [StarFieldView.Particle]) { - super.init(with: particles) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override init(layer: Any) { - super.init(layer: layer) - } - - override func layoutSublayers() { - super.layoutSublayers() - - emitterShape = .circle - emitterSize = .zero - beginTime = CACurrentMediaTime() - } - - /// Moves the emitter point to the touch location - /// - Parameters: - /// - location: The location to move the emitter point to - /// - radius: The size of the emitter - public func touchesMoved(to location: CGPoint, with radius: CGFloat = 10) { - lifetime = 1 - birthRate = 1 - speed = 10 - - emitterPosition = location - emitterSize = CGSize(width: radius, height: radius) - } - - public func touchesBegan() { - beginTime = CACurrentMediaTime() - } - - public func touchesEnded() { - lifetime = 0 - emitterSize = .zero - } -} diff --git a/WordPress/Jetpack/Classes/Utility/DataMigrator.swift b/WordPress/Jetpack/Classes/Utility/DataMigrator.swift index 4b76fa8f6b9e..6c65c92c6dec 100644 --- a/WordPress/Jetpack/Classes/Utility/DataMigrator.swift +++ b/WordPress/Jetpack/Classes/Utility/DataMigrator.swift @@ -116,7 +116,6 @@ private extension DataMigrator { /// This way we can delete just the value for its key and leave the rest of shared defaults untouched. struct DefaultsWrapper { static let dictKey = "defaults_staging_dictionary" - let defaultsDict: [String: Any] } /// Convenience wrapper to check whether the export data is ready to be imported. diff --git a/WordPress/Jetpack/Classes/ViewRelated/Login Error/JetpackLoginErrorViewController.swift b/WordPress/Jetpack/Classes/ViewRelated/Login Error/JetpackLoginErrorViewController.swift deleted file mode 100644 index a8f23105b6c0..000000000000 --- a/WordPress/Jetpack/Classes/ViewRelated/Login Error/JetpackLoginErrorViewController.swift +++ /dev/null @@ -1,104 +0,0 @@ -import UIKit - -class JetpackLoginErrorViewController: UIViewController { - private let viewModel: JetpackErrorViewModel - - // IBOutlets - @IBOutlet weak var imageView: UIImageView! - @IBOutlet weak var titleLabel: UILabel! - @IBOutlet weak var descriptionLabel: UILabel! - @IBOutlet weak var primaryButton: UIButton! - @IBOutlet weak var secondaryButton: UIButton! - - init(viewModel: JetpackErrorViewModel) { - self.viewModel = viewModel - - super.init(nibName: nil, bundle: nil) - } - - override var supportedInterfaceOrientations: UIInterfaceOrientationMask { - UIDevice.isPad() ? .all : .portrait - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - configureImageView() - configureTitleLabel() - configureDescriptionLabel() - configurePrimaryButton() - configureSecondaryButton() - } - -} - -// MARK: - View Configuration -extension JetpackLoginErrorViewController { - private func configureImageView() { - guard let image = viewModel.image else { - imageView.isHidden = true - imageView.image = nil - - return - } - - imageView.image = image - } - - private func configureTitleLabel() { - guard let title = viewModel.title else { - titleLabel.isHidden = true - return - } - - titleLabel.isHidden = false - titleLabel.text = title - } - - private func configureDescriptionLabel() { - descriptionLabel.font = Self.descriptionFont - descriptionLabel.textColor = Self.descriptionTextColor - descriptionLabel.adjustsFontForContentSizeCategory = true - - guard let attributedString = viewModel.description.attributedStringValue else { - descriptionLabel.text = viewModel.description.stringValue - return - } - - descriptionLabel.attributedText = attributedString - } - - private func configurePrimaryButton() { - guard let title = viewModel.primaryButtonTitle else { - primaryButton.isHidden = true - return - } - - primaryButton.setTitle(title, for: .normal) - primaryButton.on(.touchUpInside) { [weak self] _ in - self?.viewModel.didTapPrimaryButton(in: self) - } - } - - private func configureSecondaryButton() { - guard let title = viewModel.secondaryButtonTitle else { - secondaryButton.isHidden = true - return - } - - secondaryButton.setTitle(title, for: .normal) - secondaryButton.on(.touchUpInside) { [weak self] _ in - self?.viewModel.didTapSecondaryButton(in: self) - } - } -} - -// MARK: - Styles -extension JetpackLoginErrorViewController { - static let descriptionFont: UIFont = WPStyleGuide.fontForTextStyle(.body) - static let descriptionTextColor: UIColor = .text -} diff --git a/WordPress/Jetpack/Classes/ViewRelated/Login Error/JetpackLoginErrorViewController.xib b/WordPress/Jetpack/Classes/ViewRelated/Login Error/JetpackLoginErrorViewController.xib deleted file mode 100644 index d94e68dc65e4..000000000000 --- a/WordPress/Jetpack/Classes/ViewRelated/Login Error/JetpackLoginErrorViewController.xib +++ /dev/null @@ -1,103 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/WordPress/Jetpack/Classes/ViewRelated/Login Error/View Models/JetpackErrorViewModel.swift b/WordPress/Jetpack/Classes/ViewRelated/Login Error/View Models/JetpackErrorViewModel.swift deleted file mode 100644 index 0fc48005c4dc..000000000000 --- a/WordPress/Jetpack/Classes/ViewRelated/Login Error/View Models/JetpackErrorViewModel.swift +++ /dev/null @@ -1,46 +0,0 @@ -import Foundation - -protocol JetpackErrorViewModel { - /// The primary icon image - /// If this is nil the image will be hidden - var image: UIImage? { get } - - /// The title for the error description - /// If nil, title will be hidden - var title: String? { get } - - /// The error description text - var description: FormattedStringProvider { get } - - /// The title for the first button - /// If this is nil the button will be hidden - var primaryButtonTitle: String? { get } - - /// The title for the second button - /// If this is nil the button will be hidden - var secondaryButtonTitle: String? { get } - - /// Executes action associated to a tap in the view controller primary button - /// - Parameter viewController: usually the view controller sending the tap - func didTapPrimaryButton(in viewController: UIViewController?) - - /// Executes action associated to a tap in the view controller secondary button - /// - Parameter viewController: usually the view controller sending the tap - func didTapSecondaryButton(in viewController: UIViewController?) -} - -/// Helper struct to define a type as both a regular string and an attributed one -struct FormattedStringProvider { - let stringValue: String - let attributedStringValue: NSAttributedString? - - init(string: String) { - stringValue = string - attributedStringValue = nil - } - - init(attributedString: NSAttributedString) { - attributedStringValue = attributedString - stringValue = attributedString.string - } -} diff --git a/WordPress/Jetpack/Classes/ViewRelated/Login Error/View Models/JetpackNoSitesErrorViewModel.swift b/WordPress/Jetpack/Classes/ViewRelated/Login Error/View Models/JetpackNoSitesErrorViewModel.swift deleted file mode 100644 index da3485dd61be..000000000000 --- a/WordPress/Jetpack/Classes/ViewRelated/Login Error/View Models/JetpackNoSitesErrorViewModel.swift +++ /dev/null @@ -1,43 +0,0 @@ -import Foundation - -struct JetpackNoSitesErrorViewModel: JetpackErrorViewModel { - let image: UIImage? = UIImage(named: "jetpack-empty-state-illustration") - var title: String? = Constants.title - var description: FormattedStringProvider = FormattedStringProvider(string: Constants.description) - var primaryButtonTitle: String? = Constants.primaryButtonTitle - var secondaryButtonTitle: String? = Constants.secondaryButtonTitle - - func didTapPrimaryButton(in viewController: UIViewController?) { - guard let url = URL(string: Constants.helpURLString) else { - return - } - - let controller = WebViewControllerFactory.controller(url: url, source: "jetpack_no_sites") - let navController = UINavigationController(rootViewController: controller) - - viewController?.present(navController, animated: true) - } - - func didTapSecondaryButton(in viewController: UIViewController?) { - AccountHelper.logOutDefaultWordPressComAccount() - } - - private struct Constants { - static let title = NSLocalizedString("No Jetpack sites found", - comment: "Title when users have no Jetpack sites.") - - static let description = NSLocalizedString("If you already have a site, you’ll need to install the free Jetpack plugin and connect it to your WordPress.com account.", - comment: "Message explaining that they will need to install Jetpack on one of their sites.") - - - static let primaryButtonTitle = NSLocalizedString("See Instructions", - comment: "Action button linking to instructions for installing Jetpack." - + "Presented when logging in with a site address that does not have a valid Jetpack installation") - - static let secondaryButtonTitle = NSLocalizedString("Try With Another Account", - comment: "Action button that will restart the login flow." - + "Presented when logging in with a site address that does not have a valid Jetpack installation") - - static let helpURLString = "https://jetpack.com/support/getting-started-with-jetpack/" - } -} diff --git a/WordPress/Jetpack/Classes/ViewRelated/Login Error/View Models/JetpackNotFoundErrorViewModel.swift b/WordPress/Jetpack/Classes/ViewRelated/Login Error/View Models/JetpackNotFoundErrorViewModel.swift deleted file mode 100644 index 5aac59c8d6c4..000000000000 --- a/WordPress/Jetpack/Classes/ViewRelated/Login Error/View Models/JetpackNotFoundErrorViewModel.swift +++ /dev/null @@ -1,73 +0,0 @@ -import UIKit - -struct JetpackNotFoundErrorViewModel: JetpackErrorViewModel { - let title: String? = nil - let image: UIImage? = UIImage(named: "jetpack-empty-state-illustration") - var description: FormattedStringProvider { - let siteName = siteURL - let description = String(format: Constants.description, siteName) - let font: UIFont = JetpackLoginErrorViewController.descriptionFont.semibold() - - let attributedString = NSMutableAttributedString(string: description) - attributedString.applyStylesToMatchesWithPattern(siteName, styles: [.font: font]) - - return FormattedStringProvider(attributedString: attributedString) - } - - var primaryButtonTitle: String? = Constants.primaryButtonTitle - var secondaryButtonTitle: String? = Constants.secondaryButtonTitle - - private let siteURL: String - - init(with siteURL: String?) { - self.siteURL = siteURL?.trimURLScheme() ?? Constants.yourSite - } - - func didTapPrimaryButton(in viewController: UIViewController?) { - guard let url = URL(string: Constants.helpURLString) else { - return - } - - let controller = WebViewControllerFactory.controller(url: url, source: "jetpack_not_found") - let navController = UINavigationController(rootViewController: controller) - - viewController?.present(navController, animated: true) - } - - func didTapSecondaryButton(in viewController: UIViewController?) { - viewController?.navigationController?.popToRootViewController(animated: true) - } - - private struct Constants { - static let yourSite = NSLocalizedString("your site", - comment: "Placeholder for site url, if the url is unknown." - + "Presented when logging in with a site address that does not have a valid Jetpack installation." - + "The error would read: to use this app for your site you'll need...") - - static let description = NSLocalizedString("To use this app for %@ you'll need to have the Jetpack plugin installed and activated.", - comment: "Message explaining that Jetpack needs to be installed for a particular site. " - + "Reads like 'To use this app for example.com you'll need to have...") - - static let primaryButtonTitle = NSLocalizedString("See Instructions", - comment: "Action button linking to instructions for installing Jetpack." - + "Presented when logging in with a site address that does not have a valid Jetpack installation") - - static let secondaryButtonTitle = NSLocalizedString("Try With Another Account", - comment: "Action button that will restart the login flow." - + "Presented when logging in with a site address that does not have a valid Jetpack installation") - - static let helpURLString = "https://jetpack.com/support/getting-started-with-jetpack/" - } -} - -private extension String { - // Removes http:// or https:// - func trimURLScheme() -> String? { - guard let urlComponents = URLComponents(string: self), - let host = urlComponents.host else { - return self - } - - return host + urlComponents.path - } -} diff --git a/WordPress/Jetpack/Classes/ViewRelated/Login Error/View Models/JetpackNotWPErrorViewModel.swift b/WordPress/Jetpack/Classes/ViewRelated/Login Error/View Models/JetpackNotWPErrorViewModel.swift deleted file mode 100644 index 9f1128a788c6..000000000000 --- a/WordPress/Jetpack/Classes/ViewRelated/Login Error/View Models/JetpackNotWPErrorViewModel.swift +++ /dev/null @@ -1,27 +0,0 @@ -import Foundation - -struct JetpackNotWPErrorViewModel: JetpackErrorViewModel { - let title: String? = nil - let image: UIImage? = UIImage(named: "jetpack-empty-state-illustration") - var description: FormattedStringProvider = FormattedStringProvider(string: Constants.description) - var primaryButtonTitle: String? = Constants.primaryButtonTitle - var secondaryButtonTitle: String? = nil - - func didTapPrimaryButton(in viewController: UIViewController?) { - viewController?.navigationController?.popToRootViewController(animated: true) - } - - func didTapSecondaryButton(in viewController: UIViewController?) { } - - private struct Constants { - static let description = NSLocalizedString("We were not able to detect a WordPress site at the address you entered." - + " Please make sure WordPress is installed and that you are running" - + " the latest available version.", - comment: "Message explaining that WordPress was not detected.") - - - static let primaryButtonTitle = NSLocalizedString("Try With Another Account", - comment: "Action button that will restart the login flow." - + "Presented when logging in with a site address that does not have a valid Jetpack installation") - } -} diff --git a/WordPress/Jetpack/Classes/ViewRelated/WordPress-to-Jetpack Migration/All done/MigrationDoneViewController.swift b/WordPress/Jetpack/Classes/ViewRelated/WordPress-to-Jetpack Migration/All done/MigrationDoneViewController.swift index ce01339f36a4..6524c97d05bb 100644 --- a/WordPress/Jetpack/Classes/ViewRelated/WordPress-to-Jetpack Migration/All done/MigrationDoneViewController.swift +++ b/WordPress/Jetpack/Classes/ViewRelated/WordPress-to-Jetpack Migration/All done/MigrationDoneViewController.swift @@ -19,8 +19,7 @@ class MigrationDoneViewController: UIViewController { override func loadView() { self.view = MigrationStepView( headerView: MigrationHeaderView(configuration: viewModel.configuration.headerConfiguration), - actionsView: MigrationActionsView(configuration: viewModel.configuration.actionsConfiguration), - centerView: MigrationCenterView.deleteWordPress(with: viewModel.configuration.centerViewConfiguration) + actionsView: MigrationActionsView(configuration: viewModel.configuration.actionsConfiguration) ) } @@ -36,6 +35,11 @@ class MigrationDoneViewController: UIViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - self.tracker.track(.thanksScreenShown) + + var properties: [String: String] = [:] + if BlogListDataSource().visibleBlogsCount == 0 { + properties["no_sites"] = "true" + } + tracker.track(.thanksScreenShown, properties: properties) } } diff --git a/WordPress/Jetpack/Classes/ViewRelated/WordPress-to-Jetpack Migration/Common/Analytics/MigrationAnalyticsTracker.swift b/WordPress/Jetpack/Classes/ViewRelated/WordPress-to-Jetpack Migration/Common/Analytics/MigrationAnalyticsTracker.swift index 1188f0d3cd95..fe3d05a1ff81 100644 --- a/WordPress/Jetpack/Classes/ViewRelated/WordPress-to-Jetpack Migration/Common/Analytics/MigrationAnalyticsTracker.swift +++ b/WordPress/Jetpack/Classes/ViewRelated/WordPress-to-Jetpack Migration/Common/Analytics/MigrationAnalyticsTracker.swift @@ -1,7 +1,6 @@ import Foundation struct MigrationAnalyticsTracker { - // MARK: - Track Method func track(_ event: MigrationEvent, properties: Properties = [:]) { @@ -16,12 +15,19 @@ struct MigrationAnalyticsTracker { self.track(.contentExportEligibility, properties: properties) } - func trackContentExportSucceeded() { - self.track(.contentExportSucceeded) + func trackContentExportSucceeded(hasBlogs: Bool) { + var properties: [String: String] = [:] + if !hasBlogs { + properties["no_sites"] = "true" + } + self.track(.contentExportSucceeded, properties: properties) } - func trackContentExportFailed(reason: String) { - let properties = ["error_type": reason] + func trackContentExportFailed(reason: String, hasBlogs: Bool) { + var properties = ["error_type": reason] + if !hasBlogs { + properties["no_sites"] = "true" + } self.track(.contentExportFailed, properties: properties) } diff --git a/WordPress/Jetpack/Classes/ViewRelated/WordPress-to-Jetpack Migration/Common/MigrationDependencyContainer.swift b/WordPress/Jetpack/Classes/ViewRelated/WordPress-to-Jetpack Migration/Common/MigrationDependencyContainer.swift index e5715b52ce2a..ac3484b99141 100644 --- a/WordPress/Jetpack/Classes/ViewRelated/WordPress-to-Jetpack Migration/Common/MigrationDependencyContainer.swift +++ b/WordPress/Jetpack/Classes/ViewRelated/WordPress-to-Jetpack Migration/Common/MigrationDependencyContainer.swift @@ -34,7 +34,6 @@ struct MigrationViewControllerFactory { } private func makeAccount() -> WPAccount? { - let context = ContextManager.shared.mainContext do { return try WPAccount.lookupDefaultWordPressComAccount(in: context) diff --git a/WordPress/Jetpack/Classes/ViewRelated/WordPress-to-Jetpack Migration/Common/Navigation/MigrationFlowCoordinator.swift b/WordPress/Jetpack/Classes/ViewRelated/WordPress-to-Jetpack Migration/Common/Navigation/MigrationFlowCoordinator.swift index 9a9749c5e9b5..4db263933f0c 100644 --- a/WordPress/Jetpack/Classes/ViewRelated/WordPress-to-Jetpack Migration/Common/Navigation/MigrationFlowCoordinator.swift +++ b/WordPress/Jetpack/Classes/ViewRelated/WordPress-to-Jetpack Migration/Common/Navigation/MigrationFlowCoordinator.swift @@ -23,6 +23,13 @@ final class MigrationFlowCoordinator: ObservableObject { self.migrationEmailService = migrationEmailService self.userPersistentRepository = userPersistentRepository self.userPersistentRepository.jetpackContentMigrationState = .inProgress + + // Skip the migration if the user just created an account and haven't + // created any site yet. + if BlogListDataSource().visibleBlogsCount == 0 { + self.currentStep = MigrationStep.done + self.userPersistentRepository.jetpackContentMigrationState = .completed + } } deinit { diff --git a/WordPress/Jetpack/Classes/ViewRelated/WordPress-to-Jetpack Migration/Common/Views/Configuration/MigrationActionsConfiguration.swift b/WordPress/Jetpack/Classes/ViewRelated/WordPress-to-Jetpack Migration/Common/Views/Configuration/MigrationActionsConfiguration.swift index 3eea6a401bac..63b4684f6700 100644 --- a/WordPress/Jetpack/Classes/ViewRelated/WordPress-to-Jetpack Migration/Common/Views/Configuration/MigrationActionsConfiguration.swift +++ b/WordPress/Jetpack/Classes/ViewRelated/WordPress-to-Jetpack Migration/Common/Views/Configuration/MigrationActionsConfiguration.swift @@ -46,8 +46,8 @@ private extension MigrationActionsViewConfiguration { value: "Continue", comment: "The primary button title in the migration welcome and notifications screens.") - static let donePrimaryTitle = NSLocalizedString("migration.done.actions.primary.title", - value: "Finish", + static let donePrimaryTitle = NSLocalizedString("migrationDone.actions.primaryTitle", + value: "Let's go", comment: "Primary button title in the migration done screen.") static let welcomeSecondaryTitle = NSLocalizedString("Need help?", diff --git a/WordPress/Jetpack/Classes/ViewRelated/WordPress-to-Jetpack Migration/Common/Views/Configuration/MigrationHeaderConfiguration.swift b/WordPress/Jetpack/Classes/ViewRelated/WordPress-to-Jetpack Migration/Common/Views/Configuration/MigrationHeaderConfiguration.swift index 380e9c84972b..e1f9abf9f1ef 100644 --- a/WordPress/Jetpack/Classes/ViewRelated/WordPress-to-Jetpack Migration/Common/Views/Configuration/MigrationHeaderConfiguration.swift +++ b/WordPress/Jetpack/Classes/ViewRelated/WordPress-to-Jetpack Migration/Common/Views/Configuration/MigrationHeaderConfiguration.swift @@ -52,7 +52,7 @@ private extension MigrationHeaderConfiguration { case .notifications: return notificationsPrimaryDescription case .done: - return donePrimaryDescription + return donePrimaryDescription + "\n\n" + doneSecondaryDescription case .dismiss: return nil } @@ -95,6 +95,8 @@ private extension MigrationHeaderConfiguration { value: "We’ve transferred all your data and settings. Everything is right where you left it.", comment: "Primary description in the migration done screen.") + static let doneSecondaryDescription = NSLocalizedString("migration.done.secondaryDescription", value: "It's time to continue your WordPress journey on the Jetpack app!", comment: "Secondary description (second paragraph) in the migration done screen.") + static let notificationsSecondaryDescription = NSLocalizedString("migration.notifications.secondaryDescription", value: "We’ll disable notifications for the WordPress app.", comment: "Secondary description in the migration notifications screen") diff --git a/WordPress/Jetpack/Classes/ViewRelated/WordPress-to-Jetpack Migration/Common/Views/MigrationStepView.swift b/WordPress/Jetpack/Classes/ViewRelated/WordPress-to-Jetpack Migration/Common/Views/MigrationStepView.swift index fb5fc4876dab..b2a75b9cb52b 100644 --- a/WordPress/Jetpack/Classes/ViewRelated/WordPress-to-Jetpack Migration/Common/Views/MigrationStepView.swift +++ b/WordPress/Jetpack/Classes/ViewRelated/WordPress-to-Jetpack Migration/Common/Views/MigrationStepView.swift @@ -14,11 +14,11 @@ class MigrationStepView: UIView { // MARK: - Views private let headerView: MigrationHeaderView - private let centerView: UIView + private let centerView: UIView? private let actionsView: MigrationActionsView private lazy var mainStackView: UIStackView = { - let stackView = UIStackView(arrangedSubviews: [headerView, centerView, UIView()]) + let stackView = UIStackView(arrangedSubviews: [headerView, centerView, UIView()].compactMap { $0 }) stackView.axis = .vertical stackView.distribution = .equalSpacing stackView.directionalLayoutMargins = Constants.mainStackViewMargins @@ -50,10 +50,9 @@ class MigrationStepView: UIView { init(headerView: MigrationHeaderView, actionsView: MigrationActionsView, - centerView: UIView) { + centerView: UIView? = nil) { self.headerView = headerView self.centerView = centerView - centerView.translatesAutoresizingMaskIntoConstraints = false self.actionsView = actionsView headerView.directionalLayoutMargins = .zero actionsView.translatesAutoresizingMaskIntoConstraints = false @@ -141,6 +140,6 @@ class MigrationStepView: UIView { static let stackViewSpacing: CGFloat = 20 // Adds margins to the main sack view. - static let mainStackViewMargins = NSDirectionalEdgeInsets(top: 0, leading: 30, bottom: 0, trailing: 30) + static let mainStackViewMargins = NSDirectionalEdgeInsets(top: 0, leading: 24, bottom: 0, trailing: 24) } } diff --git a/WordPress/Jetpack/JetpackDebug.entitlements b/WordPress/Jetpack/JetpackDebug.entitlements index 5253760134d2..7300f805fe0e 100644 --- a/WordPress/Jetpack/JetpackDebug.entitlements +++ b/WordPress/Jetpack/JetpackDebug.entitlements @@ -12,11 +12,12 @@ applinks:jetpack.com webcredentials:wordpress.com + webcredentials:*.wordpress.com applinks:apps.wordpress.com applinks:wordpress.com + applinks:*.wordpress.com applinks:public-api.wordpress.com applinks:links.wp.a8cmail.com - applinks:*.wordpress.com com.apple.security.application-groups diff --git a/WordPress/Jetpack/JetpackRelease-Alpha.entitlements b/WordPress/Jetpack/JetpackRelease-Alpha.entitlements index 56b32f593884..efdce8fda2ba 100644 --- a/WordPress/Jetpack/JetpackRelease-Alpha.entitlements +++ b/WordPress/Jetpack/JetpackRelease-Alpha.entitlements @@ -8,11 +8,12 @@ applinks:jetpack.com webcredentials:wordpress.com + webcredentials:*.wordpress.com applinks:apps.wordpress.com applinks:wordpress.com + applinks:*.wordpress.com applinks:public-api.wordpress.com applinks:links.wp.a8cmail.com - applinks:*.wordpress.com com.apple.security.application-groups diff --git a/WordPress/Jetpack/JetpackRelease-Internal.entitlements b/WordPress/Jetpack/JetpackRelease-Internal.entitlements index 87e951fee79e..2df878e17e81 100644 --- a/WordPress/Jetpack/JetpackRelease-Internal.entitlements +++ b/WordPress/Jetpack/JetpackRelease-Internal.entitlements @@ -8,11 +8,12 @@ applinks:jetpack.com webcredentials:wordpress.com + webcredentials:*.wordpress.com applinks:apps.wordpress.com applinks:wordpress.com + applinks:*.wordpress.com applinks:public-api.wordpress.com applinks:links.wp.a8cmail.com - applinks:*.wordpress.com com.apple.security.application-groups diff --git a/WordPress/Jetpack/JetpackRelease.entitlements b/WordPress/Jetpack/JetpackRelease.entitlements index 5253760134d2..7300f805fe0e 100644 --- a/WordPress/Jetpack/JetpackRelease.entitlements +++ b/WordPress/Jetpack/JetpackRelease.entitlements @@ -12,11 +12,12 @@ applinks:jetpack.com webcredentials:wordpress.com + webcredentials:*.wordpress.com applinks:apps.wordpress.com applinks:wordpress.com + applinks:*.wordpress.com applinks:public-api.wordpress.com applinks:links.wp.a8cmail.com - applinks:*.wordpress.com com.apple.security.application-groups diff --git a/WordPress/Jetpack/Launch Screen.storyboard b/WordPress/Jetpack/Launch Screen.storyboard index 0b15765afd02..07c45d7eb96d 100644 --- a/WordPress/Jetpack/Launch Screen.storyboard +++ b/WordPress/Jetpack/Launch Screen.storyboard @@ -1,9 +1,9 @@ - + - + @@ -17,10 +17,10 @@ - - + + - + @@ -39,7 +39,7 @@ - + diff --git a/WordPress/Jetpack/Resources/AppStoreStrings.po b/WordPress/Jetpack/Resources/AppStoreStrings.po index 5f8459d47ec3..8cdd84a93236 100644 --- a/WordPress/Jetpack/Resources/AppStoreStrings.po +++ b/WordPress/Jetpack/Resources/AppStoreStrings.po @@ -81,13 +81,13 @@ msgctxt "app_store_keywords" msgid "social,notes,jetpack,writing,geotagging,media,blog,website,blogging,journal" msgstr "" -msgctxt "v23.5-whats-new" +msgctxt "v23.9-whats-new" msgid "" -"We’ve made some visual changes to the reader. You’ll notice updates to feed cards, headers, buttons, recommendations, and more. Please feel free to give us lots of compliments.\n" +"We updated the classic editor with new media pickers for Photos and Site Media. Don’t worry, you can still upload images, videos, and more to your site.\n" "\n" -"In the block editor, you can now split or exit a formatted block by pressing the “enter” key three times. The left-hand border is always visible for quote blocks, too, even in block-based themes on dark mode. And you can quote us on that.\n" +"Speaking of media types—you can now add media filters to the Site Media screen. If you’re using an iPhone, you’ll notice the new aspect ratio mode, too. Both options are available when you tap the title menu.\n" "\n" -"Finally, we fixed a code issue in blogging prompt settings that caused the app to crash.\n" +"Finally, we fixed the broken compliance pop-up that appears while you’re checking stats during the onboarding process. We also fixed a rare crash that happened while logging out. Sweet.\n" msgstr "" #. translators: This is a promo message that will be attached on top of the first screenshot in the App Store. diff --git a/WordPress/Jetpack/Resources/release_notes.txt b/WordPress/Jetpack/Resources/release_notes.txt index 48ac0fbfbbf8..c2c9251550cf 100644 --- a/WordPress/Jetpack/Resources/release_notes.txt +++ b/WordPress/Jetpack/Resources/release_notes.txt @@ -1,5 +1,18 @@ -We’ve made some visual changes to the reader. You’ll notice updates to feed cards, headers, buttons, recommendations, and more. Please feel free to give us lots of compliments. +* [**] [internal] A minor refactor in authentication flow, including but not limited to social sign-in and two factor authentication. [#22086] +* [***] Plans: Upgrade to a WPCOM plan from domains dashboard in Jetpack app. [#22261] +* [**] [internal] Refactor domain selection flows to use the same domain selection UI. [22254] +* [**] Re-enable the support for using Security Keys as a second factor during login [#22258] +* [*] Fix crash in editor that sometimes happens after modifying tags or categories [#22265] +* [**] Updated login screen's colors to highlight WordPress - Jetpack brand relationship +* [*] Add defensive code to make sure the retain cycles in the editor don't lead to crashes [#22252] +* [*] Updated Site Domains screen to make domains management more convenient [#22294, #22311] +* [**] [internal] Adds support for dynamic dashboard cards driven by the backend [#22326] +* [**] [internal] Add support for the Phase One Fast Media Uploads banner [#22330] +* [*] [internal] Remove personalizeHomeTab feature flag [#22280] +* [*] Fix a rare crash in post search related to tags [#22275] +* [*] Fix a rare crash when deleting posts [#22277] +* [*] Fix a rare crash in Site Media prefetching cancellation [#22278] +* [*] Fix an issue with BlogDashboardPersonalizationService being used on the background thread [#22335] +* [***] Block Editor: Avoid keyboard dismiss when interacting with text blocks [https://github.com/WordPress/gutenberg/pull/57070] +* [**] Block Editor: Auto-scroll upon block insertion [https://github.com/WordPress/gutenberg/pull/57273] -In the block editor, you can now split or exit a formatted block by pressing the “enter” key three times. The left-hand border is always visible for quote blocks, too, even in block-based themes on dark mode. And you can quote us on that. - -Finally, we fixed a code issue in blogging prompt settings that caused the app to crash. diff --git a/WordPress/Jetpack/UIColor+JetpackColors.swift b/WordPress/Jetpack/UIColor+JetpackColors.swift index 861a012f9ae6..e5a95ec43dd3 100644 --- a/WordPress/Jetpack/UIColor+JetpackColors.swift +++ b/WordPress/Jetpack/UIColor+JetpackColors.swift @@ -25,7 +25,7 @@ extension UIColor { } static var filterBarSelected: UIColor { - return .primary + return .text } static var filterBarSelectedText: UIColor { diff --git a/WordPress/JetpackStatsWidgets/Assets.xcassets/icon-jetpack.imageset/Contents.json b/WordPress/JetpackStatsWidgets/Assets.xcassets/icon-jetpack.imageset/Contents.json new file mode 100644 index 000000000000..7299a18f5799 --- /dev/null +++ b/WordPress/JetpackStatsWidgets/Assets.xcassets/icon-jetpack.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "icon-jetpack.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "icon-jetpack@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "icon-jetpack@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WordPress/JetpackStatsWidgets/Assets.xcassets/icon-jetpack.imageset/icon-jetpack.png b/WordPress/JetpackStatsWidgets/Assets.xcassets/icon-jetpack.imageset/icon-jetpack.png new file mode 100644 index 000000000000..43e302148456 Binary files /dev/null and b/WordPress/JetpackStatsWidgets/Assets.xcassets/icon-jetpack.imageset/icon-jetpack.png differ diff --git a/WordPress/JetpackStatsWidgets/Assets.xcassets/icon-jetpack.imageset/icon-jetpack@2x.png b/WordPress/JetpackStatsWidgets/Assets.xcassets/icon-jetpack.imageset/icon-jetpack@2x.png new file mode 100644 index 000000000000..7431052b24b9 Binary files /dev/null and b/WordPress/JetpackStatsWidgets/Assets.xcassets/icon-jetpack.imageset/icon-jetpack@2x.png differ diff --git a/WordPress/JetpackStatsWidgets/Assets.xcassets/icon-jetpack.imageset/icon-jetpack@3x.png b/WordPress/JetpackStatsWidgets/Assets.xcassets/icon-jetpack.imageset/icon-jetpack@3x.png new file mode 100644 index 000000000000..2975678a8d59 Binary files /dev/null and b/WordPress/JetpackStatsWidgets/Assets.xcassets/icon-jetpack.imageset/icon-jetpack@3x.png differ diff --git a/WordPress/JetpackStatsWidgets/Cache/HomeWidgetCache.swift b/WordPress/JetpackStatsWidgets/Cache/HomeWidgetCache.swift index b8ca43b88446..dd654cc156d9 100644 --- a/WordPress/JetpackStatsWidgets/Cache/HomeWidgetCache.swift +++ b/WordPress/JetpackStatsWidgets/Cache/HomeWidgetCache.swift @@ -1,4 +1,5 @@ import Foundation +import JetpackStatsWidgetsCore /// Cache manager that stores `HomeWidgetData` values in a plist file, contained in the specified security application group and with the specified file name. /// The corresponding dictionary is always in the form `[Int: T]`, where the `Int` key is the SiteID, and the `T` value is any `HomeWidgetData` instance. diff --git a/WordPress/JetpackStatsWidgets/Helpers/HomeWidgetDataFileReader.swift b/WordPress/JetpackStatsWidgets/Helpers/HomeWidgetDataFileReader.swift index aa223220fa80..469257575ff6 100644 --- a/WordPress/JetpackStatsWidgets/Helpers/HomeWidgetDataFileReader.swift +++ b/WordPress/JetpackStatsWidgets/Helpers/HomeWidgetDataFileReader.swift @@ -1,4 +1,4 @@ -import Foundation +import JetpackStatsWidgetsCore final class HomeWidgetDataFileReader: WidgetDataCacheReader { func widgetData(for siteID: String) -> T? { diff --git a/WordPress/JetpackStatsWidgets/Helpers/WidgetDataReader.swift b/WordPress/JetpackStatsWidgets/Helpers/WidgetDataReader.swift index 327c5b36fe3e..807aa2579a1f 100644 --- a/WordPress/JetpackStatsWidgets/Helpers/WidgetDataReader.swift +++ b/WordPress/JetpackStatsWidgets/Helpers/WidgetDataReader.swift @@ -1,16 +1,5 @@ import Foundation - -protocol WidgetDataCacheReader { - func widgetData(for siteID: String) -> T? - func widgetData() -> [T]? -} - -enum WidgetDataReadError: Error { - case jetpackFeatureDisabled - case noData - case noSite - case loggedOut -} +import JetpackStatsWidgetsCore final class WidgetDataReader { let userDefaults: UserDefaults? @@ -39,25 +28,10 @@ final class WidgetDataReader { return .failure(.noData) } - if let selectedSite = configuration.site?.identifier, - let widgetData: T = cacheReader.widgetData(for: selectedSite) { - return .success(widgetData) - } else if let defaultSiteID = defaultSiteID, - let widgetData: T = cacheReader.widgetData(for: String(defaultSiteID)) { - return .success(widgetData) - } else { - let loggedIn = defaults.bool(forKey: AppConfiguration.Widget.Stats.userDefaultsLoggedInKey) - - if loggedIn { - /// In rare cases there could be no default site and no defaultSiteId set - if let firstSiteData: T = cacheReader.widgetData()?.sorted(by: { $0.siteID < $1.siteID }).first { - return .success(firstSiteData) - } else { - return .failure(.noSite) - } - } else { - return .failure(.loggedOut) - } - } + return cacheReader.widgetData( + forSiteIdentifier: configuration.site?.identifier, + defaultSiteID: defaultSiteID, + userLoggedIn: defaults.bool(forKey: AppConfiguration.Widget.Stats.userDefaultsLoggedInKey) + ) } } diff --git a/WordPress/JetpackStatsWidgets/LockScreenWidgets/Configs/LockScreenAllTimePostsBestViewsStatWidgetConfig.swift b/WordPress/JetpackStatsWidgets/LockScreenWidgets/Configs/LockScreenAllTimePostsBestViewsStatWidgetConfig.swift index 2bdbd3d14a98..b0e267970de5 100644 --- a/WordPress/JetpackStatsWidgets/LockScreenWidgets/Configs/LockScreenAllTimePostsBestViewsStatWidgetConfig.swift +++ b/WordPress/JetpackStatsWidgets/LockScreenWidgets/Configs/LockScreenAllTimePostsBestViewsStatWidgetConfig.swift @@ -1,4 +1,5 @@ import WidgetKit +import JetpackStatsWidgetsCore @available(iOS 16.0, *) struct LockScreenAllTimePostsBestViewsStatWidgetConfig: LockScreenStatsWidgetConfig { diff --git a/WordPress/JetpackStatsWidgets/LockScreenWidgets/Configs/LockScreenStatsWidgetConfig.swift b/WordPress/JetpackStatsWidgets/LockScreenWidgets/Configs/LockScreenStatsWidgetConfig.swift index 013f7488f891..3eb97b260f9f 100644 --- a/WordPress/JetpackStatsWidgets/LockScreenWidgets/Configs/LockScreenStatsWidgetConfig.swift +++ b/WordPress/JetpackStatsWidgets/LockScreenWidgets/Configs/LockScreenStatsWidgetConfig.swift @@ -1,4 +1,5 @@ import WidgetKit +import JetpackStatsWidgetsCore protocol LockScreenStatsWidgetConfig { associatedtype WidgetData: HomeWidgetData diff --git a/WordPress/JetpackStatsWidgets/LockScreenWidgets/Configs/LockScreenTodayLikesCommentsStatWidgetConfig.swift b/WordPress/JetpackStatsWidgets/LockScreenWidgets/Configs/LockScreenTodayLikesCommentsStatWidgetConfig.swift index e4acbe2cc405..c19e19aeea3f 100644 --- a/WordPress/JetpackStatsWidgets/LockScreenWidgets/Configs/LockScreenTodayLikesCommentsStatWidgetConfig.swift +++ b/WordPress/JetpackStatsWidgets/LockScreenWidgets/Configs/LockScreenTodayLikesCommentsStatWidgetConfig.swift @@ -1,4 +1,5 @@ import WidgetKit +import JetpackStatsWidgetsCore @available(iOS 16.0, *) struct LockScreenTodayLikesCommentsStatWidgetConfig: LockScreenStatsWidgetConfig { diff --git a/WordPress/JetpackStatsWidgets/LockScreenWidgets/LockScreenSiteListProvider.swift b/WordPress/JetpackStatsWidgets/LockScreenWidgets/LockScreenSiteListProvider.swift index 7e7803487681..864c629f6c7a 100644 --- a/WordPress/JetpackStatsWidgets/LockScreenWidgets/LockScreenSiteListProvider.swift +++ b/WordPress/JetpackStatsWidgets/LockScreenWidgets/LockScreenSiteListProvider.swift @@ -1,5 +1,6 @@ import WidgetKit import SwiftUI +import JetpackStatsWidgetsCore struct LockScreenSiteListProvider: IntentTimelineProvider { let service: StatsWidgetsService diff --git a/WordPress/JetpackStatsWidgets/LockScreenWidgets/LockScreenStatsWidget.swift b/WordPress/JetpackStatsWidgets/LockScreenWidgets/LockScreenStatsWidget.swift index 6eac12ea4154..be5246d1dfe2 100644 --- a/WordPress/JetpackStatsWidgets/LockScreenWidgets/LockScreenStatsWidget.swift +++ b/WordPress/JetpackStatsWidgets/LockScreenWidgets/LockScreenStatsWidget.swift @@ -1,5 +1,6 @@ import WidgetKit import SwiftUI +import JetpackStatsWidgetsCore @available(iOS 16.0, *) struct LockScreenStatsWidget: Widget { @@ -24,10 +25,6 @@ struct LockScreenStatsWidget: Widget { placeholderContent: config.placeholderContent ) ) { (entry: LockScreenStatsWidgetEntry) -> LockScreenStatsWidgetsView in - defer { - tracks.trackWidgetUpdatedIfNeeded(entry: entry, - widgetKind: config.kind) - } return LockScreenStatsWidgetsView( timelineEntry: entry, viewProvider: config.viewProvider @@ -36,5 +33,6 @@ struct LockScreenStatsWidget: Widget { .configurationDisplayName(config.displayName) .description(config.description) .supportedFamilies(config.supportFamilies) + .iOS17ContentMarginsDisabled() /// Temporarily disable additional iOS17 margins for widgets } } diff --git a/WordPress/JetpackStatsWidgets/LockScreenWidgets/Models/LockScreenStatsWidgetEntry.swift b/WordPress/JetpackStatsWidgets/LockScreenWidgets/Models/LockScreenStatsWidgetEntry.swift index 960c2e5e91bb..d29182c1ed6e 100644 --- a/WordPress/JetpackStatsWidgets/LockScreenWidgets/Models/LockScreenStatsWidgetEntry.swift +++ b/WordPress/JetpackStatsWidgets/LockScreenWidgets/Models/LockScreenStatsWidgetEntry.swift @@ -1,4 +1,5 @@ import WidgetKit +import JetpackStatsWidgetsCore enum LockScreenStatsWidgetEntry: TimelineEntry { case siteSelected(Data, TimelineProviderContext) diff --git a/WordPress/JetpackStatsWidgets/LockScreenWidgets/ViewProvider/LockScreenMultiStatWidgetViewProvider.swift b/WordPress/JetpackStatsWidgets/LockScreenWidgets/ViewProvider/LockScreenMultiStatWidgetViewProvider.swift index 84cfdf56fb91..a4a9bcc5f88d 100644 --- a/WordPress/JetpackStatsWidgets/LockScreenWidgets/ViewProvider/LockScreenMultiStatWidgetViewProvider.swift +++ b/WordPress/JetpackStatsWidgets/LockScreenWidgets/ViewProvider/LockScreenMultiStatWidgetViewProvider.swift @@ -1,4 +1,5 @@ import SwiftUI +import JetpackStatsWidgetsCore @available(iOS 16.0, *) struct LockScreenMultiStatWidgetViewProvider: LockScreenStatsWidgetsViewProvider { diff --git a/WordPress/JetpackStatsWidgets/LockScreenWidgets/ViewProvider/LockScreenSingleStatWidgetViewProvider.swift b/WordPress/JetpackStatsWidgets/LockScreenWidgets/ViewProvider/LockScreenSingleStatWidgetViewProvider.swift index 2a748efbe6ed..286934f7f412 100644 --- a/WordPress/JetpackStatsWidgets/LockScreenWidgets/ViewProvider/LockScreenSingleStatWidgetViewProvider.swift +++ b/WordPress/JetpackStatsWidgets/LockScreenWidgets/ViewProvider/LockScreenSingleStatWidgetViewProvider.swift @@ -1,4 +1,5 @@ import SwiftUI +import JetpackStatsWidgetsCore @available(iOS 16.0, *) struct LockScreenSingleStatWidgetViewProvider: LockScreenStatsWidgetsViewProvider { diff --git a/WordPress/JetpackStatsWidgets/LockScreenWidgets/Views/LockScreenFieldView.swift b/WordPress/JetpackStatsWidgets/LockScreenWidgets/Views/LockScreenFieldView.swift index 87c12d10f233..af9c7466f167 100644 --- a/WordPress/JetpackStatsWidgets/LockScreenWidgets/Views/LockScreenFieldView.swift +++ b/WordPress/JetpackStatsWidgets/LockScreenWidgets/Views/LockScreenFieldView.swift @@ -2,9 +2,9 @@ import SwiftUI struct LockScreenFieldView: View { struct ValueFontSize { - static let `default`: CGFloat = 20 - static let medium: CGFloat = 18 - static let small: CGFloat = 16 + static let `default`: CGFloat = 22 + static let medium: CGFloat = 20 + static let small: CGFloat = 18 } let title: String @@ -24,18 +24,16 @@ struct LockScreenFieldView: View { var body: some View { VStack { - Spacer(minLength: 0) Text(value) .frame(maxWidth: .infinity, alignment: .leading) - .font(.system(size: valueFontSize, weight: .bold)) - .minimumScaleFactor(0.6) + .font(.system(size: valueFontSize, weight: .heavy)) + .minimumScaleFactor(0.9) .foregroundColor(.white) .allowsTightening(true) .lineLimit(1) - Spacer(minLength: 0) Text(title) .frame(maxWidth: .infinity, alignment: .leading) - .font(.system(size: 10)) + .font(.system(size: 11)) .minimumScaleFactor(0.9) .allowsTightening(true) .lineLimit(1) diff --git a/WordPress/JetpackStatsWidgets/LockScreenWidgets/Views/LockScreenMultiStatView.swift b/WordPress/JetpackStatsWidgets/LockScreenWidgets/Views/LockScreenMultiStatView.swift index cc68c7ac78bd..2ddcc195588b 100644 --- a/WordPress/JetpackStatsWidgets/LockScreenWidgets/Views/LockScreenMultiStatView.swift +++ b/WordPress/JetpackStatsWidgets/LockScreenWidgets/Views/LockScreenMultiStatView.swift @@ -9,10 +9,10 @@ struct LockScreenMultiStatView: View { var body: some View { if family == .accessoryRectangular { ZStack { - AccessoryWidgetBackground().cornerRadius(8) VStack(alignment: .leading) { + Spacer() LockScreenSiteTitleView(title: viewModel.siteName) - Spacer(minLength: 0) + Spacer().frame(height: 4) HStack(alignment: .bottom) { LockScreenFieldView( title: viewModel.primaryField.title, @@ -28,14 +28,14 @@ struct LockScreenMultiStatView: View { ) Spacer() } + Spacer() } - .padding( - EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8) - ) } + .removableWidgetBackground() .accessibilityElement(children: .combine) } else { Text("Not implemented for widget family \(family.debugDescription)") + .removableWidgetBackground() } } diff --git a/WordPress/JetpackStatsWidgets/LockScreenWidgets/Views/LockScreenSingleStatView.swift b/WordPress/JetpackStatsWidgets/LockScreenWidgets/Views/LockScreenSingleStatView.swift index ea98e787e996..c9b82721c27d 100644 --- a/WordPress/JetpackStatsWidgets/LockScreenWidgets/Views/LockScreenSingleStatView.swift +++ b/WordPress/JetpackStatsWidgets/LockScreenWidgets/Views/LockScreenSingleStatView.swift @@ -9,19 +9,19 @@ struct LockScreenSingleStatView: View { var body: some View { if family == .accessoryRectangular { ZStack { - AccessoryWidgetBackground().cornerRadius(8) VStack(alignment: .leading) { + Spacer() LockScreenSiteTitleView(title: viewModel.siteName) - Spacer(minLength: 0) + Spacer().frame(height: 4) LockScreenFieldView(title: viewModel.title, value: viewModel.value.abbreviatedString()) + Spacer() } - .padding( - EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8) - ) } + .removableWidgetBackground() .accessibilityElement(children: .combine) } else { Text("Not implemented for widget family \(family.debugDescription)") + .removableWidgetBackground() } } } diff --git a/WordPress/JetpackStatsWidgets/LockScreenWidgets/Views/LockScreenSiteTitleView.swift b/WordPress/JetpackStatsWidgets/LockScreenWidgets/Views/LockScreenSiteTitleView.swift index b94c3c1d77e0..8fa5d129c3c1 100644 --- a/WordPress/JetpackStatsWidgets/LockScreenWidgets/Views/LockScreenSiteTitleView.swift +++ b/WordPress/JetpackStatsWidgets/LockScreenWidgets/Views/LockScreenSiteTitleView.swift @@ -4,10 +4,16 @@ struct LockScreenSiteTitleView: View { let title: String var body: some View { - Text(title) - .frame(maxWidth: .infinity, alignment: .leading) - .font(.system(size: 10)) - .lineLimit(1) - .allowsTightening(true) + HStack(spacing: 4) { + Image("icon-jetpack") + .resizable() + .frame(width: 11, height: 11) + Text(title) + .frame(maxWidth: .infinity, alignment: .leading) + .font(.system(size: 11)) + .lineLimit(1) + .allowsTightening(true) + } + } } diff --git a/WordPress/JetpackStatsWidgets/LockScreenWidgets/Views/LockScreenStatsWidgetsView.swift b/WordPress/JetpackStatsWidgets/LockScreenWidgets/Views/LockScreenStatsWidgetsView.swift index 9388582ae676..5e4f556d48da 100644 --- a/WordPress/JetpackStatsWidgets/LockScreenWidgets/Views/LockScreenStatsWidgetsView.swift +++ b/WordPress/JetpackStatsWidgets/LockScreenWidgets/Views/LockScreenStatsWidgetsView.swift @@ -1,5 +1,6 @@ import SwiftUI import WidgetKit +import JetpackStatsWidgetsCore protocol LockScreenStatsWidgetsViewProvider { associatedtype SiteSelectedView: View diff --git a/WordPress/JetpackStatsWidgets/LockScreenWidgets/Views/LockScreenUnconfiguredView.swift b/WordPress/JetpackStatsWidgets/LockScreenWidgets/Views/LockScreenUnconfiguredView.swift index 480aeb488869..89f9310e5522 100644 --- a/WordPress/JetpackStatsWidgets/LockScreenWidgets/Views/LockScreenUnconfiguredView.swift +++ b/WordPress/JetpackStatsWidgets/LockScreenWidgets/Views/LockScreenUnconfiguredView.swift @@ -9,17 +9,15 @@ struct LockScreenUnconfiguredView: View { var body: some View { if family == .accessoryRectangular { ZStack { - AccessoryWidgetBackground().cornerRadius(8) Text(viewModel.message) .font(.system(size: 11)) .minimumScaleFactor(0.8) .multilineTextAlignment(.center) - .padding( - EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8) - ) } + .removableWidgetBackground() } else { Text("Not implemented for widget family \(family.debugDescription)") + .removableWidgetBackground() } } } diff --git a/WordPress/JetpackStatsWidgets/Model/HomeWidgetAllTimeData.swift b/WordPress/JetpackStatsWidgets/Model/HomeWidgetAllTimeData.swift index 439c45df3806..43a38c9f7528 100644 --- a/WordPress/JetpackStatsWidgets/Model/HomeWidgetAllTimeData.swift +++ b/WordPress/JetpackStatsWidgets/Model/HomeWidgetAllTimeData.swift @@ -1,3 +1,4 @@ +import JetpackStatsWidgetsCore struct HomeWidgetAllTimeData: HomeWidgetData { diff --git a/WordPress/JetpackStatsWidgets/Model/HomeWidgetData.swift b/WordPress/JetpackStatsWidgets/Model/HomeWidgetData.swift index 953fd622149b..533e58028a55 100644 --- a/WordPress/JetpackStatsWidgets/Model/HomeWidgetData.swift +++ b/WordPress/JetpackStatsWidgets/Model/HomeWidgetData.swift @@ -1,19 +1,7 @@ -import WidgetKit - -protocol HomeWidgetData: Codable { - - var siteID: Int { get } - var siteName: String { get } - var url: String { get } - var timeZone: TimeZone { get } - var date: Date { get } - var statsURL: URL? { get } - - static var filename: String { get } -} - +import JetpackStatsWidgetsCore // MARK: - Local cache + extension HomeWidgetData { static func read(from cache: HomeWidgetCache? = nil) -> [Int: Self]? { diff --git a/WordPress/JetpackStatsWidgets/Model/HomeWidgetThisWeekData.swift b/WordPress/JetpackStatsWidgets/Model/HomeWidgetThisWeekData.swift index 023f45a56914..5a602a53a1d8 100644 --- a/WordPress/JetpackStatsWidgets/Model/HomeWidgetThisWeekData.swift +++ b/WordPress/JetpackStatsWidgets/Model/HomeWidgetThisWeekData.swift @@ -1,3 +1,4 @@ +import JetpackStatsWidgetsCore struct HomeWidgetThisWeekData: HomeWidgetData { diff --git a/WordPress/JetpackStatsWidgets/Model/HomeWidgetTodayData.swift b/WordPress/JetpackStatsWidgets/Model/HomeWidgetTodayData.swift index b03e124045ab..8770cdbb1707 100644 --- a/WordPress/JetpackStatsWidgets/Model/HomeWidgetTodayData.swift +++ b/WordPress/JetpackStatsWidgets/Model/HomeWidgetTodayData.swift @@ -1,3 +1,4 @@ +import JetpackStatsWidgetsCore struct HomeWidgetTodayData: HomeWidgetData { diff --git a/WordPress/JetpackStatsWidgets/Model/ListViewData.swift b/WordPress/JetpackStatsWidgets/Model/ListViewData.swift index 8f484f19b16b..91b03cea78b4 100644 --- a/WordPress/JetpackStatsWidgets/Model/ListViewData.swift +++ b/WordPress/JetpackStatsWidgets/Model/ListViewData.swift @@ -1,4 +1,5 @@ import SwiftUI +import JetpackStatsWidgetsCore struct ListViewData { diff --git a/WordPress/JetpackStatsWidgets/Model/StatsWidgetEntry.swift b/WordPress/JetpackStatsWidgets/Model/StatsWidgetEntry.swift index 526d6a344afc..88b4c6a0a7d3 100644 --- a/WordPress/JetpackStatsWidgets/Model/StatsWidgetEntry.swift +++ b/WordPress/JetpackStatsWidgets/Model/StatsWidgetEntry.swift @@ -1,4 +1,5 @@ import WidgetKit +import JetpackStatsWidgetsCore enum StatsWidgetEntry: TimelineEntry { case siteSelected(HomeWidgetData, TimelineProviderContext) diff --git a/WordPress/JetpackStatsWidgets/Remote service/StatsWidgetsService.swift b/WordPress/JetpackStatsWidgets/Remote service/StatsWidgetsService.swift index c6eafc1cc3de..37c39152aa10 100644 --- a/WordPress/JetpackStatsWidgets/Remote service/StatsWidgetsService.swift +++ b/WordPress/JetpackStatsWidgets/Remote service/StatsWidgetsService.swift @@ -1,4 +1,5 @@ import WordPressKit +import JetpackStatsWidgetsCore /// Type that wraps the backend request for new stats class StatsWidgetsService { @@ -155,7 +156,7 @@ class StatsWidgetsService { url: widgetData.url, timeZone: widgetData.timeZone, date: Date(), - stats: ThisWeekWidgetStats(days: ThisWeekWidgetStats.daysFrom(summaryData: summaryData))) + stats: ThisWeekWidgetStats(days: ThisWeekWidgetStats.daysFrom(summaryData: summaryData.map { ThisWeekWidgetStats.Input(periodStartDate: $0.periodStartDate, viewsCount: $0.viewsCount) }))) completion(.success(newWidgetData)) DispatchQueue.global().async { diff --git a/WordPress/JetpackStatsWidgets/SiteListProvider.swift b/WordPress/JetpackStatsWidgets/SiteListProvider.swift index b5ef54f7834c..6a1f7cafb603 100644 --- a/WordPress/JetpackStatsWidgets/SiteListProvider.swift +++ b/WordPress/JetpackStatsWidgets/SiteListProvider.swift @@ -1,5 +1,6 @@ import WidgetKit import SwiftUI +import JetpackStatsWidgetsCore struct SiteListProvider: IntentTimelineProvider { diff --git a/WordPress/JetpackStatsWidgets/Tracks/Tracks+StatsWidgets.swift b/WordPress/JetpackStatsWidgets/Tracks/Tracks+StatsWidgets.swift deleted file mode 100644 index f1f6261f5e46..000000000000 --- a/WordPress/JetpackStatsWidgets/Tracks/Tracks+StatsWidgets.swift +++ /dev/null @@ -1,120 +0,0 @@ -import Foundation -import WidgetKit - -/// This extension implements helper tracking methods meant for Home & Lock screen widgets. -/// -extension Tracks { - - func trackWidgetUpdatedIfNeeded(entry: LockScreenStatsWidgetEntry, widgetKind: AppConfiguration.Widget.Stats.Kind) { - switch entry { - case .siteSelected(_, let context): - if !context.isPreview { - trackWidgetUpdated(widgetKind: widgetKind) - } - - case .loggedOut, .noSite, .noData: - trackWidgetUpdated(widgetKind: widgetKind) - } - } - - func trackWidgetUpdatedIfNeeded(entry: StatsWidgetEntry, widgetKind: AppConfiguration.Widget.Stats.Kind) { - switch entry { - case .siteSelected(_, let context): - if !context.isPreview { - trackWidgetUpdated(widgetKind: widgetKind) - } - - case .loggedOut, .noSite, .noData, .disabled: - trackWidgetUpdated(widgetKind: widgetKind) - } - } - - func trackWidgetUpdated(widgetKind: AppConfiguration.Widget.Stats.Kind) { - - DispatchQueue.global().async { - WidgetCenter.shared.getCurrentConfigurations { result in - - switch result { - - case .success(let widgetInfo): - let widgetKindInfo = widgetInfo.filter { $0.kind == widgetKind.rawValue } - self.trackUpdatedWidgetInfo(widgetInfo: widgetKindInfo, widgetKind: widgetKind) - - case .failure(let error): - DDLogError("Home Widget Today error: unable to read widget information. \(error.localizedDescription)") - } - } - } - } - - private func trackUpdatedWidgetInfo(widgetInfo: [WidgetInfo], widgetKind: AppConfiguration.Widget.Stats.Kind) { - let widgetPropertiesKey = widgetKind.countKey - - var properties: [String: Int] = [:] - - switch widgetKind { - case .homeToday, .homeThisWeek, .homeAllTime: - properties = ["total_widgets": widgetInfo.count, - "small_widgets": widgetInfo.filter { $0.family == .systemSmall }.count, - "medium_widgets": widgetInfo.filter { $0.family == .systemMedium }.count, - "large_widgets": widgetInfo.filter { $0.family == .systemLarge }.count] - default: - break - } - - let previousProperties = UserDefaults(suiteName: WPAppGroupName)?.object(forKey: widgetPropertiesKey) as? [String: Int] - - guard previousProperties != properties else { - return - } - - UserDefaults(suiteName: WPAppGroupName)?.set(properties, forKey: widgetPropertiesKey) - - trackExtensionEvent(ExtensionEvents.widgetUpdated(for: widgetKind), properties: properties as [String: AnyObject]?) - } - - // MARK: - Private Helpers - - fileprivate func trackExtensionEvent(_ event: ExtensionEvents, properties: [String: AnyObject]? = nil) { - track(event.rawValue, properties: properties) - } - - - // MARK: - Private Enums - - fileprivate enum ExtensionEvents: String { - // Events when user installs an instance of the widget - case homeTodayWidgetUpdated = "today_home_extension_widget_updated" - case homeAllTimeWidgetUpdated = "alltime_home_extension_widget_updated" - case homeThisWeekWidgetUpdated = "thisweek_home_extension_widget_updated" - case lockScreenTodayViewsWidgetUpdated = "today_views_lockscreen_widget_updated" - case lockScreenTodayLikesCommentsWidgetUpdated = "today_likes_comments_lockscreen_widget_updated" - case lockScreenTodayViewsVisitorsWidgetUpdated = "today_views_visitors_lockscreen_widget_updated" - case lockScreenAllTimeViewsWidgetUpdated = "all_time_views_lockscreen_widget_updated" - case lockScreenAllTimeViewsVisitorsWidgetUpdated = "all_time_views_visitors_lockscreen_widget_updated" - case lockScreenAllTimePostsBestViewsWidgetUpdated = "all_time_posts_best_views_lockscreen_widget_updated" - - static func widgetUpdated(for widgetKind: AppConfiguration.Widget.Stats.Kind) -> ExtensionEvents { - switch widgetKind { - case .homeToday: - return .homeTodayWidgetUpdated - case .homeAllTime: - return .homeAllTimeWidgetUpdated - case .homeThisWeek: - return .homeThisWeekWidgetUpdated - case .lockScreenTodayViews: - return .lockScreenTodayViewsWidgetUpdated - case .lockScreenTodayLikesComments: - return .lockScreenTodayLikesCommentsWidgetUpdated - case .lockScreenTodayViewsVisitors: - return .lockScreenTodayViewsVisitorsWidgetUpdated - case .lockScreenAllTimeViews: - return .lockScreenAllTimeViewsWidgetUpdated - case .lockScreenAllTimeViewsVisitors: - return .lockScreenAllTimeViewsVisitorsWidgetUpdated - case .lockScreenAllTimePostsBestViews: - return .lockScreenAllTimePostsBestViewsWidgetUpdated - } - } - } -} diff --git a/WordPress/JetpackStatsWidgets/Tracks/WidgetAnalytics.swift b/WordPress/JetpackStatsWidgets/Tracks/WidgetAnalytics.swift new file mode 100644 index 000000000000..abfdf44a4cad --- /dev/null +++ b/WordPress/JetpackStatsWidgets/Tracks/WidgetAnalytics.swift @@ -0,0 +1,68 @@ +import Foundation +import WidgetKit + +@objcMembers class WidgetAnalytics: NSObject { + static func trackLoadedWidgetsOnApplicationOpened() { + guard AppConfiguration.isJetpack else { return } + + WidgetCenter.shared.getCurrentConfigurations { result in + let properties = self.properties(from: result) + WPAnalytics.track(.widgetsLoadedOnApplicationOpened, properties: properties) + } + } + + private static func properties(from widgetInfo: Result<[WidgetInfo], Error>) -> [String: Int] { + guard let installedWidgets = try? widgetInfo.get() else { + return [:] + } + + let widgetAnalyticNames: [String] = installedWidgets.map { widgetInfo in + guard let eventKind = AppConfiguration.Widget.Stats.Kind(rawValue: widgetInfo.kind) else { + DDLogWarn("⚠️ Make sure the widget: \(widgetInfo.kind), has the correct kind.") + return "\(widgetInfo.kind)_\(widgetInfo.family)" + } + return "\(Events.eventPrefix(for: eventKind).rawValue)_\(widgetInfo.family.description.lowercased())" + } + + let dict = widgetAnalyticNames.reduce(into: [String: Int]()) { partialResult, name in + partialResult[name, default: 0] += 1 + } + + return dict + } + + private enum Events: String { + case homeTodayWidget = "widget_today_home" + case homeAllTimeWidget = "widget_all_time_home" + case homeThisWeekWidget = "widget_this_week_home" + case lockScreenTodayViewsWidget = "widget_today_views_lockscreen" + case lockScreenTodayLikesCommentsWidget = "widget_today_likes_comments_lockscreen" + case lockScreenTodayViewsVisitorsWidget = "widget_today_views_visitors_lockscreen" + case lockScreenAllTimeViewsWidget = "widget_all_time_views_lockscreen" + case lockScreenAllTimeViewsVisitorsWidget = "widget_all_time_views_visitors_lockscreen" + case lockScreenAllTimePostsBestViewsWidget = "widget_all_time_posts_best_views_lockscreen" + + static func eventPrefix(for widgetKind: AppConfiguration.Widget.Stats.Kind) -> Events { + switch widgetKind { + case .homeToday: + return .homeTodayWidget + case .homeAllTime: + return .homeAllTimeWidget + case .homeThisWeek: + return .homeThisWeekWidget + case .lockScreenTodayViews: + return .lockScreenTodayViewsWidget + case .lockScreenTodayLikesComments: + return .lockScreenTodayLikesCommentsWidget + case .lockScreenTodayViewsVisitors: + return .lockScreenTodayViewsVisitorsWidget + case .lockScreenAllTimeViews: + return .lockScreenAllTimeViewsWidget + case .lockScreenAllTimeViewsVisitors: + return .lockScreenAllTimeViewsVisitorsWidget + case .lockScreenAllTimePostsBestViews: + return .lockScreenAllTimePostsBestViewsWidget + } + } + } +} diff --git a/WordPress/JetpackStatsWidgets/Views/Cards/LockScreenFlexibleCard.swift b/WordPress/JetpackStatsWidgets/Views/Cards/LockScreenFlexibleCard.swift new file mode 100644 index 000000000000..47a48ec05dcc --- /dev/null +++ b/WordPress/JetpackStatsWidgets/Views/Cards/LockScreenFlexibleCard.swift @@ -0,0 +1,44 @@ +import SwiftUI + +/// A card with a title and a string value that is shown on LockScreen without background +struct LockScreenFlexibleCard: View { + let title: LocalizedString + let description: LocalizedString + let lineLimit: Int + + init(title: LocalizedString, description: LocalizedString, lineLimit: Int = 1) { + self.title = title + self.description = description + self.lineLimit = lineLimit + } + + var body: some View { + VStack(alignment: .leading) { + HStack(alignment: .firstTextBaseline, spacing: 4) { + Image("icon-jetpack") + .resizable() + .frame(width: 14, height: 14) + Text(description) + .font(Appearance.textFont) + .fontWeight(Appearance.textFontWeight) + .foregroundColor(Appearance.textColor) + .lineLimit(lineLimit) + } + Text(title) + .font(Appearance.titleFont) + .foregroundColor(Appearance.titleColor) + } + } +} + +// MARK: - Appearance +extension LockScreenFlexibleCard { + private enum Appearance { + static let textFont = Font.headline + static let textFontWeight = Font.Weight.semibold + static let textColor = Color(.label) + + static let titleFont = Font.subheadline + static let titleColor = Color(.secondaryLabel) + } +} diff --git a/WordPress/JetpackStatsWidgets/Views/Cards/LockScreenVerticalCard.swift b/WordPress/JetpackStatsWidgets/Views/Cards/LockScreenVerticalCard.swift new file mode 100644 index 000000000000..752c31c24d84 --- /dev/null +++ b/WordPress/JetpackStatsWidgets/Views/Cards/LockScreenVerticalCard.swift @@ -0,0 +1,39 @@ +import SwiftUI + +/// A card with a title and a value stacked vertically shown on LockScreen without background +struct LockScreenVerticalCard: View { + let title: LocalizedString + let value: Int + + private var accessibilityLabel: Text { + // The colon makes VoiceOver pause between elements + Text(title) + Text(": ") + Text(value.abbreviatedString()) + } + + var body: some View { + VStack(alignment: .leading) { + StatsValueView(value: value, + font: Appearance.extraLargeTextFont, + fontWeight: .heavy, + foregroundColor: Appearance.textColor, + lineLimit: nil) + .accessibility(label: accessibilityLabel) + Text(title) + .font(Appearance.titleFont) + .fontWeight(Appearance.titleFontWeight) + .foregroundColor(Appearance.titleColor) + .accessibility(hidden: true) + } + } +} + +// MARK: - Appearance +extension LockScreenVerticalCard { + private enum Appearance { + static let titleFont = Font.headline + static let titleFontWeight = Font.Weight.semibold + static let titleColor = Color(UIColor.label) + static let extraLargeTextFont = Font.system(size: 48, weight: .bold) + static let textColor = Color(UIColor.label) + } +} diff --git a/WordPress/JetpackStatsWidgets/Views/Cards/StatsValueView.swift b/WordPress/JetpackStatsWidgets/Views/Cards/StatsValueView.swift index 3436559bd445..324ced823334 100644 --- a/WordPress/JetpackStatsWidgets/Views/Cards/StatsValueView.swift +++ b/WordPress/JetpackStatsWidgets/Views/Cards/StatsValueView.swift @@ -30,5 +30,6 @@ struct StatsValueView: View { .fontWeight(fontWeight) .foregroundColor(foregroundColor) .lineLimit(lineLimit) + .minimumScaleFactor(0.5) } } diff --git a/WordPress/JetpackStatsWidgets/Views/Cards/VerticalCard.swift b/WordPress/JetpackStatsWidgets/Views/Cards/VerticalCard.swift index afef33c3743a..b9fb75193e2f 100644 --- a/WordPress/JetpackStatsWidgets/Views/Cards/VerticalCard.swift +++ b/WordPress/JetpackStatsWidgets/Views/Cards/VerticalCard.swift @@ -6,6 +6,8 @@ struct VerticalCard: View { let value: Int let largeText: Bool + @Environment(\.showsWidgetContainerBackground) var showsWidgetContainerBackground: Bool + private var titleFont: Font { largeText ? Appearance.largeTextFont : Appearance.textFont } @@ -17,18 +19,31 @@ struct VerticalCard: View { var body: some View { VStack(alignment: .leading) { - Text(title) - .font(Appearance.titleFont) - .fontWeight(Appearance.titleFontWeight) - .foregroundColor(Appearance.titleColor) - .accessibility(hidden: true) - StatsValueView(value: value, - font: titleFont, - fontWeight: .regular, - foregroundColor: Appearance.textColor, - lineLimit: nil) - .accessibility(label: accessibilityLabel) - + if showsWidgetContainerBackground { + Text(title) + .font(Appearance.titleFont) + .fontWeight(Appearance.titleFontWeight) + .foregroundColor(Appearance.titleColor) + .accessibility(hidden: true) + StatsValueView(value: value, + font: titleFont, + fontWeight: .regular, + foregroundColor: Appearance.textColor, + lineLimit: nil) + .accessibility(label: accessibilityLabel) + } else { + StatsValueView(value: value, + font: Appearance.extraLargeTextFont, + fontWeight: .heavy, + foregroundColor: Appearance.textColor, + lineLimit: nil) + .accessibility(label: accessibilityLabel) + Text(title) + .font(Appearance.titleFont) + .fontWeight(Appearance.titleFontWeight) + .foregroundColor(Appearance.titleColor) + .accessibility(hidden: true) + } } } } @@ -43,6 +58,7 @@ extension VerticalCard { static let titleColor = Color(UIColor.primary) static let largeTextFont = Font.largeTitle + static let extraLargeTextFont = Font.system(size: 48, weight: .bold) static let textFont = Font.title static let textColor = Color(.label) } diff --git a/WordPress/JetpackStatsWidgets/Views/Helpers/iOS17WidgetAPIs.swift b/WordPress/JetpackStatsWidgets/Views/Helpers/iOS17WidgetAPIs.swift new file mode 100644 index 000000000000..01e4ea037c92 --- /dev/null +++ b/WordPress/JetpackStatsWidgets/Views/Helpers/iOS17WidgetAPIs.swift @@ -0,0 +1,24 @@ +import SwiftUI +import WidgetKit + +extension View { + func removableWidgetBackground(_ backgroundView: some View = EmptyView()) -> some View { + if #available(iOSApplicationExtension 17.0, *) { + return containerBackground(for: .widget) { + backgroundView + } + } else { + return background(backgroundView) + } + } +} + +extension WidgetConfiguration { + func iOS17ContentMarginsDisabled() -> some WidgetConfiguration { + if #available(iOSApplicationExtension 17.0, *) { + return contentMarginsDisabled() + } else { + return self + } + } +} diff --git a/WordPress/JetpackStatsWidgets/Views/ListStatsView.swift b/WordPress/JetpackStatsWidgets/Views/ListStatsView.swift index d96ecbde2e7c..6dcc94a67657 100644 --- a/WordPress/JetpackStatsWidgets/Views/ListStatsView.swift +++ b/WordPress/JetpackStatsWidgets/Views/ListStatsView.swift @@ -1,5 +1,6 @@ import SwiftUI import WidgetKit +import JetpackStatsWidgetsCore struct ListStatsView: View { @Environment(\.widgetFamily) var family: WidgetFamily @@ -33,6 +34,7 @@ struct ListStatsView: View { } } } + .removableWidgetBackground() } } diff --git a/WordPress/JetpackStatsWidgets/Views/Localization/LocalizationConfiguration.swift b/WordPress/JetpackStatsWidgets/Views/Localization/LocalizationConfiguration.swift deleted file mode 100644 index f454f5493abe..000000000000 --- a/WordPress/JetpackStatsWidgets/Views/Localization/LocalizationConfiguration.swift +++ /dev/null @@ -1,7 +0,0 @@ -extension AppConfiguration.Widget { - struct Localization { - static let unconfiguredViewTodayTitle = LocalizableStrings.unconfiguredViewTodayTitle - static let unconfiguredViewThisWeekTitle = LocalizableStrings.unconfiguredViewThisWeekTitle - static let unconfiguredViewAllTimeTitle = LocalizableStrings.unconfiguredViewAllTimeTitle - } -} diff --git a/WordPress/JetpackStatsWidgets/Views/MultiStatsView.swift b/WordPress/JetpackStatsWidgets/Views/MultiStatsView.swift index 86b86be89bdc..07dcb61ad517 100644 --- a/WordPress/JetpackStatsWidgets/Views/MultiStatsView.swift +++ b/WordPress/JetpackStatsWidgets/Views/MultiStatsView.swift @@ -23,6 +23,7 @@ Spacer() } } + .removableWidgetBackground() } /// Constructs a two-card column for the medium size Today widget diff --git a/WordPress/JetpackStatsWidgets/Views/SingleStatView.swift b/WordPress/JetpackStatsWidgets/Views/SingleStatView.swift index 580d755823b2..f606543d2121 100644 --- a/WordPress/JetpackStatsWidgets/Views/SingleStatView.swift +++ b/WordPress/JetpackStatsWidgets/Views/SingleStatView.swift @@ -1,19 +1,59 @@ import SwiftUI +import WidgetKit struct SingleStatView: View { + let title: String + let description: String + let valueTitle: String + let value: Int - let viewData: GroupedViewData + @Environment(\.showsWidgetContainerBackground) var showsWidgetContainerBackground: Bool + + init(viewData: GroupedViewData) { + self.title = viewData.widgetTitle + self.description = viewData.siteName + self.valueTitle = viewData.upperLeftTitle + self.value = viewData.upperLeftValue + } + + init(title: String, description: String, valueTitle: String, value: Int) { + self.title = title + self.description = description + self.valueTitle = valueTitle + self.value = value + } var body: some View { HStack { - VStack(alignment: .leading) { - FlexibleCard(axis: .vertical, title: viewData.widgetTitle, value: .description(viewData.siteName), lineLimit: 2) - - Spacer() - VerticalCard(title: viewData.upperLeftTitle, value: viewData.upperLeftValue, largeText: true) + if showsWidgetContainerBackground { + VStack(alignment: .leading) { + FlexibleCard(axis: .vertical, title: title, value: .description(description), lineLimit: 2) + Spacer() + VerticalCard(title: valueTitle, value: value, largeText: true) + } + .padding() + } else { + VStack(alignment: .leading) { + Spacer() + LockScreenFlexibleCard(title: title, description: description, lineLimit: 2) + Spacer().frame(height: 4) + LockScreenVerticalCard(title: valueTitle, value: value) + Spacer() + } } Spacer() } + .removableWidgetBackground() + } +} + +@available(iOS 16.0, *) +struct SingleStatView_Previews: PreviewProvider { + static var previews: some View { + SingleStatView(title: "Today", description: "My WordPress Site", valueTitle: "Views", value: 124909) + .previewContext( + WidgetPreviewContext(family: .systemSmall) + ) } } diff --git a/WordPress/JetpackStatsWidgets/Views/StatsWidgetsView.swift b/WordPress/JetpackStatsWidgets/Views/StatsWidgetsView.swift index d7f16d5a46ed..4428961b9b08 100644 --- a/WordPress/JetpackStatsWidgets/Views/StatsWidgetsView.swift +++ b/WordPress/JetpackStatsWidgets/Views/StatsWidgetsView.swift @@ -1,3 +1,4 @@ +import JetpackStatsWidgetsCore import SwiftUI import WidgetKit @@ -26,7 +27,6 @@ struct StatsWidgetsView: View { case .systemSmall: SingleStatView(viewData: viewData) .widgetURL(viewData.statsURL?.appendingSource(.homeScreenWidget)) - .padding() case .systemMedium: MultiStatsView(viewData: viewData) diff --git a/WordPress/JetpackStatsWidgets/Views/UnconfiguredView.swift b/WordPress/JetpackStatsWidgets/Views/UnconfiguredView.swift index f515edd8008b..9145b392d1be 100644 --- a/WordPress/JetpackStatsWidgets/Views/UnconfiguredView.swift +++ b/WordPress/JetpackStatsWidgets/Views/UnconfiguredView.swift @@ -3,13 +3,15 @@ import SwiftUI struct UnconfiguredView: View { var timelineEntry: StatsWidgetEntry + @Environment(\.showsWidgetContainerBackground) var showsWidgetContainerBackground: Bool var body: some View { Text(unconfiguredMessage) .font(.footnote) - .foregroundColor(Color(.secondaryLabel)) + .foregroundColor(showsWidgetContainerBackground ? Color(.secondaryLabel) : Color(.label)) .multilineTextAlignment(.center) .padding() + .removableWidgetBackground() } var unconfiguredMessage: LocalizedString { diff --git a/WordPress/JetpackStatsWidgets/Widgets/HomeWidgetAllTime.swift b/WordPress/JetpackStatsWidgets/Widgets/HomeWidgetAllTime.swift index 5d6d5c071f0a..0bbe43f5e0f6 100644 --- a/WordPress/JetpackStatsWidgets/Widgets/HomeWidgetAllTime.swift +++ b/WordPress/JetpackStatsWidgets/Widgets/HomeWidgetAllTime.swift @@ -22,16 +22,11 @@ struct HomeWidgetAllTime: Widget { placeholderContent: placeholderContent, widgetKind: .allTime) ) { (entry: StatsWidgetEntry) -> StatsWidgetsView in - - defer { - tracks.trackWidgetUpdatedIfNeeded(entry: entry, - widgetKind: AppConfiguration.Widget.Stats.Kind.homeAllTime) - } - return StatsWidgetsView(timelineEntry: entry) } .configurationDisplayName(LocalizableStrings.allTimeWidgetTitle) .description(LocalizableStrings.allTimePreviewDescription) .supportedFamilies([.systemSmall, .systemMedium]) + .iOS17ContentMarginsDisabled() /// Temporarily disable additional iOS17 margins for widgets } } diff --git a/WordPress/JetpackStatsWidgets/Widgets/HomeWidgetThisWeek.swift b/WordPress/JetpackStatsWidgets/Widgets/HomeWidgetThisWeek.swift index 9d419523fefc..f48f4ebb6479 100644 --- a/WordPress/JetpackStatsWidgets/Widgets/HomeWidgetThisWeek.swift +++ b/WordPress/JetpackStatsWidgets/Widgets/HomeWidgetThisWeek.swift @@ -1,5 +1,6 @@ import WidgetKit import SwiftUI +import JetpackStatsWidgetsCore struct HomeWidgetThisWeek: Widget { @@ -42,16 +43,11 @@ struct HomeWidgetThisWeek: Widget { placeholderContent: placeholderContent, widgetKind: .thisWeek) ) { (entry: StatsWidgetEntry) -> StatsWidgetsView in - - defer { - tracks.trackWidgetUpdatedIfNeeded(entry: entry, - widgetKind: AppConfiguration.Widget.Stats.Kind.homeThisWeek) - } - return StatsWidgetsView(timelineEntry: entry) } .configurationDisplayName(LocalizableStrings.thisWeekWidgetTitle) .description(LocalizableStrings.thisWeekPreviewDescription) .supportedFamilies([.systemMedium, .systemLarge]) + .iOS17ContentMarginsDisabled() /// Temporarily disable additional iOS17 margins for widgets } } diff --git a/WordPress/JetpackStatsWidgets/Widgets/HomeWidgetToday.swift b/WordPress/JetpackStatsWidgets/Widgets/HomeWidgetToday.swift index e4637d5aee09..93225f77cc33 100644 --- a/WordPress/JetpackStatsWidgets/Widgets/HomeWidgetToday.swift +++ b/WordPress/JetpackStatsWidgets/Widgets/HomeWidgetToday.swift @@ -23,16 +23,11 @@ struct HomeWidgetToday: Widget { placeholderContent: placeholderContent, widgetKind: .today) ) { (entry: StatsWidgetEntry) -> StatsWidgetsView in - - defer { - tracks.trackWidgetUpdatedIfNeeded(entry: entry, - widgetKind: AppConfiguration.Widget.Stats.Kind.homeToday) - } - return StatsWidgetsView(timelineEntry: entry) } .configurationDisplayName(LocalizableStrings.todayWidgetTitle) .description(LocalizableStrings.todayPreviewDescription) .supportedFamilies([.systemSmall, .systemMedium]) + .iOS17ContentMarginsDisabled() /// Temporarily disable additional iOS17 margins for widgets for StandBy } } diff --git a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Theme/Contents.json b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/Contents.json similarity index 100% rename from WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Theme/Contents.json rename to WordPress/Resources/AppImages.xcassets/Bottom Tabs/Contents.json diff --git a/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-home-selected.imageset/Contents.json b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-home-selected.imageset/Contents.json new file mode 100644 index 000000000000..389937d2e482 --- /dev/null +++ b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-home-selected.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "tab-bar-home-selected.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-home-selected.imageset/tab-bar-home-selected.pdf b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-home-selected.imageset/tab-bar-home-selected.pdf new file mode 100644 index 000000000000..cf3214b74428 Binary files /dev/null and b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-home-selected.imageset/tab-bar-home-selected.pdf differ diff --git a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Border/borderDivider.colorset/Contents.json b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-home-unselected.imageset/Contents.json similarity index 56% rename from WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Border/borderDivider.colorset/Contents.json rename to WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-home-unselected.imageset/Contents.json index 5a9111d90cd6..406a1bf929c8 100644 --- a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Border/borderDivider.colorset/Contents.json +++ b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-home-unselected.imageset/Contents.json @@ -1,10 +1,7 @@ { - "colors" : [ + "images" : [ { - "color" : { - "platform" : "ios", - "reference" : "separatorColor" - }, + "filename" : "tab-bar-home-unselected.pdf", "idiom" : "universal" }, { @@ -14,10 +11,7 @@ "value" : "dark" } ], - "color" : { - "platform" : "ios", - "reference" : "separatorColor" - }, + "filename" : "tab-bar-home-unselected-dark.pdf", "idiom" : "universal" } ], diff --git a/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-home-unselected.imageset/tab-bar-home-unselected-dark.pdf b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-home-unselected.imageset/tab-bar-home-unselected-dark.pdf new file mode 100644 index 000000000000..7982e2fb7e8b Binary files /dev/null and b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-home-unselected.imageset/tab-bar-home-unselected-dark.pdf differ diff --git a/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-home-unselected.imageset/tab-bar-home-unselected.pdf b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-home-unselected.imageset/tab-bar-home-unselected.pdf new file mode 100644 index 000000000000..a04417928e26 Binary files /dev/null and b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-home-unselected.imageset/tab-bar-home-unselected.pdf differ diff --git a/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-me-selected.imageset/Contents.json b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-me-selected.imageset/Contents.json new file mode 100644 index 000000000000..fd311e91eeb0 --- /dev/null +++ b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-me-selected.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "tab-bar-me-selected.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-me-selected.imageset/tab-bar-me-selected.pdf b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-me-selected.imageset/tab-bar-me-selected.pdf new file mode 100644 index 000000000000..a8f6dcf410a0 Binary files /dev/null and b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-me-selected.imageset/tab-bar-me-selected.pdf differ diff --git a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Border/borderSecondary.colorset/Contents.json b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-me-unselected.imageset/Contents.json similarity index 56% rename from WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Border/borderSecondary.colorset/Contents.json rename to WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-me-unselected.imageset/Contents.json index 6078a53374fd..506d3351507e 100644 --- a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Border/borderSecondary.colorset/Contents.json +++ b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-me-unselected.imageset/Contents.json @@ -1,10 +1,7 @@ { - "colors" : [ + "images" : [ { - "color" : { - "platform" : "ios", - "reference" : "systemGray3Color" - }, + "filename" : "tab-bar-me-unselected.pdf", "idiom" : "universal" }, { @@ -14,10 +11,7 @@ "value" : "dark" } ], - "color" : { - "platform" : "ios", - "reference" : "systemGray3Color" - }, + "filename" : "tab-bar-me-unselected-dark.pdf", "idiom" : "universal" } ], diff --git a/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-me-unselected.imageset/tab-bar-me-unselected-dark.pdf b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-me-unselected.imageset/tab-bar-me-unselected-dark.pdf new file mode 100644 index 000000000000..03957cbba0e4 Binary files /dev/null and b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-me-unselected.imageset/tab-bar-me-unselected-dark.pdf differ diff --git a/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-me-unselected.imageset/tab-bar-me-unselected.pdf b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-me-unselected.imageset/tab-bar-me-unselected.pdf new file mode 100644 index 000000000000..7f6272ef5bf8 Binary files /dev/null and b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-me-unselected.imageset/tab-bar-me-unselected.pdf differ diff --git a/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-notifications-selected.imageset/Contents.json b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-notifications-selected.imageset/Contents.json new file mode 100644 index 000000000000..a62a87b53879 --- /dev/null +++ b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-notifications-selected.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "tab-bar-notifications-selected.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-notifications-selected.imageset/tab-bar-notifications-selected.pdf b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-notifications-selected.imageset/tab-bar-notifications-selected.pdf new file mode 100644 index 000000000000..222f8140415c Binary files /dev/null and b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-notifications-selected.imageset/tab-bar-notifications-selected.pdf differ diff --git a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Foreground/foregroundPrimary.colorset/Contents.json b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-notifications-unread-jp.imageset/Contents.json similarity index 56% rename from WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Foreground/foregroundPrimary.colorset/Contents.json rename to WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-notifications-unread-jp.imageset/Contents.json index 1effc4f12791..f9f8ed8b37b1 100644 --- a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Foreground/foregroundPrimary.colorset/Contents.json +++ b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-notifications-unread-jp.imageset/Contents.json @@ -1,10 +1,7 @@ { - "colors" : [ + "images" : [ { - "color" : { - "platform" : "universal", - "reference" : "labelColor" - }, + "filename" : "tab-bar-notifications-unread-jp.pdf", "idiom" : "universal" }, { @@ -14,10 +11,7 @@ "value" : "dark" } ], - "color" : { - "platform" : "universal", - "reference" : "labelColor" - }, + "filename" : "tab-bar-notifications-unread-jp-dark.pdf", "idiom" : "universal" } ], diff --git a/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-notifications-unread-jp.imageset/tab-bar-notifications-unread-jp-dark.pdf b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-notifications-unread-jp.imageset/tab-bar-notifications-unread-jp-dark.pdf new file mode 100644 index 000000000000..21cd3e937b19 Binary files /dev/null and b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-notifications-unread-jp.imageset/tab-bar-notifications-unread-jp-dark.pdf differ diff --git a/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-notifications-unread-jp.imageset/tab-bar-notifications-unread-jp.pdf b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-notifications-unread-jp.imageset/tab-bar-notifications-unread-jp.pdf new file mode 100644 index 000000000000..2c0cdbc01368 Binary files /dev/null and b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-notifications-unread-jp.imageset/tab-bar-notifications-unread-jp.pdf differ diff --git a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Border/borderPrimary.colorset/Contents.json b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-notifications-unread-wp.imageset/Contents.json similarity index 55% rename from WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Border/borderPrimary.colorset/Contents.json rename to WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-notifications-unread-wp.imageset/Contents.json index 274b3ba4edf5..28b70218b344 100644 --- a/WordPress/Classes/ViewRelated/Site Creation/Web Address/DesignSystem/Colors.xcassets/Foundation/Border/borderPrimary.colorset/Contents.json +++ b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-notifications-unread-wp.imageset/Contents.json @@ -1,10 +1,7 @@ { - "colors" : [ + "images" : [ { - "color" : { - "platform" : "ios", - "reference" : "opaqueSeparatorColor" - }, + "filename" : "tab-bar-notifications-unread-wp.pdf", "idiom" : "universal" }, { @@ -14,10 +11,7 @@ "value" : "dark" } ], - "color" : { - "platform" : "ios", - "reference" : "opaqueSeparatorColor" - }, + "filename" : "tab-bar-notifications-unread-wp-dark.pdf", "idiom" : "universal" } ], diff --git a/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-notifications-unread-wp.imageset/tab-bar-notifications-unread-wp-dark.pdf b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-notifications-unread-wp.imageset/tab-bar-notifications-unread-wp-dark.pdf new file mode 100644 index 000000000000..f830b4afc8bd Binary files /dev/null and b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-notifications-unread-wp.imageset/tab-bar-notifications-unread-wp-dark.pdf differ diff --git a/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-notifications-unread-wp.imageset/tab-bar-notifications-unread-wp.pdf b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-notifications-unread-wp.imageset/tab-bar-notifications-unread-wp.pdf new file mode 100644 index 000000000000..3d4ff59f51af Binary files /dev/null and b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-notifications-unread-wp.imageset/tab-bar-notifications-unread-wp.pdf differ diff --git a/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-notifications-unselected.imageset/Contents.json b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-notifications-unselected.imageset/Contents.json new file mode 100644 index 000000000000..98462678c016 --- /dev/null +++ b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-notifications-unselected.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "tab-bar-notifications-unselected.pdf", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "tab-bar-notifications-unselected-dark.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-notifications-unselected.imageset/tab-bar-notifications-unselected-dark.pdf b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-notifications-unselected.imageset/tab-bar-notifications-unselected-dark.pdf new file mode 100644 index 000000000000..64520deb2378 Binary files /dev/null and b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-notifications-unselected.imageset/tab-bar-notifications-unselected-dark.pdf differ diff --git a/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-notifications-unselected.imageset/tab-bar-notifications-unselected.pdf b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-notifications-unselected.imageset/tab-bar-notifications-unselected.pdf new file mode 100644 index 000000000000..3663a2fa0a02 Binary files /dev/null and b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-notifications-unselected.imageset/tab-bar-notifications-unselected.pdf differ diff --git a/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-reader-selected.imageset/Contents.json b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-reader-selected.imageset/Contents.json new file mode 100644 index 000000000000..aedb797c23ed --- /dev/null +++ b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-reader-selected.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "tab-bar-reader-selected.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-reader-selected.imageset/tab-bar-reader-selected.pdf b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-reader-selected.imageset/tab-bar-reader-selected.pdf new file mode 100644 index 000000000000..339c6d26aa43 Binary files /dev/null and b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-reader-selected.imageset/tab-bar-reader-selected.pdf differ diff --git a/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-reader-unselected.imageset/Contents.json b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-reader-unselected.imageset/Contents.json new file mode 100644 index 000000000000..44332fb41316 --- /dev/null +++ b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-reader-unselected.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "tab-bar-reader-unselected.pdf", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "tab-bar-reader-unselected-dark.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-reader-unselected.imageset/tab-bar-reader-unselected-dark.pdf b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-reader-unselected.imageset/tab-bar-reader-unselected-dark.pdf new file mode 100644 index 000000000000..ad4e40803f65 Binary files /dev/null and b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-reader-unselected.imageset/tab-bar-reader-unselected-dark.pdf differ diff --git a/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-reader-unselected.imageset/tab-bar-reader-unselected.pdf b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-reader-unselected.imageset/tab-bar-reader-unselected.pdf new file mode 100644 index 000000000000..f8f89ece67cf Binary files /dev/null and b/WordPress/Resources/AppImages.xcassets/Bottom Tabs/tab-bar-reader-unselected.imageset/tab-bar-reader-unselected.pdf differ diff --git a/WordPress/Resources/AppImages.xcassets/Domains/block-layout.imageset/Contents.json b/WordPress/Resources/AppImages.xcassets/Domains/block-layout.imageset/Contents.json new file mode 100644 index 000000000000..30c6d9fa68e2 --- /dev/null +++ b/WordPress/Resources/AppImages.xcassets/Domains/block-layout.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "block-layout.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WordPress/Resources/AppImages.xcassets/Domains/block-layout.imageset/block-layout.pdf b/WordPress/Resources/AppImages.xcassets/Domains/block-layout.imageset/block-layout.pdf new file mode 100644 index 000000000000..51183f1e4317 Binary files /dev/null and b/WordPress/Resources/AppImages.xcassets/Domains/block-layout.imageset/block-layout.pdf differ diff --git a/WordPress/Resources/AppImages.xcassets/New Jetpack prologue/JPBackground.imageset/Contents.json b/WordPress/Resources/AppImages.xcassets/New Jetpack prologue/JPBackground.imageset/Contents.json index 0ce038741595..f997bc669365 100644 --- a/WordPress/Resources/AppImages.xcassets/New Jetpack prologue/JPBackground.imageset/Contents.json +++ b/WordPress/Resources/AppImages.xcassets/New Jetpack prologue/JPBackground.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "JPBackground.pdf", + "filename" : "background-light.pdf", "idiom" : "universal" } ], diff --git a/WordPress/Resources/AppImages.xcassets/New Jetpack prologue/JPBackground.imageset/JPBackground.pdf b/WordPress/Resources/AppImages.xcassets/New Jetpack prologue/JPBackground.imageset/JPBackground.pdf deleted file mode 100644 index 1538f6f7c14c..000000000000 Binary files a/WordPress/Resources/AppImages.xcassets/New Jetpack prologue/JPBackground.imageset/JPBackground.pdf and /dev/null differ diff --git a/WordPress/Resources/AppImages.xcassets/New Jetpack prologue/JPBackground.imageset/background-light.pdf b/WordPress/Resources/AppImages.xcassets/New Jetpack prologue/JPBackground.imageset/background-light.pdf new file mode 100644 index 000000000000..663767edb6ad Binary files /dev/null and b/WordPress/Resources/AppImages.xcassets/New Jetpack prologue/JPBackground.imageset/background-light.pdf differ diff --git a/WordPress/Resources/AppImages.xcassets/block-tag-cloud.imageset/Contents.json b/WordPress/Resources/AppImages.xcassets/block-tag-cloud.imageset/Contents.json new file mode 100644 index 000000000000..737a43df6e96 --- /dev/null +++ b/WordPress/Resources/AppImages.xcassets/block-tag-cloud.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "block-tag-cloud.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/WordPress/Resources/AppImages.xcassets/block-tag-cloud.imageset/block-tag-cloud.pdf b/WordPress/Resources/AppImages.xcassets/block-tag-cloud.imageset/block-tag-cloud.pdf new file mode 100644 index 000000000000..2fb3c9efed36 Binary files /dev/null and b/WordPress/Resources/AppImages.xcassets/block-tag-cloud.imageset/block-tag-cloud.pdf differ diff --git a/WordPress/Resources/AppImages.xcassets/bloganuary-icon-page.imageset/Contents.json b/WordPress/Resources/AppImages.xcassets/bloganuary-icon-page.imageset/Contents.json new file mode 100644 index 000000000000..b948e1f83a2e --- /dev/null +++ b/WordPress/Resources/AppImages.xcassets/bloganuary-icon-page.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "bloganuary-icon-page.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/WordPress/Resources/AppImages.xcassets/bloganuary-icon-page.imageset/bloganuary-icon-page.pdf b/WordPress/Resources/AppImages.xcassets/bloganuary-icon-page.imageset/bloganuary-icon-page.pdf new file mode 100644 index 000000000000..de83d3086a4e Binary files /dev/null and b/WordPress/Resources/AppImages.xcassets/bloganuary-icon-page.imageset/bloganuary-icon-page.pdf differ diff --git a/WordPress/Resources/AppImages.xcassets/bloganuary-icon-people.imageset/Contents.json b/WordPress/Resources/AppImages.xcassets/bloganuary-icon-people.imageset/Contents.json new file mode 100644 index 000000000000..34fb916a0055 --- /dev/null +++ b/WordPress/Resources/AppImages.xcassets/bloganuary-icon-people.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "blognuary-icon-people.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/WordPress/Resources/AppImages.xcassets/bloganuary-icon-people.imageset/blognuary-icon-people.pdf b/WordPress/Resources/AppImages.xcassets/bloganuary-icon-people.imageset/blognuary-icon-people.pdf new file mode 100644 index 000000000000..bd3dbfb06a95 Binary files /dev/null and b/WordPress/Resources/AppImages.xcassets/bloganuary-icon-people.imageset/blognuary-icon-people.pdf differ diff --git a/WordPress/Resources/AppImages.xcassets/bloganuary-icon-verse.imageset/Contents.json b/WordPress/Resources/AppImages.xcassets/bloganuary-icon-verse.imageset/Contents.json new file mode 100644 index 000000000000..849eed1d57f2 --- /dev/null +++ b/WordPress/Resources/AppImages.xcassets/bloganuary-icon-verse.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "bloganuary-icon-verse.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/WordPress/Resources/AppImages.xcassets/bloganuary-icon-verse.imageset/bloganuary-icon-verse.pdf b/WordPress/Resources/AppImages.xcassets/bloganuary-icon-verse.imageset/bloganuary-icon-verse.pdf new file mode 100644 index 000000000000..28cfb92eb067 Binary files /dev/null and b/WordPress/Resources/AppImages.xcassets/bloganuary-icon-verse.imageset/bloganuary-icon-verse.pdf differ diff --git a/WordPress/Resources/AppImages.xcassets/comment-author-gravatar.imageset/Contents.json b/WordPress/Resources/AppImages.xcassets/comment-author-gravatar.imageset/Contents.json new file mode 100644 index 000000000000..8ca0159c916c --- /dev/null +++ b/WordPress/Resources/AppImages.xcassets/comment-author-gravatar.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "comment-author-gravatar.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/WordPress/Resources/AppImages.xcassets/comment-author-gravatar.imageset/comment-author-gravatar.pdf b/WordPress/Resources/AppImages.xcassets/comment-author-gravatar.imageset/comment-author-gravatar.pdf new file mode 100644 index 000000000000..9b99c36755c7 Binary files /dev/null and b/WordPress/Resources/AppImages.xcassets/comment-author-gravatar.imageset/comment-author-gravatar.pdf differ diff --git a/WordPress/Resources/AppImages.xcassets/home.imageset/Contents.json b/WordPress/Resources/AppImages.xcassets/home.imageset/Contents.json new file mode 100644 index 000000000000..346fd4f0038b --- /dev/null +++ b/WordPress/Resources/AppImages.xcassets/home.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "block-home.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/WordPress/Resources/AppImages.xcassets/home.imageset/block-home.pdf b/WordPress/Resources/AppImages.xcassets/home.imageset/block-home.pdf new file mode 100644 index 000000000000..ebbcad2cdae7 Binary files /dev/null and b/WordPress/Resources/AppImages.xcassets/home.imageset/block-home.pdf differ diff --git a/WordPress/Resources/AppImages.xcassets/icon-people.imageset/Contents.json b/WordPress/Resources/AppImages.xcassets/icon-people.imageset/Contents.json new file mode 100644 index 000000000000..c6cd050129de --- /dev/null +++ b/WordPress/Resources/AppImages.xcassets/icon-people.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "icon-people.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WordPress/Resources/AppImages.xcassets/icon-people.imageset/icon-people.pdf b/WordPress/Resources/AppImages.xcassets/icon-people.imageset/icon-people.pdf new file mode 100644 index 000000000000..93c3f10e3b58 Binary files /dev/null and b/WordPress/Resources/AppImages.xcassets/icon-people.imageset/icon-people.pdf differ diff --git a/WordPress/Resources/AppImages.xcassets/logo-bloganuary-large.imageset/Contents.json b/WordPress/Resources/AppImages.xcassets/logo-bloganuary-large.imageset/Contents.json new file mode 100644 index 000000000000..30a38a27b61a --- /dev/null +++ b/WordPress/Resources/AppImages.xcassets/logo-bloganuary-large.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "logo-bloganuary-large.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WordPress/Resources/AppImages.xcassets/logo-bloganuary-large.imageset/logo-bloganuary-large.pdf b/WordPress/Resources/AppImages.xcassets/logo-bloganuary-large.imageset/logo-bloganuary-large.pdf new file mode 100644 index 000000000000..d083793dc79b Binary files /dev/null and b/WordPress/Resources/AppImages.xcassets/logo-bloganuary-large.imageset/logo-bloganuary-large.pdf differ diff --git a/WordPress/Resources/AppImages.xcassets/logo-bloganuary.imageset/Contents.json b/WordPress/Resources/AppImages.xcassets/logo-bloganuary.imageset/Contents.json new file mode 100644 index 000000000000..4d3ca8dcd5a0 --- /dev/null +++ b/WordPress/Resources/AppImages.xcassets/logo-bloganuary.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "logo-bloganuary.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/WordPress/Resources/AppImages.xcassets/logo-bloganuary.imageset/logo-bloganuary.svg b/WordPress/Resources/AppImages.xcassets/logo-bloganuary.imageset/logo-bloganuary.svg new file mode 100644 index 000000000000..87debb9a3124 --- /dev/null +++ b/WordPress/Resources/AppImages.xcassets/logo-bloganuary.imageset/logo-bloganuary.svg @@ -0,0 +1,3 @@ + + + diff --git a/WordPress/Resources/AppImages.xcassets/posts.imageset/Contents.json b/WordPress/Resources/AppImages.xcassets/posts.imageset/Contents.json new file mode 100644 index 000000000000..7ac3a4229e83 --- /dev/null +++ b/WordPress/Resources/AppImages.xcassets/posts.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "site-menu-posts.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/WordPress/Resources/AppImages.xcassets/posts.imageset/site-menu-posts.pdf b/WordPress/Resources/AppImages.xcassets/posts.imageset/site-menu-posts.pdf new file mode 100644 index 000000000000..d702263b23ed Binary files /dev/null and b/WordPress/Resources/AppImages.xcassets/posts.imageset/site-menu-posts.pdf differ diff --git a/WordPress/Resources/AppImages.xcassets/subdirectory.imageset/Contents.json b/WordPress/Resources/AppImages.xcassets/subdirectory.imageset/Contents.json new file mode 100644 index 000000000000..40e8b4710185 --- /dev/null +++ b/WordPress/Resources/AppImages.xcassets/subdirectory.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "subdirectory.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/WordPress/Resources/AppImages.xcassets/subdirectory.imageset/subdirectory.pdf b/WordPress/Resources/AppImages.xcassets/subdirectory.imageset/subdirectory.pdf new file mode 100644 index 000000000000..5a53fc97c97e Binary files /dev/null and b/WordPress/Resources/AppImages.xcassets/subdirectory.imageset/subdirectory.pdf differ diff --git a/WordPress/Resources/AppStoreStrings.po b/WordPress/Resources/AppStoreStrings.po index af8ea867ced4..fd57ef1a04d7 100644 --- a/WordPress/Resources/AppStoreStrings.po +++ b/WordPress/Resources/AppStoreStrings.po @@ -45,15 +45,13 @@ msgctxt "app_store_keywords" msgid "blogger,writing,blogging,web,maker,online,store,business,make,create,write,blogs" msgstr "" -msgctxt "v23.5-whats-new" +msgctxt "v23.9-whats-new" msgid "" -"In the block editor, you can now split or exit a formatted block by pressing the “enter” key three times. The left-hand border is always visible for quote blocks, too. And you can quote us on that.\n" +"We updated the classic editor with new media pickers for Photos and Site Media. Don’t worry, you can still upload images, videos, and more to your site.\n" "\n" -"We also squashed a handful of bugs.\n" +"Speaking of media types—you can now add media filters to the Site Media screen. If you’re using an iPhone, you’ll notice the new aspect ratio mode, too. Both options are available when you tap the title menu.\n" "\n" -"- We fixed a code issue in blogging prompt settings that caused the app to crash.\n" -"- During the signup process, you won’t end up in the reader by accident.\n" -"- Creating a .com site? You should no longer see two overlays after completing the signup process. One and done.\n" +"Finally, we fixed the broken compliance pop-up that appears while you’re checking stats during the onboarding process. Sweet.\n" msgstr "" #. translators: This is a standard chunk of text used to tell a user what's new with a release when nothing major has changed. diff --git a/WordPress/Resources/ar.lproj/Localizable.strings b/WordPress/Resources/ar.lproj/Localizable.strings index 97ff4cd33e79..9e84947c5d14 100644 --- a/WordPress/Resources/ar.lproj/Localizable.strings +++ b/WordPress/Resources/ar.lproj/Localizable.strings @@ -1,4 +1,4 @@ -/* Translation-Revision-Date: 2023-10-18 14:54:08+0000 */ +/* Translation-Revision-Date: 2024-01-03 15:54:27+0000 */ /* Plural-Forms: nplurals=6; plural=(n == 0) ? 0 : ((n == 1) ? 1 : ((n == 2) ? 2 : ((n % 100 >= 3 && n % 100 <= 10) ? 3 : ((n % 100 >= 11 && n % 100 <= 99) ? 4 : 5)))); */ /* Generator: GlotPress/4.0.0-alpha.11 */ /* Language: ar */ @@ -152,9 +152,6 @@ /* Difference label for Period Overview stat, indicating change from previous period. Ex: +99.9K (5%) */ "%@%@ (%@%%)" = "%1$@%2$@ (%3$@%%)"; -/* Accessibility label for a post in the post list. The parameters are the title, and date respectively. For example, \"Let it Go, 1 hour ago.\" */ -"%@, %@." = "%1$@، %2$@."; - /* Accessibility value for a Stats' Posting Activity Month if the user selected a day with posts. The first parameter is day (e.g. November 2019). The second parameter is the number of posts. */ "%@. %d posts." = "%1$@. %2$d من المقالات."; @@ -210,9 +207,6 @@ /* Age between dates over one year. */ "%d years" = "%d من السنوات"; -/* Max image size in pixels (e.g. 300x300px) */ -"%dx%dpx" = "%1$d×%2$d بكسل"; - /* One menu area available in the theme */ "%i menu area in this theme" = "%i من مكان القائمة في هذا القالب"; @@ -271,6 +265,9 @@ translators: Block name. %s: The localized block name */ /* translators: %s: social link name e.g: \"Instagram\". */ "%s social icon" = "أيقونة %s الاجتماعية"; +/* translators: displayed right after the classic block is converted to blocks. %s: The localized classic block name */ +"'%s' block converted to blocks" = "تم تحويل مكوّن \"%s\" إلى المكوّنات"; + /* translators: Missing block alert title. %s: The localized block name */ "'%s' is not fully-supported" = "لم يتم دعم \"%s\" بالكامل"; @@ -478,13 +475,6 @@ translators: Block name. %s: The localized block name */ /* Label for the Activity Type filter button when there are more than 1 activity type selected */ "Activity Type (%1$d)" = "نوع النشاط (%1$d)"; -/* Accessibility label for add button to add items to the user's media library - Remove asset from media picker list */ -"Add" = "إضافة"; - -/* Action for Media Picker to indicate selection of media. The argument in the string represents the number of elements (as numeric digits) selected */ -"Add %@" = "إضافة %@"; - /* No comment provided by engineer. */ "Add Block After" = "إضافة مكوّن بعد"; @@ -581,9 +571,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Add menu item to children" = "إضافة عنصر قائمة للعناصر الفرعية"; -/* Accessibility hint for add button to add items to the user's media library */ -"Add new media" = "إضافة وسائط جديدة"; - /* Screen reader text for Menus button that adds a new menu to a site. */ "Add new menu" = "إضافة قائمة جديدة"; @@ -649,9 +636,6 @@ translators: Block name. %s: The localized block name */ /* Option to select the Airmail app when logging in with magic links */ "Airmail" = "Airmail"; -/* Description of albums in the photo libraries */ -"Albums" = "الألبومات"; - /* Image alignment option title. Title of the screen for choosing an image's alignment. */ "Alignment" = "محاذاة"; @@ -665,6 +649,9 @@ translators: Block name. %s: The localized block name */ Title of the drafts filter. This filter shows a list of draft posts. */ "All" = "الكل"; +/* Information about redeeming domain credit on site dashboard. */ +"All WordPress.com annual plans include a custom domain name. Register your free domain now." = "تتضمن جميع خطط ووردبريس.كوم السنوية اسم نطاق مخصصًا. سجِّل نطاقك المجاني الآن."; + /* Footer of the free domain registration section for a paid plan. Information about redeeming domain credit on site dashboard. */ "All WordPress.com plans include a custom domain name. Register your free premium domain now." = "تتضمن جميع خطط WordPress.com اسم نطاق مخصصًا. قم بتسجيل نطاقك المتميز المجاني الآن."; @@ -730,10 +717,13 @@ translators: Block name. %s: The localized block name */ "Alt Text" = "النص البديل"; /* No comment provided by engineer. */ -"Alternatively, you can detach and edit these blocks separately by tapping “Detach patterns”." = "وبدلاً من ذلك، يمكنك فصل هذه المكوّنات وتحريرها على حدة عن طريق الضغط على \"فصل الأنماط\"."; +"Alternatively, you can convert the content to blocks." = "بدلاً من ذلك، يمكنك تحويل المحتوى إلى مكوّنات."; /* No comment provided by engineer. */ -"Alternatively, you can detach and edit this block separately by tapping “Detach pattern”." = "وبدلاً من ذلك، يمكنك فصل هذا المكوّن وتحريره على حدة عن طريق النقر على \"فصل النمط\"."; +"Alternatively, you can detach and edit this block separately by tapping “Detach”." = "بدلاً من ذلك، يمكنك فصل هذا المكوّن وتحريره على حدة عن طريق النقر على \"فصل\"."; + +/* translators: Alternative option included in a warning related to having blocks deeply nested. */ +"Alternatively, you can flatten the content by ungrouping the block." = "بدلاً من ذلك، يمكنك تمهيد المحتوى عن طريق فك تجميع المكوّن."; /* Instruction text to explain to help users type their password instead of using magic link login option. */ "Alternatively, you may enter the password for this account." = "بدلاً من ذلك، قد تقوم بإدخال كلمة مرور هذا الحساب."; @@ -882,15 +872,9 @@ translators: Block name. %s: The localized block name */ /* Message prompting the user to confirm that they want to disconnect Jetpack from the site. */ "Are you sure you want to disconnect Jetpack from the site?" = "هل تريد بالتأكيد قطع اتصال Jetpack عن هذا الموقع؟"; -/* Message prompting the user to confirm that they want to permanently delete a group of media items. */ -"Are you sure you want to permanently delete these items?" = "هل تريد بالتأكيد حذف هذه العناصر نهائيًّا؟"; - /* Message prompting the user to confirm that they want to permanently delete a media item. Should match Calypso. */ "Are you sure you want to permanently delete this item?" = "هل تريد بالتأكيد حذف هذا العنصر نهائيًا؟"; -/* Message of the confirmation alert when deleting a page from the trash. */ -"Are you sure you want to permanently delete this page?" = "هل أنت متأكد من رغبتك في حذف هذه الصفحة نهائيًا؟"; - /* Message of the confirmation alert when deleting a post from the trash. */ "Are you sure you want to permanently delete this post?" = "هل أنت متأكد من رغبتك في حذف هذه المقالة نهائيًا؟"; @@ -922,9 +906,6 @@ translators: Block name. %s: The localized block name */ /* Title of message shown when user taps submit for review. */ "Are you sure you want to submit for review?" = "هل تريد بالتأكيد الإرسال للمراجعة؟"; -/* Message of the trash page confirmation alert. */ -"Are you sure you want to trash this page?" = "هل أنت متأكد من رغبتك في نقل هذه الصفحة إلى سلة المهملات؟"; - /* Message of the trash confirmation alert. */ "Are you sure you want to trash this post?" = "هل أنت متأكد من رغبتك في نقل هذه المقالة إلى سلة المهملات؟"; @@ -965,9 +946,6 @@ translators: Block name. %s: The localized block name */ /* translators: accessibility text. Empty Audio caption. */ "Audio caption. Empty" = "تسمية الملف الصوتي. فارغة"; -/* Accessibility label for audio items in the media collection view. The parameter is the creation date of the audio. */ -"Audio, %@" = "الصوت، %@"; - /* No comment provided by engineer. */ "Authenticating" = "مصادقة"; @@ -1178,10 +1156,7 @@ translators: Block name. %s: The localized block name */ "Blocks menu" = "قائمة المكوّنات"; /* translators: Warning related to having blocks deeply nested. %d: The deepest nesting level. */ -"Blocks nested deeper than %d levels may not render properly in the mobile editor. For this reason, we recommend flattening the content by ungrouping the block or editing the block using the web editor." = "قد لا تظهر المكوّنات المتداخلة بشكل أكثر عمقًا من %d من المستويات بشكل صحيح في محرر الهاتف المحمول. لهذا السبب، نوصي بتهيئة المحتوى عن طريق إلغاء تجميع المكوّن أو تحرير المكوّن باستخدام محرر الويب."; - -/* translators: Warning related to having blocks deeply nested. %d: The deepest nesting level. */ -"Blocks nested deeper than %d levels may not render properly in the mobile editor. For this reason, we recommend flattening the content by ungrouping the block or editing the block using your web browser." = "قد لا تظهر المكوّنات المتداخلة بشكل أكثر عمقًا من %d من المستويات بشكل صحيح في محرر الهاتف المحمول. لهذا السبب، نوصي بتهيئة المحتوى عن طريق إلغاء تجميع المكوّن أو تحرير المكوّن باستخدام متصفح الويب لديك."; +"Blocks nested deeper than %d levels may not render properly in the mobile editor." = "قد لا تظهر المكوّنات المتداخلة بشكل أكثر عمقًا من %d من المستويات بشكل صحيح في محرر الهاتف المحمول."; /* Title of a button that displays the WordPress.com blog */ "Blog" = "مدونة"; @@ -1259,9 +1234,6 @@ translators: Block name. %s: The localized block name */ /* Label for the post author in the post detail. */ "By " = "بواسطة "; -/* Accessibility label for the post author in the post list. The parameter is the author name. For example, \"By Elsa.\" */ -"By %@." = "بواسطة %@."; - /* Legal disclaimer for logging in. The underscores _..._ denote underline. */ "By continuing, you agree to our _Terms of Service_." = "بالمتابعة، فإنك توافق على _شروط الخدمة_ الخاصة بنا."; @@ -1281,8 +1253,7 @@ translators: Block name. %s: The localized block name */ /* Label for size of media while it's being calculated. */ "Calculating..." = "جاري الحساب..."; -/* Accessibility label for taking an image or video with the camera on formatting toolbar. - Accessibility label for the camera tile in the collection view */ +/* Accessibility label for taking an image or video with the camera on formatting toolbar. */ "Camera" = "كاميرا"; /* Title of an alert informing the user the camera permission for the app is disabled and its needed to proceed */ @@ -1333,10 +1304,7 @@ translators: Block name. %s: The localized block name */ Cancels an Action Cancels an alert. Cancels the mark all as read action. - Dismiss the post action sheet Editing GIF alert cancel action button. - Label for a cancel button - Label for the auto-upload cancelation button in the post list. Tapping will prevent the app from auto-uploading the post. Menus cancel button for deleting a menu. Menus cancel button within text bar while editing items. Menus: Cancel button title for canceling an edited menu item. @@ -1353,10 +1321,6 @@ translators: Block name. %s: The localized block name */ Verb. Title for Jetpack Restore cancel button. */ "Cancel" = "إلغاء"; -/* Label for the Post List option that cancels automatic uploading of a post. - Media Library option to cancel an in-progress or failed upload. */ -"Cancel Upload" = "إلغاء الرفع"; - /* Menus alert button title to cancel discarding changes and not createa a new menu. Menus alert button title to cancel discarding changes and not select a new menu Menus alert button title to cancel discarding changes and not select a new menu location */ @@ -1415,9 +1379,6 @@ translators: Block name. %s: The localized block name */ Main title */ "Change Password" = "تغيير كلمة المرور"; -/* Title of button that takes user to the system Settings section for the app */ -"Change Settings" = "تغيير الإعدادات"; - /* Change Username title. */ "Change Username" = "تغيير اسم المستخدم"; @@ -1557,9 +1518,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Choose file" = "اختيار ملف"; -/* Menu option for selecting media from the device's photo library. */ -"Choose from My Device" = "الاختيار من جهازي"; - /* Explanatory text for Homepage Settings homepage type selection. */ "Choose from a homepage that displays your latest posts (classic blog) or a fixed \/ static page." = "اختر من صفحة رئيسية تعرض أحدث مقالاتك (مدونة تقليدية) أو صفحة ثابتة\/ساكنة."; @@ -1760,9 +1718,6 @@ translators: Block name. %s: The localized block name */ /* Community & Non-Profit site intent topic */ "Community & Non-Profit" = "المجتمع وغير ربحي"; -/* Accessibility indication that the current Post List style is currently Compact. */ -"Compact" = "صغير"; - /* The action is completed */ "Completed" = "اكتمال"; @@ -1948,10 +1903,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Copied block" = "المكوِّن الذي تم نسخه"; -/* Copy the post url and paste anywhere in phone - Label for page copy link. Tapping copy the url of page */ -"Copy Link" = "نسخ الرابط"; - /* Copy link to oomment button title */ "Copy Link to Comment" = "نسخ الرابط إلى التعليق"; @@ -2060,9 +2011,6 @@ translators: Block name. %s: The localized block name */ /* Error title displayed when unable to close user account. */ "Couldn’t close account automatically" = "تعذّر إغلاق الحساب تلقائياً"; -/* Message to show while media data source is finding the number of items available. */ -"Counting media items..." = "جاري حساب عناصر الوسائط..."; - /* Period Stats 'Countries' header */ "Countries" = "الدول"; @@ -2313,7 +2261,6 @@ translators: Block name. %s: The localized block name */ /* Delete Delete button title for the warning shown to the user when he refuses to re-login when the authToken is missing. Title for button that permanently deletes a media item (photo / video) - Title for button that permanently deletes one or more media items (photos / videos) Title of a delete button Title of the trash confirmation alert. */ "Delete" = "حذف"; @@ -2321,15 +2268,11 @@ translators: Block name. %s: The localized block name */ /* Menus confirmation button for deleting a menu. */ "Delete Menu" = "حذف القائمة"; -/* Delete option in the confirmation alert when deleting a page from the trash. - Delete option in the confirmation alert when deleting a post from the trash. - Label for a button permanently deletes a page. - Label for the delete post option. Tapping permanently deletes a post. +/* Delete option in the confirmation alert when deleting a post from the trash. Title for button on the comment details page that deletes the comment when tapped. */ "Delete Permanently" = "حذف بشكل دائم"; -/* Title of the confirmation alert when deleting a page from the trash. - Title of the confirmation alert when deleting a post from the trash. */ +/* Title of the confirmation alert when deleting a post from the trash. */ "Delete Permanently?" = "هل تريد الحذف نهائيًا؟"; /* Button label for deleting the current site @@ -2448,7 +2391,6 @@ translators: Block name. %s: The localized block name */ /* Accessibility label for button to dismiss a bottom sheet Accessibility label for the transparent space above the login dialog which acts as a button to dismiss the dialog. Accessibility label for the transparent space above the signup dialog which acts as a button to dismiss the dialog. - Action to show on alert when view asset fails. Dismiss a view. Verb */ "Dismiss" = "تجاهل"; @@ -2466,12 +2408,6 @@ translators: Block name. %s: The localized block name */ User's Display Name */ "Display Name" = "اسم العرض"; -/* Accessibility label for other media items in the media collection view. The parameter is the creation date of the document. */ -"Document, %@" = "المستند، %@"; - -/* Accessibility label for other media items in the media collection view. The parameter is the filename file. */ -"Document: %@" = "المستند: %@"; - /* Message shown when all tours have been completed */ "Doesn't it feel good to cross things off a list?" = "أليس من الأفضل حذف أشياء من القائمة؟"; @@ -2632,8 +2568,7 @@ translators: Block name. %s: The localized block name */ /* Description of a Quick Start Tour */ "Draft and publish a post." = "اكتب مسودّة مقالة ثم أنشرها."; -/* Title of the drafts filter. This filter shows a list of draft posts. - Title of the drafts header in search list. */ +/* Title of the drafts filter. This filter shows a list of draft posts. */ "Drafts" = "المسودات"; /* No comment provided by engineer. */ @@ -2645,10 +2580,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Drag to adjust focal point" = "السحب لضبط نقطة التركيز"; -/* Label for page duplicate option. Tapping creates a copy of the page. - Label for post duplicate option. Tapping creates a copy of the post. */ -"Duplicate" = "تكرار"; - /* No comment provided by engineer. */ "Duplicate block" = "تكرار المكوِّن"; @@ -2661,13 +2592,9 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Each block has its own settings. To find them, tap on a block. Its settings will appear on the toolbar at the bottom of the screen." = "لكل مكوِّن الإعدادات الخاصة به. للعثور عليها، أنقر على مكوِّن. ستظهر إعداداته على شريط الأدوات في الجزء السفلي من الشاشة."; -/* Accessibility label for edit button to enable multi selection mode in the user's media library - Edit the post. - Editing GIF alert default action button. +/* Editing GIF alert default action button. Edits a Comment Edits the comment - Label for a button that opens the Edit Page view controller - Label for the edit post button. Tapping displays the editor. User action to edit media details. Verb, edit a comment */ "Edit" = "تحرير"; @@ -2675,9 +2602,6 @@ translators: Block name. %s: The localized block name */ /* Title for the edit more button section */ "Edit \"More\" button" = "تحرير زر \"المزيد\""; -/* Button that displays the media editor to the user */ -"Edit %@" = "تحرير %@"; - /* Blocklist Keyword Edition Title */ "Edit Blocklist Word" = "تحرير كلمة قائمة الحظر"; @@ -2870,9 +2794,6 @@ translators: Block name. %s: The localized block name */ /* Secondary message shown when there are no domains that match the user entered text. */ "Enter different words above and we'll look for an address that matches it." = "أدخل كلمات مختلفة أعلاه وسنبحث عن عنوان يطابقها."; -/* Accessibility hint for edit button to enable multi selection mode in the user's media library */ -"Enter edit mode to enable multi select to delete" = "دخول وضع التحرير لتمكين التحديد المتعدد للحذف"; - /* (placeholder) Help enter WordPress password Placeholder of a field to type a password to protect the post. */ "Enter password" = "أدخل كلمة المرور"; @@ -3028,9 +2949,6 @@ translators: Block name. %s: The localized block name */ /* Short title telling the user they will receive a blogging reminder every day of the week. */ "Every day at %@" = "كل يوم في %@"; -/* Label for the post author filter. This filter shows posts for all users on the blog. */ -"Everyone" = "الجميع"; - /* Example story title description */ "Example story title" = "مثال لعنوان قصة"; @@ -3040,9 +2958,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Excerpt length (words)" = "طول المقتطف (كلمات)"; -/* Accessibility label for a post's excerpt in the post list. The parameter is the post excerpt. For example, \"Excerpt. This is the first paragraph.\" */ -"Excerpt. %@." = "المقتطف. %@."; - /* Should be the same as the text displayed if the user clicks the (i) in Calypso. */ "Excerpts are optional hand-crafted summaries of your content." = "المقتطفات هي ملخصات يدوية اختيارية لمحتواك."; @@ -3052,8 +2967,7 @@ translators: Block name. %s: The localized block name */ /* Accessibility Label for the exit full screen button on the full screen comment reply mode */ "Exit Full Screen" = "إنهاء وضع ملء الشاشة"; -/* Accessibility indication that the current Post List style is currently Expanded. - Screen reader text to represent the expanded state of a UI control */ +/* Screen reader text to represent the expanded state of a UI control */ "Expanded" = "موسعة"; /* Accessibility hint */ @@ -3103,9 +3017,6 @@ translators: Block name. %s: The localized block name */ The action failed */ "Failed" = "فشل"; -/* Error title when picked media cannot be imported into stories. */ -"Failed Media Export" = "فشل تصدير الوسائط"; - /* Message for mark all as read success notice */ "Failed marking Notifications as read" = "فشل وضع علامة على التنبيهات بأنها مقروءة"; @@ -3307,6 +3218,12 @@ translators: Block name. %s: The localized block name */ /* An example tag used in the login prologue screens. */ "Football" = "كرة القدم"; +/* translators: Recommendation included in a warning related to having blocks deeply nested. */ +"For this reason, we recommend editing the block using the web editor." = "لهذا السبب، نوصي بتحرير المكوّن باستخدام محرر الويب."; + +/* translators: Recommendation included in a warning related to having blocks deeply nested. */ +"For this reason, we recommend editing the block using your web browser." = "لهذا السبب، نوصي بتحرير المكوّن باستخدام متصفح الويب لديك."; + /* Register Domain - Domain contact information section header description */ "For your convenience, we have pre-filled your WordPress.com contact information. Please review to be sure it’s the correct information you want to use for this domain." = "قمنا بتعبئة معلومات جهات الاتصال الخاصة بك مسبقًا على حسابك في WordPress.com من أجل راحتك. يرجى المراجعة للتأكد من صحة المعلومات التي ترغب في استخدامها لهذا النطاق."; @@ -3624,8 +3541,7 @@ translators: Block name. %s: The localized block name */ "Home" = "الرئيسية"; /* Title for setting which shows the current page assigned as a site's homepage - Title for the homepage section in site settings screen - Title of the Homepage Badge */ + Title for the homepage section in site settings screen */ "Homepage" = "الصفحة الرئيسية"; /* Label for Homepage Settings site settings section @@ -3722,9 +3638,6 @@ translators: Block name. %s: The localized block name */ /* Hint for image title on image settings. */ "Image title" = "عنوان الصورة"; -/* Accessibility label for image thumbnails in the media collection view. The parameter is the creation date of the image. */ -"Image, %@" = "الصورة %@"; - /* Undated post time label */ "Immediately" = "حالاً"; @@ -4210,9 +4123,6 @@ translators: Block name. %s: The localized block name */ Settings: Comments Approval settings */ "Links in comments" = "الروابط في التعليقات"; -/* The accessibility label for the list style button in the Post List. */ -"List style" = "نمط القائمة"; - /* Title of the screen that load selected the revisions. */ "Load" = "تحميل"; @@ -4228,18 +4138,12 @@ translators: Block name. %s: The localized block name */ /* Text displayed while loading the activity feed for a site */ "Loading Backups..." = "جاري تحميل النسخ الاحتياطية..."; -/* Phrase to show when the user has searched for GIFs and they are being loaded. */ -"Loading GIFs..." = "جاري تحميل صور GIF..."; - /* Menus label text displayed while menus are loading */ "Loading Menus..." = "جاري تحميل القوائم..."; /* Text displayed while loading site People. */ "Loading People..." = "جاري تحميل الأشخاص..."; -/* Phrase to show when the user has searched for images and they are being loaded. */ -"Loading Photos..." = "جاري تحميل الصورة..."; - /* Text displayed while loading plans details */ "Loading Plan..." = "جاري تحميل الخطة..."; @@ -4300,8 +4204,7 @@ translators: Block name. %s: The localized block name */ /* Local Services site intent topic */ "Local Services" = "الخدمات المحلية"; -/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. - Title of the Local Changes Badge */ +/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. */ "Local changes" = "التغييرات المحلية"; /* Title for alert when a generic error happened when trying to find the location of the device */ @@ -4465,7 +4368,6 @@ translators: Block name. %s: The localized block name */ "Max Video Upload Size" = "أقصى حجم لمقطع الفيديو الذي سيتم رفعه"; /* Accessibility label for the Me button in My Site. - Label for the post author filter. This filter shows posts only authored by the current user. Me page title Noun. Title. Links to the Me screen. The accessibility value of the me tab. @@ -4473,9 +4375,7 @@ translators: Block name. %s: The localized block name */ "Me" = "أنا"; /* Noun. Title. Links to the blog's Media library. - Tab bar title for the Media tab in Media Picker The menu item to select during a guided tour. - Title for Media Library section of the app. Title for the media section in site settings screen Title label for the media settings section in the app settings */ "Media" = "وسائط"; @@ -4487,13 +4387,6 @@ translators: Block name. %s: The localized block name */ /* Label for size of media cache in the app. */ "Media Cache Size" = "حجم ذاكرة التخزين المؤقت للوسائط"; -/* Title for alert when access to media capture is not granted */ -"Media Capture" = "التقاط الوسائط"; - -/* Title for alert when a generic error happened when loading media - Title for alert when access to the media library is not granted by the user */ -"Media Library" = "مكتبة الوسائط"; - /* Title for action sheet with media options. */ "Media Options" = "خيارات الوسائط"; @@ -4516,9 +4409,6 @@ translators: Block name. %s: The localized block name */ /* translators: %s: block title e.g: \"Paragraph\". */ "Media options" = "خيارات الوسائط"; -/* Alert title when there is issues loading an asset to preview. */ -"Media preview failed." = "فشل معاينة الوسائط."; - /* Alert displayed to the user when multiple media items have uploaded successfully. */ "Media uploaded (%ld files)" = "الوسائط المرفوعة (%ld الملفات)"; @@ -4556,9 +4446,6 @@ translators: Block name. %s: The localized block name */ Label for the share message field on the post settings. */ "Message" = "رسالة"; -/* Title of section containing image / video metadata such as size and file type */ -"Metadata" = "بيانات التعريف"; - /* Option to select the Microsft Outlook app when logging in with magic links */ "Microsoft Outlook" = "Microsoft Outlook"; @@ -4578,13 +4465,11 @@ translators: Block name. %s: The localized block name */ "Months and Years" = "شهور وسنوات"; /* Accessibility label for more button in dashboard quick start card. - Accessibility label for the More button in Page List. Accessibility label for the More button in Post List (compact view). Accessibility label for the More button on formatting toolbar. Accessibility label for the More button on Reader Cell Accessibility label for the More button on Reader's post details - Action button to display more available options - Label for the more post button. Tapping displays an action sheet with post options. */ + Action button to display more available options */ "More" = "المزيد"; /* Action button to display more available options @@ -4642,15 +4527,8 @@ translators: Block name. %s: The localized block name */ /* Screen reader text for button that will move the menu item */ "Move menu item" = "نقل عنصر القائمة"; -/* Label for a button that moves a page to the draft folder - Label for an option that moves a post to the draft folder */ -"Move to Draft" = "نقل إلى المسودة"; - -/* Label for a button that moves a page to the trash folder - Label for a option that moves a post to the trash folder - Title for button on the comment details page that moves the comment to trash when tapped. +/* Title for button on the comment details page that moves the comment to trash when tapped. Trash option in the trash confirmation alert. - Trash option in the trash page confirmation alert. Trashes the comment */ "Move to Trash" = "نقل إلى سلة المهملات"; @@ -4682,7 +4560,8 @@ translators: Block name. %s: The localized block name */ Title of My Site tab */ "My Site" = "موقعي"; -/* Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ +/* Title for site picker screen. + Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ "My Sites" = "مواقعي"; /* Siri Suggestion to open My Sites */ @@ -4932,9 +4811,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title for the view when there aren't any Activities to display in the Activity Log for a given filter. */ "No matching events found." = "لم يتم العثور على أحداث متطابقة."; -/* Message displayed when no results are returned from a media library search. Should match Calypso. - Phrase to show when the user search for images but there are no result to show. - Phrase to show when the user searches for GIFs but there are no result to show. */ +/* Message displayed when no results are returned from a media library search. Should match Calypso. */ "No media matching your search" = "لا توجد وسائط مطابقة لبحثك"; /* Menus text shown when no menus were available for loading the Menus editor. */ @@ -4952,8 +4829,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Displayed in the Notifications Tab as a title, when there are no notifications */ "No notifications yet" = "لا توجد إشعارات حتى الآن"; -/* Displayed when the user is searching the pages list and there are no matching pages - Text displayed when there's no matching with the text search */ +/* Text displayed when there's no matching with the text search */ "No pages matching your search" = "لا تتطابق أي صفحات مع بحثك"; /* Text displayed when search for plugins returns no results */ @@ -4974,9 +4850,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Message shown whent the reader finds no posts for the chosen tag */ "No posts have been made recently with this tag." = "لم يتم نشر أي مقالات مؤخرًا بهذا الوسم."; -/* Displayed when the user is searching the posts list and there are no matching posts */ -"No posts matching your search" = "لا تتطابق أي مقالة مع بحثك"; - /* Accessibility value for a Stats' Posting Activity Month if there are no posts. */ "No posts." = "لا توجد مقالات."; @@ -5077,9 +4950,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* A message title */ "Nothing liked yet" = "لم ينل إعجابي شيء حتي الآن"; -/* Default message for empty media picker */ -"Nothing to show" = "لا يوجد شيء لعرضه"; - /* Notifications Details Accessibility Identifier */ "Notification Details Table" = "جدول تفاصيل التنبيهات"; @@ -5139,7 +5009,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Alert dismissal title Button title. Acknowledges a prompt. Button title. An acknowledgement of the message displayed in a prompt. - Confirmation of action Default action Dismisses the alert Marks all notifications as read. @@ -5201,9 +5070,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* No comment provided by engineer. */ "Only show excerpt" = "إظهار المقتطف فقط"; -/* Message telling the user that they've only enabled limited photo library permissions for the app. */ -"Only the selected photos you've given access to are available." = "تتوافر فقط الصور المُحدَّدة التي منحت حق الوصول إليها."; - /* An informal exclaimation that means `something went wrong`. Title for the view when there's an error loading a comment. Title for the view when there's an error loading Activity Log @@ -5238,9 +5104,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title of a button that opens the apps settings in the system Settings.app */ "Open Settings" = "فتح الإعدادات"; -/* Editor button to swich the media picker from quick mode to full picker */ -"Open full media picker" = "فتح أداة انتقاء الوسائط بالكامل"; - /* No comment provided by engineer. */ "Open in Safari" = "فتح في Safari"; @@ -5280,6 +5143,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Divider on initial auth view separating auth options. */ "Or" = "أو"; +/* Instruction text for other forms of two-factor auth methods. */ +"Or choose another form of authentication." = "أو اختر نموذج مصادقة آخر."; + /* Label for button to log in using site address. Underscores _..._ denote underline. */ "Or log in by _entering your site address_." = "أو تسجيل الدخول عن طريق _إدخال عنوان رابط موقعك_."; @@ -5338,15 +5204,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title shown when selecting a post type of Page from the Share Extension. */ "Page" = "الصفحة"; -/* Prompts the user that a restored page was moved to the drafts list. */ -"Page Restored to Drafts" = "تمت استعادة الصفحة إلى مسودات"; - -/* Prompts the user that a restored page was moved to the published list. */ -"Page Restored to Published" = "تمت استعادة الصفحة إلى منشور"; - -/* Prompts the user that a restored page was moved to the scheduled list. */ -"Page Restored to Scheduled" = "تمت استعادة الصفحة إلى مجدول"; - /* Name of the button to open the page settings The title of the Page Settings screen. */ "Page Settings" = "إعدادات الصفحة"; @@ -5363,9 +5220,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of notification displayed when a page has failed to upload. */ "Page failed to upload" = "فشل رفع الصفحة"; -/* A short message explaining that a page was moved to the trash bin. */ -"Page moved to trash." = "تم نقل الصفحة إلى سلة المهملات."; - /* Title of notification displayed when a page has been successfully saved as a draft. */ "Page pending review" = "الصفحة في انتظار المراجعة"; @@ -5437,8 +5291,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title of pending Comments filter. */ "Pending" = "انتظار"; -/* Name for the status of a post pending review. - Title of the Pending Review Badge */ +/* Name for the status of a post pending review. */ "Pending review" = "في انتظار المراجعة"; /* Noun. Title of the people management feature. @@ -5467,12 +5320,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Photography site intent topic */ "Photography" = "التصوير الفوتوغرافي"; -/* Tab bar title for the Photos tab in Media Picker */ -"Photos" = "الصور"; - -/* Subtitle for placeholder in Free Photos. The company name 'Pexels' should always be written as it is. */ -"Photos provided by Pexels" = "الصور مقدمة من Pexels"; - /* Title for selecting a new username in the site creation flow. */ "Pick username" = "اختيار اسم مستخدم"; @@ -5565,7 +5412,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Please enter the password for your WordPress.com account to log in with your Apple ID." = "يرجى إدخال كلمة المرور لحساب WordPress.com الخاص بك لتسجيل الدخول باستخدام مُعرّف Apple الخاص بك."; /* Instruction text on the two-factor screen. */ -"Please enter the verification code from your authenticator app, or tap the link below to receive a code via SMS." = "يُرجى إدخال رمز التحقُّق من تطبيق الموثق الخاص بك، أو اضغط على الرابط أدناه لتلقي كود عبر خدمة الرسائل النصية القصيرة."; +"Please enter the verification code from your authenticator app." = "يرجى إدخال رمز التحقق من تطبيق الموثق الخاص بك."; /* Popup message to ask for user credentials (fields shown below). */ "Please enter your credentials" = "يُرجى إدخال بيانات اعتمادك"; @@ -5660,15 +5507,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ The post formats available for the post. Should be the same as in core WP. */ "Post Format" = "تنسيق المقالة"; -/* Prompts the user that a restored post was moved to the drafts list. */ -"Post Restored to Drafts" = "تم استعادة المقالة إلى المسودات"; - -/* Prompts the user that a restored post was moved to the published list. */ -"Post Restored to Published" = "تم استعادة المقالة إلى النشر"; - -/* Prompts the user that a restored post was moved to the scheduled list. */ -"Post Restored to Scheduled" = "تم استعادة المقالة إلى الجدول"; - /* Name of the button to open the post settings The title of the Post Settings screen. */ "Post Settings" = "إعدادات المقالة"; @@ -5688,9 +5526,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of notification displayed when a post has failed to upload. */ "Post failed to upload" = "فشل رفع المقالة"; -/* A short message explaining that a post was moved to the trash bin. */ -"Post moved to trash." = "تم نقل المقالة إلى سلة المهملات."; - /* Title of notification displayed when a post has been successfully saved as a draft. */ "Post pending review" = "المقالة في انتظار المراجعة"; @@ -5749,9 +5584,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Period Stats 'Posts and Pages' header */ "Posts and Pages" = "المقالات والصفحات"; -/* Title of the Posts Page Badge */ -"Posts page" = "صفحة المقالات"; - /* Message informing the user that their static homepage for posts was set successfully */ "Posts page successfully updated" = "تم تحديث صفحة المقالات بنجاح"; @@ -5764,9 +5596,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* A message explaining the Posts I Like feature in the reader */ "Posts that you like will appear here." = "ستظهر المنشورات التي تعجب بها هنا."; -/* Subtitle for placeholder in Tenor picker. `The company name 'Tenor' should always be written as it is. */ -"Powered by Tenor" = "مدعوم من Tenor"; - /* Browse premium themes selection title */ "Premium" = "الإصدار المميز"; @@ -5785,18 +5614,12 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title for screen to preview a static content. */ "Preview" = "معاينة"; -/* Action for Media Picker to preview the selected media items. The argument in the string represents the number of elements (as numeric digits) selected */ -"Preview %@" = "معاينة %@"; - /* Title for web preview device switching button */ "Preview Device" = "معاينة الجهاز"; /* Title on display preview error */ "Preview Unavailable" = "المعاينة غير متوفرة"; -/* Accessibility label for media item preview for user's viewing an item in their media library */ -"Preview media" = "معاينة الوسائط"; - /* No comment provided by engineer. */ "Preview page" = "معاينة الصفحة"; @@ -5843,8 +5666,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Privacy notice for California users" = "إشعار الخصوصية الخاص بالمستخدمين في كاليفورنيا"; /* Name for the status of a post that is marked private. - Privacy setting for posts set to 'Private'. Should be the same as in core WP. - Title of the Private Badge */ + Privacy setting for posts set to 'Private'. Should be the same as in core WP. */ "Private" = "خاص"; /* No comment provided by engineer. */ @@ -5894,12 +5716,10 @@ translators: %s: Select control button label e.g. \"Button width\" */ Label for the publish date button. */ "Publish Date" = "تاريخ النشر"; -/* A short phrase indicating a post is due to be immedately published. - Label for a button that moves a page to the published folder, publishing with the current date/time. */ +/* A short phrase indicating a post is due to be immedately published. */ "Publish Immediately" = "النشر في الحال"; /* Label for a button that publishes the post - Label for an option that moves a publishes a post immediately Title of button allowing the user to immediately publish the post they are editing. */ "Publish Now" = "النشر الآن"; @@ -5917,8 +5737,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Name for the status of a published post. Period Stats 'Published' header - Title of the published filter. This filter shows a list of posts that the user has published. - Title of the published header in search list. */ + Title of the published filter. This filter shows a list of posts that the user has published. */ "Published" = "منشور"; /* Precedes the name of the blog just posted on */ @@ -6060,8 +5879,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of the completion screen of the Blogging Reminders Settings screen when the reminders are removed. */ "Reminders removed" = "تمت إزالة التذكيرات"; -/* Add asset to media picker list - Alert button to confirm a plugin to be removed +/* Alert button to confirm a plugin to be removed Button label when removing a blog Label for the Remove Feature Image icon. Tapping will show a confirmation screen for removing the feature image from the post. Remove Action @@ -6214,9 +6032,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of secondary button on alert prompting verify their accounts while attempting to publish */ "Resend" = "إعادة إرسال"; -/* Title of the reset button */ -"Reset" = "استعادة"; - /* Accessibility label for the reset activity type button */ "Reset Activity Type filter" = "إعادة تعيين عامل تصفية نوع النشاط"; @@ -6271,12 +6086,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ Button title, displayed when media has failed to upload. Allows the user to try the upload again. Button title. Retries uploading a post. If a user taps the button with this label, the action that evinced this error view will be retried. - Label for a button that attempts to re-upload a page that previously failed to upload. - Label for the retry post upload button. Tapping attempts to upload the post again. Opens the media library . Retries the upload of a user's post. Retry updating User's Role - Retry uploading the post. Retry. Action Retry. Verb – retry a failed media upload. The Jetpack view button title used when an error occurred @@ -6288,9 +6100,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Button title that triggers a scan */ "Retry Scan" = "إعادة محاولة الفحص"; -/* User action to retry media upload. */ -"Retry Upload" = "إعادة محاولة الرفع"; - /* User action to retry all failed media uploads. */ "Retry all" = "محاولة الجميع مرة أخرى"; @@ -6388,9 +6197,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Accessibility label for the 'Save Post' button when a post has been saved. */ "Saved Post" = "تم حفظ المقالة"; -/* Text displayed in HUD when a media item's metadata (title, etc) is saved successfully. */ -"Saved!" = "تم الحفظ!"; - /* Accessibility hint for the 'Save Post' button. */ "Saves this post for later." = "يحفظ هذه المقالة لوقت لاحق."; @@ -6401,7 +6207,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Saving post…" = "جاري حفظ المقالة…"; /* Menus save button title while it is saving a Menu. - Text displayed in HUD while a media item's metadata (title, etc) is being saved. Text displayed in HUD while a post is being saved as a draft. */ "Saving..." = "جاري الحفظ..."; @@ -6492,21 +6297,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* No comment provided by engineer. */ "Search or type URL" = "ابحث أو اكتب رابطًا"; -/* Text displayed when the search controller will be presented */ -"Search pages" = "بحث في الصفحات"; - -/* Text displayed when the search controller will be presented */ -"Search posts" = "بحث في المقالات"; - /* No comment provided by engineer. */ "Search settings" = "إعدادات البحث"; -/* Title for placeholder in Tenor picker */ -"Search to find GIFs to add to your Media Library!" = "ابحث للعثور على صور GIF لإضافتها إلى مكتبة الوسائط الخاصة بك!"; - -/* Title for placeholder in Free Photos */ -"Search to find free photos to add to your Media Library!" = "ابحث للعثور على صور مجانية لإضافتها إلى مكتبة الوسائط الخاصة بك!"; - /* Menus search bar placeholder text. */ "Search..." = "يجري البحث..."; @@ -6577,9 +6370,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Register Domain - Domain contact information field placeholder for Country */ "Select Country" = "تحديد البلد"; -/* Title of button that allows the user to select more photos to access within the app */ -"Select More" = "تحديد مزيد"; - /* Blog Picker's Title */ "Select Site" = "تحديد موقع"; @@ -6601,9 +6391,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Register domain - Title for the Choose domain button of Suggested domains screen */ "Select domain" = "تحديد نطاق"; -/* Accessibility hint for actions when displaying media items. */ -"Select media." = "تحديد الوسائط."; - /* Accessibility label for selecting paragraph style button on formatting toolbar. */ "Select paragraph style" = "تحديد نمط الفقرة"; @@ -6707,19 +6494,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label for connected service in Publicize stat. */ "Service" = "الخدمة"; -/* Label for a button that opens the Set Parent options view controller - Navigation title displayed on the navigation bar */ +/* Navigation title displayed on the navigation bar */ "Set Parent" = "تعيين الأصل"; /* No comment provided by engineer. */ "Set as Featured Image" = "تعيين كصورة بارزة"; -/* Label for a button that sets the selected page as the site's Homepage */ -"Set as Homepage" = "تعيين كصفحة رئيسية"; - -/* Label for a button that sets the selected page as the site's Posts page */ -"Set as Posts Page" = "تعيين كصفحة مقالات"; - /* Notice confirming that an image has been set as the post's featured image. */ "Set as featured image" = "تعيين كصورة بارزة"; @@ -6763,7 +6543,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Accessibility label for share buttons in nav bars Button label to share a post Button label to share a web page - Share the post. Shares the comment URL Spoken accessibility label Title for a button that allows the user to share their answer to the prompt. @@ -7149,8 +6928,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Name of setting configured when a site uses a static page as its homepage */ "Static Homepage" = "صفحة رئيسية ثابتة"; -/* Label for post stats option. Tapping displays statistics for a post. - Noun. Abbreviation of Statistics. Name of the Stats feature +/* Noun. Abbreviation of Statistics. Name of the Stats feature Noun. Abbv. of Statistics. Links to a blog's Stats screen. Stats 3D Touch Shortcut Stats window title @@ -7181,9 +6959,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label text that defines a post marked as sticky */ "Sticky" = "مثبّت"; -/* Accessibility label for a sticky post in the post list. */ -"Sticky." = "مثبت."; - /* User action to stop upload. */ "Stop upload" = "إيقاف الرفع"; @@ -7240,7 +7015,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ Theme Support action title */ "Support" = "الدعم"; -/* Button used to switch site */ +/* Title for back button that leads to the site picker screen. */ "Switch Site" = "تبديل الموقع"; /* Switches the Editor to HTML Mode */ @@ -7328,9 +7103,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label explaining why users might want to add tags. */ "Tags help tell readers what a post is about. Separate different tags with commas." = "تساعد الوسوم على إخبار القراء بما تدور حوله المقالة. افصل بين الوسوم المختلفة بفواصل."; -/* Menu option for taking an image or video with the device's camera. */ -"Take Photo or Video" = "التقاط صورة أو تسجيل فيديو"; - /* No comment provided by engineer. */ "Take a Photo" = "التقاط صورة"; @@ -7401,12 +7173,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Accessibility hint */ "Tap to select the previous period" = "اضغط لتحديد الفترة السابقة"; -/* Accessibility hint for button used to switch site */ -"Tap to switch to another site, or add a new site" = "أنقر للتبديل إلى موقع آخر، أو إضافة موقع جديد"; - -/* Accessibility hint for media item preview for user's viewing an item in their media library */ -"Tap to view media in full screen" = "النقر لعرض الوسائط في وضع الشاشة الكاملة"; - /* Accessibility hint for a button that opens a new view with more details. */ "Tap to view more details." = "اضغط لعرض مزيد من التفاصيل."; @@ -7452,10 +7218,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* No comment provided by engineer. */ "Text formatting controls are located within the toolbar positioned above the keyboard while editing a text block" = "توجد عناصر التحكم في تنسيق النص ضمن شريط الأدوات الذي يوجد فوق لوحة المفاتيح في أثناء تحرير مكوِّن نص"; -/* Button title - The button's title text to send a 2FA code via SMS text message. */ +/* Button title */ "Text me a code instead" = "اكتب رمزًا لي بدلاً من ذلك"; +/* The button's title text to send a 2FA code via SMS text message. */ +"Text me a code via SMS" = "إرسال الكود إليَّ عبر رسالة نصية قصيرة"; + /* Message of alert when theme activation succeeds */ "Thanks for choosing %@ by %@" = "شكرًا لاختيارك %1$@ من %2$@"; @@ -7483,9 +7251,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Error message shown to a user who is trying to share to Facebook but does not have any available Facebook Pages. */ "The Facebook connection cannot find any Pages. Publicize cannot connect to Facebook Profiles, only published Pages." = "يتعذر الاتصال بـ Facebook العثور على أي صفحات. لا يمكن لآلية النشر الاتصال بملفات تعريف Facebook الشخصية، فقط للصفحات المنشورة."; -/* Message shown when a GIF failed to load while trying to add it to the Media library. */ -"The GIF could not be added to the Media Library." = "تتعذر إضافة تنسيق GIF إلى مكتبة الوسائط."; - /* Description shown when a user logs in with Google but no matching WordPress.com account is found */ "The Google account \"%@\" doesn't match any account on WordPress.com" = "لا يتطابق حساب جوجل \"‎%@\" مع أي حساب موجود على WordPress.com."; @@ -7613,7 +7378,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Error message shown when user attempts to remove the site owner. */ "The user you are trying to remove is the owner of this site. Please contact support for assistance." = "المستخدم الذي تحاول إزالته هو مالك هذا الموقع. يرجى الاتصال بالدعم للحصول على المساعدة."; -/* Error message informing a user about an invalid password. */ +/* No comment provided by engineer. */ "The username or password stored in the app may be out of date. Please re-enter your password in the settings and try again." = "قد يكون اسم المستخدم أو كلمة المرور المخزنان في التطبيق غير محدثين. يُرجى إعادة إدخال كلمة المرور في الإعدادات والمحاولة مرة أخرى."; /* Message shown when a video failed to load while trying to add it to the Media library. */ @@ -7681,9 +7446,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* A short error message letting the user know about a problem displaying a post. */ "There was a problem displaying this post." = "حدثت مشكلة أثناء عرض هذه المقالة."; -/* Error message displayed when the Media Library is unable to load a full sized preview of an item. */ -"There was a problem loading the media item." = "حدثت مشكلة أثناء تحميل عنصر الوسائط."; - /* The loading view subtitle displayed when an error occurred */ "There was a problem loading your data, refresh your page to try again." = "حدثت مشكلة أثناء تحميل بياناتك، يمكنك تحديث صفحتك للمحاولة مجددًا."; @@ -7696,9 +7458,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Explaining to the user there was an error trying to obtain the current location of the user. */ "There was a problem when trying to access your location. Please try again later." = "حدثت مشكلة عند محاولة الوصول إلى موقعك. يُرجى المحاولة مرة أخرى لاحقاً."; -/* Explaining to the user there was an generic error accesing media. */ -"There was a problem when trying to access your media. Please try again later." = "حدثت مشكلة أثناء محاولة الوصول إلى الوسائط الخاصة بك. يُرجى المحاولة مرة أخرى لاحقاً."; - /* Message for stories unknown error. */ "There was a problem with the Stories editor. If the problem persists you can contact us via the Me > Help & Support screen." = "حدثت مشكلة في محرر القصص. إذا استمرت المشكلة، يمكنك الاتصال بنا من خلال شاشة \"أنا\" > \"المساعدة والدعم\"."; @@ -7769,9 +7528,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* A description informing the user in order to proceed with this feature we will need camera permissions, and how to enable it. */ "This app needs permission to access the Camera to scan login codes, tap on the Open Settings button to enable it." = "يحتاج هذا التطبيق إلى صلاحية للوصول إلى الكاميرا لمسح رموز تسجيل الدخول ضوئيًا، انقر على زر فتح الإعدادات لتمكينها."; -/* Explaining to the user why the app needs access to the device media library. */ -"This app needs permission to access your device media library in order to add photos and\/or video to your posts. Please change the privacy settings if you wish to allow this." = "يلزم هذا التطبيق إذن بالوصول إلى مكتبة وسائط الجهاز لإضافة الصور و\/أو مقاطع الفيديو إلى مقالاتك. يُرجى تغيير إعدادات الخصوصية إذا كنت ترغب في السماح بهذا."; - /* No comment provided by engineer. */ "This color combination may be hard for people to read. Try using a brighter background color and\/or a darker text color." = "قد يصعب على الأشخاص قراءة تركيبة الألوان هذه. حاول استخدام لون خلفية أغمق و\/أو لون نص أكثر إشراقًا."; @@ -7881,6 +7637,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Description of a Quick Start Tour */ "Time to finish setting up your site! Our checklist walks you through the next steps." = "حان الوقت لإنهاء إعداد موقعك! توضِّح لك قائمة الاختيار الخاصة بنا الخطوات التالية."; +/* Error when the uses takes more than 1 minute to submit a security key. */ +"Time's up, but don't worry, your security is our priority. Please try again!" = "انتهى الوقت، لكن لا داعي للقلق، فأمانك هو أولويتنا. يرجى المحاولة مرة أخرى!"; + /* WordPress.com Marketing Footer Text */ "Tips for getting the most out of WordPress.com." = "نصائح للحصول على أكبر استفادة من WordPress.com."; @@ -8004,24 +7763,20 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title for the traffic section in site settings screen */ "Traffic" = "المرور"; +/* Describes a domain that was transferred from elsewhere to wordpress.com */ +"Transferred Domain" = "النطاق المنقول"; + /* translators: %s: block title e.g: \"Paragraph\". */ "Transform %s to" = "تحويل %s إلى"; /* No comment provided by engineer. */ "Transform block…" = "تحويل المكوِّن…"; -/* Accessibility label for trash button to delete items from the user's media library - Accessibility label for trash buttons in nav bars +/* Accessibility label for trash buttons in nav bars Trashes a comment Trashes the comment */ "Trash" = "سلة المهملات"; -/* Accessibility hint for trash button to delete items from the user's media library */ -"Trash selected media" = "نقل الوسائط المُحدَّدة إلى سلة المهملات"; - -/* Title of the trash page confirmation alert. */ -"Trash this page?" = "هل تريد نقل هذه الصفحة إلى سلة المهملات؟"; - /* Title of the trash confirmation alert. */ "Trash this post?" = "هل تريد نقل هذه المقالة إلى سلة المهملات؟"; @@ -8139,9 +7894,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Shown when a user logs in with Google but it subsequently fails to work as login to WordPress.com */ "Unable To Connect" = "يتعذر الاتصال"; -/* An error message. */ -"Unable to Connect" = "يتعذر الاتصال"; - /* Title for stories unknown error. */ "Unable to Create Stories Editor" = "تعذر إنشاء محرر القصص"; @@ -8157,9 +7909,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* An error message shown when there is an issue creating new invite links. */ "Unable to create new invite links." = "تعذر إنشاء روابط دعوة جديدة."; -/* Text displayed in HUD if there was an error attempting to delete a group of media items. */ -"Unable to delete all media items." = "يتعذر حذف جميع عناصر الوسائط."; - /* Text displayed in HUD if there was an error attempting to delete a media item. */ "Unable to delete media item." = "يتعذر حذف عنصر الوسائط."; @@ -8223,12 +7972,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Message displayed when sharing a link to the downloadable backup fails. */ "Unable to share link" = "تعذرت مشاركة الرابط"; -/* Message that appears when a user tries to trash a page while their device is offline. */ -"Unable to trash pages while offline. Please try again later." = "غير قادر على نقل الصفحات إلى سلة المُهملات في وضع عدم الاتصال. يرجى المحاولة مرة أخرى لاحقًا."; - -/* Message that appears when a user tries to trash a post while their device is offline. */ -"Unable to trash posts while offline. Please try again later." = "غير قادر على نقل المقالات إلى سلة المُهملات في وضع عدم الاتصال. يرجى المحاولة مرة أخرى لاحقًا."; - /* Notice title when turning site notifications off fails. */ "Unable to turn off site notifications" = "تعذر إيقاف تشغيل إشعارات الموقع"; @@ -8301,8 +8044,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ Button title. Reverts the previous notification operation Revert an operation Revert enabling notification after successfully subcribing to the comments for the post. - The title of an 'undo' button. Tapping the button moves a trashed page out of the trash folder. - The title of an 'undo' button. Tapping the button moves a trashed post out of the trash folder. Undo action */ "Undo" = "تراجع"; @@ -8345,9 +8086,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title for Unknown HTML Editor */ "Unknown HTML" = "HTML غير معروف"; -/* Label to use when creation date from media asset is not know. */ -"Unknown creation date" = "تاريخ الإنشاء مجهول"; - /* No comment provided by engineer. */ "Unknown error" = "خطأ غير معروف"; @@ -8513,6 +8251,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of a row displayed on the debug screen used to configure the sandbox store use in the App. */ "Use Sandbox Store" = "استخدام مخزن Sandbox"; +/* The button's title text to use a security key. */ +"Use a security key" = "استخدام مفتاح الأمان"; + /* Option to enable the block editor for new posts */ "Use block editor" = "استخدام مُحرر المكوّنات"; @@ -8588,15 +8329,10 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of an alert informing users that the video they are trying to select is not allowed. */ "Video not uploaded" = "لم يتم رفع مقطع فيديو!"; -/* Accessibility label for video thumbnails in the media collection view. The parameter is the creation date of the video. */ -"Video, %@" = "الفيديو %@"; - /* Period Stats 'Videos' header */ "Videos" = "الفيديوهات"; /* Button title. Displays a summary / sharing screen for a specific post. - Label for a button that opens the page when tapped. - Label for the view post button. Tapping displays the post as it appears on the web. Opens the post epilogue screen to allow sharing / viewing of a post. Theme View action title Verb. The screen title shown when viewing a post inside the app. */ @@ -8711,6 +8447,10 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Message shown on screen while waiting for Google to finish its signup process. */ "Waiting for Google to complete…" = "بانتظار اكتمال Google…"; +/* Text while the webauthn signature is being verified + Text while waiting for a security key challenge */ +"Waiting for security key" = "انتظار مفتاح الأمان"; + /* View title during the Google auth process. */ "Waiting..." = "جاري الانتظار..."; @@ -9082,6 +8822,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* An error message shown when a wpcom user provides the wrong password. */ "Whoops, something went wrong and we couldn't log you in. Please try again!" = "عذرًا، حدث خطأ ما وتعذر عليك تسجيل الدخول. يرجى المحاولة مرة أخرى!"; +/* Generic error on the 2FA screen */ +"Whoops, something went wrong. Please try again!" = "عذرًا، حدث خطأ ما. يرجى المحاولة مرة أخرى!"; + +/* Error when the uses chooses an invalid security key on the 2FA screen. */ +"Whoops, that security key does not seem valid. Please try again with another one" = "عذرًا، لا يبدو مفتاح الأمان ذلك صالحًا. يرجى المحاولة مجددًا باستخدام واحد آخر"; + /* Error message shown when an incorrect two factor code is provided. */ "Whoops, that's not a valid two-factor verification code. Double-check your code and try again!" = "عذرًا، هذا رمز تحقق من عاملين غير صالح. تحقق مجددًا من رمزك وحاول مجددًا!"; @@ -9109,9 +8855,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Siri Suggestion to open Support */ "WordPress Help" = "مساعدة ووردبريس"; -/* Name for the WordPress Media Library */ -"WordPress Media" = "وسائط ووردبريس"; - /* Accessibility label for selecting an image or video from the user's WordPress media library on formatting toolbar. */ "WordPress Media Library" = "مكتبة وسائط ووردبريس"; @@ -9426,9 +9169,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Share extension error dialog text. */ "Your account does not have permission to upload media to this site. The Site Administrator can change these permissions." = "لا يمتلك حسابك إذنًا برفع الوسائط إلى هذا الموقع. يمكن لمسؤول الموقع تغيير هذه الأذونات."; -/* Explaining to the user why the app needs access to the device media library. */ -"Your app is not authorized to access media library due to active restrictions such as parental controls. Please check your parental control settings in this device." = "لا يُسمح لتطبيقك بالوصول إلى مكتبة الوسائط بسبب القيود النشطة مثل الرقابة الأبوية. يرجى التحقّق من إعدادات الرقابة الأبوية لديك في هذا الجهاز."; - /* Title for the Jetpack Backup Complete message. */ "Your backup is now available for download" = "أصبحت نسختك الاحتياطية متاحة الآن للتنزيل"; @@ -9447,9 +9187,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of the site address section in the Domains Dashboard. */ "Your free WordPress.com address is" = "عنوان ووردبريس.كوم المجاني الخاص بك هو"; -/* Error message when picked media cannot be imported into stories. */ -"Your media could not be exported. If the problem persists you can contact us via the Me > Help & Support screen." = "لا يمكن تصدير ملفات الوسائط الخاصة بك. إذا استمرت المشكلة، يمكنك الاتصال بنا من خلال شاشة \"أنا\" > \"المساعدة والدعم\"."; - /* Details about recently acquired domain on domain credit redemption success screen */ "Your new domain %@ is being set up. It may take up to 30 minutes for your domain to start working." = "جاري الآن إعداد نطاقك الجديد %@. قد يستغرق الأمر ما يصل إلى 30 دقيقة لكي يبدأ نطاقك بالعمل."; @@ -9573,8 +9310,38 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* This is the string we display when prompting the user to review the WordPress app */ "appRatings.wordpress.prompt" = "ما هو رأيك في ووردبريس؟"; -/* Label displayed on audio media items. */ -"audio" = "صوت"; +/* Option to enable the optimization of images when uploading. */ +"appSettings.media.imageOptimizationRow" = "تحسين الصور"; + +/* Indicates an image will use high quality when uploaded. */ +"appSettings.media.imageQuality.high" = "مرتفع"; + +/* Indicates an image will use low quality when uploaded. */ +"appSettings.media.imageQuality.low" = "منخفض"; + +/* Indicates an image will use maximum quality when uploaded. */ +"appSettings.media.imageQuality.maximum" = "الحد الأقصى"; + +/* Indicates an image will use medium quality when uploaded. */ +"appSettings.media.imageQuality.medium" = "متوسط"; + +/* The quality of image used when uploading */ +"appSettings.media.imageQuality.title" = "الجودة"; + +/* Title for the image quality settings option. */ +"appSettings.media.imageQualityRow" = "جودة الصورة"; + +/* Message of an alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.message" = "يؤدي تحسين الصورة إلى تقليل حجم الصور المطلوب رفعها بسرعة.\n\nتم تمكين هذا الخيار افتراضيًا، لكن يمكنك تغييره ضمن إعدادات التطبيق في أي وقت."; + +/* Title of an alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.title" = "هل تريد الاستمرار في تحسين الصور؟"; + +/* Title of button for turning off image optimization, displayed in the alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.turnOff" = "لا، توقف عن الاستخدام"; + +/* Title of button for leaving on image optimization, displayed in the alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.turnOn" = "نعم، واصل الاستخدام"; /* translators: displays audio file extension. e.g. MP3 audio file */ "audio file" = "ملف صوت"; @@ -9648,6 +9415,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Short status description */ "blazeCampaign.status.active" = "نشط"; +/* Short status description */ +"blazeCampaign.status.approved" = "تمت الموافقة على"; + /* Short status description */ "blazeCampaign.status.canceled" = "تم الإلغاء"; @@ -9682,7 +9452,41 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "blogHeader.actionCopyURL" = "نسخ عنوان URL"; /* Context menu button title */ -"blogHeader.actionOpenInBrowser" = "فتح في متصفح"; +"blogHeader.actionVisitSite" = "زيارة الموقع"; + +/* Title for a button that, when tapped, shows more info about participating in Bloganuary. */ +"bloganuary.dashboard.card.button.learnMore" = "معرفة المزيد"; + +/* Short description for the Bloganuary event, shown right below the title. */ +"bloganuary.dashboard.card.description" = "في شهر يناير، ستأتي موجّهات التدوين من Bloganuary - تحدٍّ مجتمعي لدينا لإنشاء عادة التدوين الخاصة بالعام الجديد."; + +/* Title for the Bloganuary dashboard card while Bloganuary is running. */ +"bloganuary.dashboard.card.runningTitle" = "أصبحت Bloganuary هنا!"; + +/* Title for the Bloganuary dashboard card. */ +"bloganuary.dashboard.card.title" = "Bloganuary قادمة!"; + +/* Title of a button that calls the user to enable the Blogging Prompts feature. */ +"bloganuary.learnMore.modal.button.promptsDisabled" = "تشغيل موجّهات التدوين"; + +/* Title of a button that will dismiss the Bloganuary modal when tapped. +Note that the word 'go' here should have a closer meaning to 'start' rather than 'move forward'. */ +"bloganuary.learnMore.modal.button.promptsEnabled" = "هيا بنا!"; + +/* The second line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.description.second" = "انشر ردك."; + +/* The third line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.description.third" = "اقرأ ردود المدونين الأخرى للحصول على الإلهام وإقامة اتصالات جديدة."; + +/* The first line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.descriptions.first" = "استقبل موجّهًا جديدًا لإلهامك في كل يوم."; + +/* An additional piece of information shown in case the user has the Blogging Prompts feature disabled. */ +"bloganuary.learnMore.modal.footer.addition" = "للانضمام إلى Bloganuary، يتعين عليك تمكين موجّهات التدوين."; + +/* An informative excerpt shown in a subtler tone. */ +"bloganuary.learnMore.modal.footer.text" = "ستستخدم Bloganuary موجّهات التدوين اليومية لإرسال الموضوعات الخاصة بشهر يناير إليك."; /* Verb. Dismisses the blogging prompt notification. */ "bloggingPrompt.pushNotification.customActionDescription.dismiss" = "تجاهُل"; @@ -9711,6 +9515,12 @@ Example: Comment on Example: Reply to Pamela Nguyen */ "comment.header.subText.reply" = "الرد على %1$@"; +/* Error message informing a user about an invalid password. */ +"common.reEnterPasswordMessage" = "قد يكون اسم المستخدم أو كلمة المرور المخزَّنان في التطبيق غير محدَّثين. يرجى إعادة إدخال كلمة المرور في الإعدادات والمحاولة مرة أخرى."; + +/* An error message. */ +"common.unableToConnect" = "يتعذر الاتصال"; + /* Footnote for the privacy compliance popover. */ "compliance.analytics.popover.footnote" = "تسمح لنا ملفات تعريف الارتباط هذه بتحسين الأداء عن طريق جمع المعلومات حول كيفية تفاعل المستخدمين مع مواقعنا على الويب."; @@ -9861,6 +9671,81 @@ Example: Reply to Pamela Nguyen */ /* Title for a menu action in the context menu on the Jetpack install card. */ "domain.dashboard.card.menu.hide" = "إخفاء هذا"; +/* Search domain - Title for the Suggested domains screen */ +"domain.management.addDomain.search.title" = "البحث عن نطاق"; + +/* Domain management buy domain card button title */ +"domain.management.buy.card.button.title" = "الحصول على نطاق"; + +/* Domain management buy domain card subtitle */ +"domain.management.buy.card.subtitle" = "أضف موقعًا لاحقًا."; + +/* Domain management buy domain card title */ +"domain.management.buy.card.title" = "شراء نطاق فقط"; + +/* The expired label of the domain card in All Domains screen. */ +"domain.management.card.expired.label" = "منتهي الصلاحية"; + +/* The renews label of the domain card in All Domains screen. */ +"domain.management.card.renews.label" = "التجديدات"; + +/* The empty state button title in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.button.title" = "العثور على نطاق"; + +/* The empty state description in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.description" = "اضغط أدناه للعثور على نطاقك المثالي."; + +/* The empty state title in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.title" = "لا تتوافر لديك أي نطاقات"; + +/* The empty state description in All Domains screen when an error occurs */ +"domain.management.error.empty.state.description" = "واجهنا خطأ في أثناء تحميل نطاقاتك. يرجى الاتصال بالدعم إذا استمرت المشكلة."; + +/* The empty state title in All Domains screen when an error occurs */ +"domain.management.error.empty.state.title" = "حدث خطأ"; + +/* The empty state button title in All Domains screen when an error occurs */ +"domain.management.error.state.button.title" = "المحاولة مجددًا"; + +/* The empty state description in All Domains screen when the user is offline */ +"domain.management.offline.empty.state.description" = "يرجى التحقق من اتصال شبكتك والمحاولة لاحقًا."; + +/* The empty state title in All Domains screen when the user is offline */ +"domain.management.offline.empty.state.title" = "لا يوجد اتصال بالإنترنت"; + +/* Domain management choose site card button title */ +"domain.management.purchase.footer" = "* تتضمّن جميع الخطط السنوية المدفوعة نطاقًا مجانيًّا لمدة عام كامل"; + +/* Domain management purchase domain screen title */ +"domain.management.purchase.subtitle" = "لا تقلق، يمكنك بسهولة إضافة موقع لاحقًا."; + +/* Domain management purchase domain screen title. */ +"domain.management.purchase.title" = "اختيار طريقة استخدام نطاقك"; + +/* The search bar title in All Domains screen. */ +"domain.management.search-bar.title" = "البحث عن النطاقات"; + +/* The empty state description in All Domains screen when the are no domains matching the search criteria */ +"domain.management.search.empty.state.description" = "يتعذر العثور على أي نطاقات تتطابق مع البحث عن \"%@\""; + +/* The empty state title in All Domains screen when the are no domains matching the search criteria */ +"domain.management.search.empty.state.title" = "لم يتم العثور على نطاقات متطابقة"; + +/* Domain management choose site card button title */ +"domain.management.site.card.button.title" = "اختيار موقع"; + +/* Domain management choose site card subtitle */ +"domain.management.site.card.footer" = "نطاق مجاني للعام الأول*"; + +/* Domain management choose site card subtitle */ +"domain.management.site.card.subtitle" = "الاستخدام مع أي موقع بدأته بالفعل."; + +/* Domain management choose site card title */ +"domain.management.site.card.title" = "موقع ووردبريس.كوم الحالي"; + +/* Domain Management Screen Title */ +"domain.management.title" = "جميع النطاقات"; + /* Domain Purchase Completion footer */ "domain.purchase.preview.footer" = "قد يستغرق الأمر ما يصل إلى 30 دقيقة لكي يبدأ نطاقك المخصص بالعمل."; @@ -9873,21 +9758,6 @@ Example: Reply to Pamela Nguyen */ /* Reflects that site is live when domain purchase feature flag is ON. */ "domain.purchase.preview.title" = "أحسنت، أصبح موقعك نابضًا بالحياة!"; -/* Status of a domain in `Action Required` state */ -"domain.status.action.required" = "الإجراء مطلوب"; - -/* Status of a domain in `Complete Setup` state */ -"domain.status.complete.setup" = "إعداد كامل"; - -/* Status of a domain in `In Progress` state */ -"domain.status.in.progress" = "قيد التقدم"; - -/* Status of a domain in `Verify Email` state */ -"domain.status.verify.email" = "التحقق من البريد الإلكتروني"; - -/* Status of a domain in `Verifying` state */ -"domain.status.verifying" = "التحقق"; - /* The 'Best Alternative' label under the domain name in 'Choose a domain' screen */ "domain.suggestions.row.best-alternative" = "أفضل بديل"; @@ -9909,12 +9779,24 @@ Example: Reply to Pamela Nguyen */ /* The text to display for paid domains in 'Site Creation > Choose a domain' screen */ "domain.suggestions.row.yearly" = "سنوياً"; +/* Title for the checkout screen. */ +"domains.checkout.title" = "السداد"; + /* Action shown in a bottom notice to dismiss it. */ "domains.failure.dismiss" = "تجاهل"; /* Content show when the domain selection action fails. */ "domains.failure.title" = "عذرًا، يتعذر شراء النطاق الذي تحاول إضافته على تطبيق Jetpack في هذه المرة."; +/* Title for the screen where the user can choose how to use the domain they're end up purchasing. */ +"domains.purchase.choice.title" = "شراء النطاق"; + +/* Back button title that navigates back to the search domains screen. */ +"domains.search.backButton.title" = "البحث"; + +/* Title of screen where user chooses a site to connect to their selected domain */ +"domains.sitePicker.title" = "اختيار موقع"; + /* No comment provided by engineer. */ "double-tap to change unit" = "أنقر نقرًا مزدوجًا لتغيير الوحدة"; @@ -9932,6 +9814,15 @@ Example: Reply to Pamela Nguyen */ Site Address placeholder */ "example.com" = "example.com"; +/* Title for confirmation navigation bar button item */ +"externalMediaPicker.add" = "إضافة"; + +/* Bottom toolbar title in the selection mode */ +"externalMediaPicker.toolbarSelectItemsPrompt" = "تحديد صور"; + +/* Bottom toolbar title in the selection mode */ +"externalMediaPicker.toolbarViewSelected" = "عرض العناصر المحدَّدة (%@)"; + /* Title of screen the displays the details of an advertisement campaign. */ "feature.blaze.campaignDetails.title" = "تفاصيل الحملة"; @@ -10031,9 +9922,6 @@ Example: Reply to Pamela Nguyen */ /* (placeholder) Help the user enter a URL into the field */ "http:\/\/my-site-address (URL)" = "http:\/\/my-site-address (عنوان URL)"; -/* Label displayed on image media items. */ -"image" = "صورة"; - /* Sentence to justify why the app is asking permission from the user to use their camera. */ "infoplist.NSCameraUsageDescription" = "لالتقاط صور أو مقاطع فيديو لاستخدامها في مقالاتك."; @@ -10334,6 +10222,9 @@ Example: Reply to Pamela Nguyen */ /* Indicating that referrer was marked as spam */ "marked as spam" = "تم وضع علامة على أنه بريد مزعج"; +/* Products header text in Me Screen. */ +"me.products.header" = "المنتجات"; + /* Title of error prompt shown when a sync fails. */ "media.syncFailed" = "تتعذر مزامنة الوسائط"; @@ -10346,18 +10237,24 @@ Example: Reply to Pamela Nguyen */ /* Message of an alert informing users that the video they are trying to select is not allowed. */ "mediaExporter.videoLimitExceededError" = "يتطلب رفع مقاطع فيديو تزيد على 5 دقائق خطة مدفوعة."; -/* Verb. User action to dismiss error alert when failing to load media item. */ -"mediaItemTable.errorAlert.dismissButton" = "تجاهُل"; - /* Accessibility hint for add button to add items to the user's media library */ "mediaLibrary.addButtonAccessibilityHint" = "إضافة وسائط جديدة"; /* Accessibility label for add button to add items to the user's media library */ "mediaLibrary.addButtonAccessibilityLabel" = "إضافة"; +/* Button name in the more menu */ +"mediaLibrary.aspectRatioGrid" = "شبكة نسبة العرض إلى الارتفاع"; + +/* Context menu button */ +"mediaLibrary.buttonDelete" = "حذف"; + /* Media screen navigation bar button Select title */ "mediaLibrary.buttonSelect" = "تحديد"; +/* Context menu button */ +"mediaLibrary.buttonShare" = "مشاركة"; + /* Verb. Button title. Tapping cancels an action. */ "mediaLibrary.deleteConfirmationCancel" = "إلغاء"; @@ -10379,6 +10276,21 @@ Example: Reply to Pamela Nguyen */ /* Text displayed in HUD after successfully deleting a media item */ "mediaLibrary.deletionSuccessMessage" = "محذوف!"; +/* The name of the media filter */ +"mediaLibrary.filterAll" = "الكل"; + +/* The name of the media filter */ +"mediaLibrary.filterAudio" = "الصوت"; + +/* The name of the media filter */ +"mediaLibrary.filterDocuments" = "الوثائق"; + +/* The name of the media filter */ +"mediaLibrary.filterImages" = "الصور"; + +/* The name of the media filter */ +"mediaLibrary.filterVideos" = "الفيديوهات"; + /* User action to delete un-uploaded media. */ "mediaLibrary.retryOptionsAlert.delete" = "حذف"; @@ -10391,6 +10303,12 @@ Example: Reply to Pamela Nguyen */ /* Message displayed when no results are returned from a media library search. Should match Calypso. */ "mediaLibrary.searchResultsEmptyTitle" = "لا توجد وسائط مطابقة لبحثك"; +/* Text displayed in HUD if there was an error attempting to share a group of media items. */ +"mediaLibrary.sharingFailureMessage" = "تتعذر مشاركة العناصر المحددة."; + +/* Button name in the more menu */ +"mediaLibrary.squareGrid" = "شبكة مربعة"; + /* Media screen navigation title */ "mediaLibrary.title" = "الوسائط"; @@ -10412,6 +10330,12 @@ Example: Reply to Pamela Nguyen */ /* The title of the button to dismiss the alert shown when the picked media cannot be imported into stories. */ "mediaPicker.failedMediaExportAlert.dismissButton" = "تجاهُل"; +/* Error message when picked media cannot be imported into stories. */ +"mediaPicker.failedMediaExportAlert.message" = "يتعذر تصدير وسائطك. إذا استمرت المشكلة، يمكنك الاتصال بنا من خلال شاشة \"أنا\" > \"المساعدة والدعم\"."; + +/* Error title when picked media cannot be imported into stories. */ +"mediaPicker.failedMediaExportAlert.title" = "فشل تصدير الوسائط"; + /* Message for alert when access to camera is not granted */ "mediaPicker.noCameraAccessMessage" = "يلزم هذا التطبيق إذن بالوصول إلى الكاميرا لالتقاط صورة جديدة، يُرجى تغيير إعدادا الخصوصية إذا كنت ترغب في السماح بهذا."; @@ -10445,6 +10369,12 @@ Example: Reply to Pamela Nguyen */ /* The name of the action in the context menu */ "mediaPicker.takeVideo" = "تسجيل فيديو"; +/* Navigation title for media preview. Example: 1 of 3 */ +"mediaPreview.NofM" = "%1$@ من %2$@"; + +/* Max image size in pixels (e.g. 300x300px) */ +"mediaSizeSlider.valueFormat" = "%1$d × ⁦%2$d⁩ بكسل"; + /* The description in the Delete WordPress screen */ "migration.deleteWordpress.description" = "يبدو أن تطبيق ووردبريس لا يزال مثبتًا لديك."; @@ -10457,9 +10387,6 @@ Example: Reply to Pamela Nguyen */ /* The title in the Delete WordPress screen */ "migration.deleteWordpress.title" = "لن تحتاج إلى تطبيق ووردبريس على جهازك بعد الآن"; -/* Primary button title in the migration done screen. */ -"migration.done.actions.primary.title" = "إنهاء"; - /* Footer for the migration done screen. */ "migration.done.footer" = "نوصي بأن تحذف تطبيق ووردبريس لتفادي تعارضات البيانات."; @@ -10469,6 +10396,9 @@ Example: Reply to Pamela Nguyen */ /* Primary description in the migration done screen. */ "migration.done.primaryDescription" = "لقد نقلنا كل بياناتك وإعداداتك. يصبح كل شيء صحيحًا عندما تتركه."; +/* Secondary description (second paragraph) in the migration done screen. */ +"migration.done.secondaryDescription" = "حان الوقت لمواصلة رحلتك في ووردبريس.كوم على تطبيق Jetpack!"; + /* Title of the migration done screen. */ "migration.done.title" = "شكرًا على التبديل إلى Jetpack!"; @@ -10517,6 +10447,9 @@ Example: Reply to Pamela Nguyen */ /* The title in the migration welcome screen */ "migration.welcome.title" = "مرحبًا بك في Jetpack!"; +/* Primary button title in the migration done screen. */ +"migrationDone.actions.primaryTitle" = "لنبدأ"; + /* Description for the static screen displayed prompting users to switch the Jetpack app. */ "movedToJetpack.description" = "يحتوي تطبيق Jetpack على كل وظائف تطبيق ووردبريس، إلى جانب وصول حصري الآن إلى ميزات Stats وReader وNotifications والمزيد."; @@ -10592,6 +10525,30 @@ Example: Reply to Pamela Nguyen */ /* Message title for when a user has no sites. */ "mySite.noSites.title" = "ليس لديك أي مواقع"; +/* Menu title for the add site option */ +"mySite.siteActions.addSite" = "إضافة موقع"; + +/* Button that reveals more site actions */ +"mySite.siteActions.button" = "إجراءات الموقع"; + +/* Accessibility hint for button used to show more site actions */ +"mySite.siteActions.hint" = "النقر لإظهار مزيد من إجراءات الموقع"; + +/* Menu title for the personalize home option */ +"mySite.siteActions.personalizeHome" = "تخصيص الصفحة الرئيسية"; + +/* Menu title for the change site icon option */ +"mySite.siteActions.siteIcon" = "تغيير أيقونة الموقع"; + +/* Menu title for the change site title option */ +"mySite.siteActions.siteTitle" = "تغيير عنوان الموقع"; + +/* Menu title for the switch site option */ +"mySite.siteActions.switchSite" = "تغيير الموقع"; + +/* Menu title for the visit site option */ +"mySite.siteActions.visitSite" = "زيارة الموقع"; + /* Dismiss button title. */ "noResultsViewController.dismissButton" = "تجاهل"; @@ -10607,14 +10564,20 @@ Example: Reply to Pamela Nguyen */ /* This is one of the buttons we display when prompting the user for a review */ "notifications.appRatings.sendFeedback.yes.buttonTitle" = "إرسال الملاحظات"; -/* Word separating the current index from the total amount. I.e.: 7 of 9 */ -"of" = "من"; +/* Badge for page cells */ +"pageList.badgeHomepage" = "الصفحة الرئيسية"; + +/* Badge for page cells */ +"pageList.badgeLocalChanges" = "التغييرات المحلية"; -/* Label displayed on media items that are not video, image, or audio. */ -"other" = "أخرى"; +/* Badge for page cells */ +"pageList.badgePendingReview" = "في انتظار المراجعة"; -/* Promote the page with Blaze. */ -"pages.blaze.actionTitle" = "الترويج مع الإبراز"; +/* Badge for page cells */ +"pageList.badgePosts" = "صفحة التدوينات"; + +/* Badge for page cells */ +"pageList.badgePrivate" = "خاص"; /* Subtitle of the theme template homepage cell */ "pages.template.subtitle" = "تستخدم صفحتك الرئيسية قالب القالب وستفتح في محرر الويب."; @@ -10622,6 +10585,36 @@ Example: Reply to Pamela Nguyen */ /* Title of the theme template homepage cell */ "pages.template.title" = "الصفحة الرئيسية"; +/* Message informing the user that their static homepage page was set successfully */ +"pages.updatePage.successTitle" = "تم تحديث الصفحة بنجاح."; + +/* Delete option in the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.actionTitle" = "الحذف نهائيًا"; + +/* Message of the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.alertMessage" = "هل تريد بالتأكد حذف هذه الصفحة نهائيًا؟"; + +/* Title of the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.alertTitle" = "هل تريد الحذف نهائيًا؟"; + +/* Menu option for filtering posts by everyone */ +"pagesList.pagesByEveryone" = "الصفحات بواسطة الجميع"; + +/* Menu option for filtering posts by me */ +"pagesList.pagesByMe" = "الصفحات بواسطتي"; + +/* Trash option in the trash page confirmation alert. */ +"pagesList.trash.actionTitle" = "نقل إلى سلة المهملات"; + +/* Message of the trash page confirmation alert. */ +"pagesList.trash.alertMessage" = "هل تريد بالتأكيد نقل هذه الصفحة إلى سلة المهملات؟"; + +/* Title of the trash page confirmation alert. */ +"pagesList.trash.alertTitle" = "هل تريد نقل هذه الصفحة إلى سلة المهملات؟"; + +/* Cancels an Action */ +"pagesList.trash.cancel" = "إلغاء"; + /* No comment provided by engineer. */ "password" = "كلمة المرور"; @@ -10661,6 +10654,51 @@ Example: Reply to Pamela Nguyen */ /* Register Domain - Domain contact information field Phone */ "phone number" = "رقم الهاتف"; +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.createdTimeAgo" = "%@ الذي تم إنشاؤه"; + +/* Status mesasge for post cells */ +"post.deletingPostPermanentlyStatusMessage" = "جارٍ حذف التدوينة..."; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.editedTimeAgo" = "%@ الذي تم تحريره"; + +/* Status mesasge for post cells */ +"post.movingToTrashStatusMessage" = "جارٍ نقل التدوينة إلى سلة المهملات..."; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.publishedTimeAgo" = "%@ المنشور"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.scheduledForDate" = "%@ المجدول"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.trashedTimeAgo" = "%@ الموضوع في سلة المهملات"; + +/* Accessibility label for the post author in the post list. The parameter is the author name. For example, \"By Elsa.\" */ +"postList.a11y.authorChunkFormat" = "بواسطة %@."; + +/* Accessibility label for a post's excerpt in the post list. The parameter is the post excerpt. For example, \"Excerpt. This is the first paragraph.\" */ +"postList.a11y.exerptChunkFormat" = "المقتطف. %@."; + +/* Accessibility label for a sticky post in the post list. */ +"postList.a11y.sticky" = "مثبت."; + +/* Accessibility label for a post in the post list. The first placeholder is the post title. The second placeholder is the date. */ +"postList.a11y.titleAndDateChunkFormat" = "%1$@، %2$@."; + +/* Title for the 'Trash' post list row swipe action */ +"postList.swipeActionDelete" = "سلة المهملات"; + +/* Title for the 'Delete' post list row swipe action */ +"postList.swipeActionDeletePermanently" = "حذف"; + +/* Title for the 'Share' post list row swipe action */ +"postList.swipeActionShare" = "مشاركة"; + +/* Title for the 'View' post list row swipe action */ +"postList.swipeActionView" = "عرض"; + /* User action to dismiss featured media options. */ "postSettings.featuredImageUploadActionSheet.dismiss" = "تجاهل"; @@ -10679,9 +10717,84 @@ Example: Reply to Pamela Nguyen */ /* Button in Post Settings */ "postSettings.setFeaturedImageButton" = "تعيين صورة بارزة"; +/* Error message on post/page settings screen */ +"postSettings.updateFailedMessage" = "فشل تحديث إعدادات التدوينة"; + /* Promote the post with Blaze. */ "posts.blaze.actionTitle" = "الترويج مع الإبراز"; +/* Label for the Post List option that cancels automatic uploading of a post. */ +"posts.cancelUpload.actionTitle" = "إلغاء الرفع"; + +/* Label for post comments option. Tapping displays comments for a post. */ +"posts.comments.actionTitle" = "التعليقات"; + +/* Label for the delete post option. Tapping permanently deletes a post. */ +"posts.delete.actionTitle" = "الحذف نهائيًا"; + +/* Label for an option that moves a post to the draft folder */ +"posts.draft.actionTitle" = "انتقل إلى المسودة"; + +/* Label for post duplicate option. Tapping creates a copy of the post. */ +"posts.duplicate.actionTitle" = "تكرار"; + +/* Opens a submenu for page attributes. */ +"posts.pageAttributes.actionTitle" = "سمات الصفحة"; + +/* Label for the preview post button. Tapping displays the post as it appears on the web. */ +"posts.preview.actionTitle" = "معاينة"; + +/* Label for an option that moves a publishes a post immediately */ +"posts.publish.actionTitle" = "النشر الآن"; + +/* Retry uploading the post. */ +"posts.retry.actionTitle" = "إعادة المحاولة"; + +/* Set the selected page as the homepage. */ +"posts.setHomepage.actionTitle" = "تعيين كصفحة رئيسية"; + +/* Set the parent page for the selected page. */ +"posts.setParent.actionTitle" = "تعيين الأصل"; + +/* Set the selected page as a posts page. */ +"posts.setPostsPage.actionTitle" = "تعيين كصفحة تدوينات"; + +/* Set the selected page as a regular page. */ +"posts.setRegularPage.actionTitle" = "تعيين كصفحة عادية"; + +/* Label for post settings option. Tapping displays settings for a post. */ +"posts.settings.actionTitle" = "الإعدادات"; + +/* Share the post. */ +"posts.share.actionTitle" = "مشاركة"; + +/* Label for post stats option. Tapping displays statistics for a post. */ +"posts.stats.actionTitle" = "الإحصاءات"; + +/* Label for a option that moves a post to the trash folder */ +"posts.trash.actionTitle" = "نقل إلى سلة المهملات"; + +/* Label for the view post button. Tapping displays the post as it appears on the web. */ +"posts.view.actionTitle" = "عرض"; + +/* A short message explaining that a page was deleted permanently. */ +"postsList.deletePage.message" = "تم حذف الصفحة نهائيًا"; + +/* A short message explaining that a post was deleted permanently. */ +"postsList.deletePost.message" = "تم حذف التدوينة نهائيًا"; + +/* A short message explaining that a page was moved to the trash bin. */ +"postsList.movePageToTrash.message" = "تم نقل الصفحة إلى سلة المهملات"; + +/* A short message explaining that a post was moved to the trash bin. */ +"postsList.movePostToTrash.message" = "تم نقل التدوينة إلى سلة المهملات"; + +/* Menu option for filtering posts by everyone */ +"postsList.postsByEveryone" = "التدوينات بواسطة الجميع"; + +/* Menu option for filtering posts by me */ +"postsList.postsByMe" = "التدوينات بواسطتي"; + /* Title for the button to subscribe to Jetpack Social on the remaining shares view */ "postsettings.social.remainingshares.subscribe" = "الاشتراك الآن لمشاركة المزيد"; @@ -10797,10 +10910,12 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Accessibility hint for the comment button on the reader post card cell */ "reader.post.button.comment.accessibility.hint" = "لفتح التعليقات الخاصة بالتدوينة."; -/* Accessibility hint for the like button on the reader post card cell - Accessibility hint for the liked button on the reader post card cell */ +/* Accessibility hint for the like button on the reader post card cell */ "reader.post.button.like.accessibility.hint" = "للإعجاب بالتدوينة."; +/* Accessibility hint for the liked button on the reader post card cell */ +"reader.post.button.liked.accessibility.hint" = "لإلغاء الإعجاب بالتدوينة."; + /* Accessibility hint for the site header on the reader post card cell */ "reader.post.button.menu.accessibility.hint" = "لفتح قائمة تتضمن مزيدًا من الإجراءات."; @@ -10858,6 +10973,12 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Title for the tooltip anchor. */ "readerDetail.tooltipAnchorTitle.accessibilityLabel" = "جديد"; +/* The button title for the transfer footer view in Register Domain screen */ +"register.domain.transfer.button.title" = "نقل النطاق"; + +/* The title for the transfer footer view in Register Domain screen */ +"register.domain.transfer.title" = "هل تبحث عن نطاق تمتلكه بالفعل؟"; + /* Information of what related post are and how they are presented */ "relatedPostsSettings.optionsFooter" = "تعرض التدوينات ذات الصلة المحتوى ذا الصلة من موقعك أسفل تدويناتك."; @@ -10945,6 +11066,21 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Accessibility hint for actions when displaying media items. */ "siteMedia.cellAccessibilityHint" = "حدِّد الوسائط."; +/* Accessibility hint for media item preview for user's viewing an item in their media library */ +"siteMediaItem.contentViewAccessibilityHint" = "النقر لعرض الوسائط في وضع الشاشة الكاملة"; + +/* Accessibility label for media item preview for user's viewing an item in their media library */ +"siteMediaItem.contentViewAccessibilityLabel" = "معاينة الوسائط"; + +/* Title for confirmation navigation bar button item */ +"siteMediaPicker.add" = "إضافة"; + +/* Button selection media in media picker */ +"siteMediaPicker.deselect" = "إلغاء التحديد"; + +/* Button selection media in media picker */ +"siteMediaPicker.select" = "تحديد"; + /* Media screen navigation title */ "siteMediaPicker.title" = "الوسائط"; @@ -10952,7 +11088,7 @@ Example: given a notice format "Following %@" and empty site name, this will be "siteSettings.privacy.title" = "الخصوصية"; /* Hint for users when hidden privacy setting is set */ -"siteVisibility.hidden.hint" = "إن موقعك مرئي للجميع، ولكنه يطلب من محركات البحث عدم فهرسة موقعك."; +"siteVisibility.hidden.hint" = "تم إخفاء موقعك عن الزوار خلف ملاحظة \"قريبًا\" حتى يصبح جاهزًا لمشاهدته."; /* Text for privacy settings: Hidden */ "siteVisibility.hidden.title" = "مخفي"; @@ -11113,6 +11249,12 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Dismiss the AlertView */ "stockPhotos.strings.dismiss" = "تجاهُل"; +/* Subtitle for placeholder in Free Photos. The company name 'Pexels' should always be written as it is. */ +"stockPhotos.subtitle" = "الصور مقدمة من Pexels"; + +/* Title for placeholder in Free Photos */ +"stockPhotos.title" = "ابحث للعثور على صور مجانية لإضافتها إلى مكتبة الوسائط الخاصة بك!"; + /* Section title for prominent suggestions */ "suggestions.section.prominent" = "في هذه المحادثة"; @@ -11260,6 +11402,9 @@ Example: given a notice format "Following %@" and empty site name, this will be /* View title for Help & Support page. */ "support.title" = "مساعدة"; +/* Title for placeholder in Tenor picker */ +"tenor.welcomeMessage" = "ابحث للعثور على صور بتنسيق GIF لإضافتها إلى مكتبة الوسائط الخاصة بك!"; + /* Header of delete screen section listing things that will be deleted. */ "these items will be deleted:" = "سيتم حذف هذه العناصر:"; @@ -11275,9 +11420,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Displayed in the confirmation alert when marking unread notifications as read. */ "unread" = "غير مقروء"; -/* Label displayed on video media items. */ -"video" = "فيديو"; - /* Portion of a message for Jetpack users that have multisite WP installation, thus Restore is not available. This part is a link, colored with a different color. */ "visit our documentation page" = "زيارة صفحة الوثائق الخاصة بنا"; diff --git a/WordPress/Resources/bg.lproj/Localizable.strings b/WordPress/Resources/bg.lproj/Localizable.strings index 1524166eb4a4..9485329681c9 100644 --- a/WordPress/Resources/bg.lproj/Localizable.strings +++ b/WordPress/Resources/bg.lproj/Localizable.strings @@ -51,9 +51,6 @@ /* Age between dates over one year. */ "%d years" = "%d години"; -/* Max image size in pixels (e.g. 300x300px) */ -"%dx%dpx" = "%1$dx%2$dpx"; - /* One menu area available in the theme */ "%i menu area in this theme" = "%i място за меню в тази тема"; @@ -133,10 +130,6 @@ /* No comment provided by engineer. */ "Activity Logs" = "История на активността"; -/* Accessibility label for add button to add items to the user's media library - Remove asset from media picker list */ -"Add" = "Добавяне"; - /* The title on the add category screen */ "Add a Category" = "Добавяне на категория"; @@ -162,9 +155,6 @@ /* Title for the advanced section in site settings screen */ "Advanced" = "Разширени"; -/* Description of albums in the photo libraries */ -"Albums" = "Албуми"; - /* Image alignment option title. Title of the screen for choosing an image's alignment. */ "Alignment" = "Подравняване"; @@ -261,12 +251,6 @@ /* Message prompting the user to confirm that they want to disconnect Jetpack from the site. */ "Are you sure you want to disconnect Jetpack from the site?" = "Сигурни ли сте, че искате да прекратите връзката на Jetpack със сайта?"; -/* Message prompting the user to confirm that they want to permanently delete a group of media items. */ -"Are you sure you want to permanently delete these items?" = "Сигурен ли сте, че желаете безвъзвратно да изтриете избраните елементи?"; - -/* Accessibility label for audio items in the media collection view. The parameter is the creation date of the audio. */ -"Audio, %@" = "Аудио, %@"; - /* No comment provided by engineer. */ "Authenticating" = "Разрешаване"; @@ -343,8 +327,7 @@ /* Label for size of media while it's being calculated. */ "Calculating..." = "Пресмятане..."; -/* Accessibility label for taking an image or video with the camera on formatting toolbar. - Accessibility label for the camera tile in the collection view */ +/* Accessibility label for taking an image or video with the camera on formatting toolbar. */ "Camera" = "Камера"; /* Title of an alert letting the user know */ @@ -386,10 +369,7 @@ Cancels an Action Cancels an alert. Cancels the mark all as read action. - Dismiss the post action sheet Editing GIF alert cancel action button. - Label for a cancel button - Label for the auto-upload cancelation button in the post list. Tapping will prevent the app from auto-uploading the post. Menus cancel button for deleting a menu. Menus cancel button within text bar while editing items. Menus: Cancel button title for canceling an edited menu item. @@ -636,9 +616,6 @@ /* No comment provided by engineer. */ "Couldn't Connect" = "Неуспешно свързване"; -/* Message to show while media data source is finding the number of items available. */ -"Counting media items..." = "Преброяване на файловете..."; - /* Period Stats 'Countries' header */ "Countries" = "Държава"; @@ -703,7 +680,6 @@ /* Delete Delete button title for the warning shown to the user when he refuses to re-login when the authToken is missing. Title for button that permanently deletes a media item (photo / video) - Title for button that permanently deletes one or more media items (photos / videos) Title of a delete button Title of the trash confirmation alert. */ "Delete" = "Изтриване"; @@ -711,10 +687,7 @@ /* Menus confirmation button for deleting a menu. */ "Delete Menu" = "Изтриване на менюто"; -/* Delete option in the confirmation alert when deleting a page from the trash. - Delete option in the confirmation alert when deleting a post from the trash. - Label for a button permanently deletes a page. - Label for the delete post option. Tapping permanently deletes a post. +/* Delete option in the confirmation alert when deleting a post from the trash. Title for button on the comment details page that deletes the comment when tapped. */ "Delete Permanently" = "Изтриване завинаги"; @@ -788,7 +761,6 @@ /* Accessibility label for button to dismiss a bottom sheet Accessibility label for the transparent space above the login dialog which acts as a button to dismiss the dialog. Accessibility label for the transparent space above the signup dialog which acts as a button to dismiss the dialog. - Action to show on alert when view asset fails. Dismiss a view. Verb */ "Dismiss" = "Премахване"; @@ -797,9 +769,6 @@ User's Display Name */ "Display Name" = "Публично име"; -/* Accessibility label for other media items in the media collection view. The parameter is the filename file. */ -"Document: %@" = "Документ: %@"; - /* Noun. Title. Links to the Domains screen. */ "Domains" = "Домейни"; @@ -824,13 +793,9 @@ /* Name for the status of a draft post. */ "Draft" = "Чернова"; -/* Accessibility label for edit button to enable multi selection mode in the user's media library - Edit the post. - Editing GIF alert default action button. +/* Editing GIF alert default action button. Edits a Comment Edits the comment - Label for a button that opens the Edit Page view controller - Label for the edit post button. Tapping displays the editor. User action to edit media details. Verb, edit a comment */ "Edit" = "Редакция"; @@ -927,9 +892,6 @@ /* Title of error dialog when updating jetpack settins fail. */ "Error updating Jetpack settings" = "Грешкa при обновяването на настройките на Jetpack"; -/* Label for the post author filter. This filter shows posts for all users on the blog. */ -"Everyone" = "Всички"; - /* Placeholder text for the tagline of a site */ "Explain what this site is about." = "Описание на сайта."; @@ -1147,9 +1109,6 @@ /* Hint for image title on image settings. */ "Image title" = "Заглавие на изображението"; -/* Accessibility label for image thumbnails in the media collection view. The parameter is the creation date of the image. */ -"Image, %@" = "Изображение, %@"; - /* Undated post time label */ "Immediately" = "Веднага"; @@ -1420,7 +1379,6 @@ "Max Video Upload Size" = "Максимален размер за качване на видео"; /* Accessibility label for the Me button in My Site. - Label for the post author filter. This filter shows posts only authored by the current user. Me page title Noun. Title. Links to the Me screen. The accessibility value of the me tab. @@ -1428,20 +1386,11 @@ "Me" = "Аз"; /* Noun. Title. Links to the blog's Media library. - Tab bar title for the Media tab in Media Picker The menu item to select during a guided tour. - Title for Media Library section of the app. Title for the media section in site settings screen Title label for the media settings section in the app settings */ "Media" = "Файлове"; -/* Title for alert when access to media capture is not granted */ -"Media Capture" = "Описание"; - -/* Title for alert when a generic error happened when loading media - Title for alert when access to the media library is not granted by the user */ -"Media Library" = "Библиотека с медиа файлове"; - /* Message to indicate progress of uploading media to server */ "Media Uploading" = "Файловете се качват"; @@ -1471,24 +1420,15 @@ "Months and Years" = "Месеци и години"; /* Accessibility label for more button in dashboard quick start card. - Accessibility label for the More button in Page List. Accessibility label for the More button in Post List (compact view). Accessibility label for the More button on formatting toolbar. Accessibility label for the More button on Reader Cell Accessibility label for the More button on Reader's post details - Action button to display more available options - Label for the more post button. Tapping displays an action sheet with post options. */ + Action button to display more available options */ "More" = "Още"; -/* Label for a button that moves a page to the draft folder - Label for an option that moves a post to the draft folder */ -"Move to Draft" = "Чернова"; - -/* Label for a button that moves a page to the trash folder - Label for a option that moves a post to the trash folder - Title for button on the comment details page that moves the comment to trash when tapped. +/* Title for button on the comment details page that moves the comment to trash when tapped. Trash option in the trash confirmation alert. - Trash option in the trash page confirmation alert. Trashes the comment */ "Move to Trash" = "Преместване в кошчето"; @@ -1501,7 +1441,8 @@ Title of My Site tab */ "My Site" = "Моят сайт"; -/* Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ +/* Title for site picker screen. + Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ "My Sites" = "Моите сайтове"; /* 'Need help?' button label, links off to the WP for iOS FAQ. */ @@ -1640,7 +1581,6 @@ Alert dismissal title Button title. Acknowledges a prompt. Button title. An acknowledgement of the message displayed in a prompt. - Confirmation of action Default action Dismisses the alert Marks all notifications as read. @@ -1746,18 +1686,6 @@ Title shown when selecting a post type of Page from the Share Extension. */ "Page" = "Страница"; -/* Prompts the user that a restored page was moved to the drafts list. */ -"Page Restored to Drafts" = "Страницата бе възстановена като чернова"; - -/* Prompts the user that a restored page was moved to the published list. */ -"Page Restored to Published" = "Страницата бе възстановена към публикувана"; - -/* Prompts the user that a restored page was moved to the scheduled list. */ -"Page Restored to Scheduled" = "Страницата бе възстановена като насрочена."; - -/* A short message explaining that a page was moved to the trash bin. */ -"Page moved to trash." = "Страницата е преместестена във кошчето."; - /* Noun. Title. Links to the blog's Pages screen. The item to select during a guided tour. This is the section title @@ -1790,8 +1718,7 @@ Title of pending Comments filter. */ "Pending" = "На изчакване"; -/* Name for the status of a post pending review. - Title of the Pending Review Badge */ +/* Name for the status of a post pending review. */ "Pending review" = "Очаква отзив"; /* Noun. Title of the people management feature. @@ -1865,18 +1792,6 @@ The post formats available for the post. Should be the same as in core WP. */ "Post Format" = "Формат"; -/* Prompts the user that a restored post was moved to the drafts list. */ -"Post Restored to Drafts" = "Публикацията бе възстановена като чернова"; - -/* Prompts the user that a restored post was moved to the published list. */ -"Post Restored to Published" = "Публикацията е възстановена"; - -/* Prompts the user that a restored post was moved to the scheduled list. */ -"Post Restored to Scheduled" = "Публикацията е върната в чернова"; - -/* A short message explaining that a post was moved to the trash bin. */ -"Post moved to trash." = "Публикацията е изтрита."; - /* Insights 'Posting Activity' header Title for stats Posting Activity view. */ "Posting Activity" = "Активност"; @@ -1921,8 +1836,7 @@ "Privacy Policy" = "Поверителност"; /* Name for the status of a post that is marked private. - Privacy setting for posts set to 'Private'. Should be the same as in core WP. - Title of the Private Badge */ + Privacy setting for posts set to 'Private'. Should be the same as in core WP. */ "Private" = "Лична"; /* Message for the warning shown to the user when he refuses to re-login when the authToken is missing. */ @@ -1942,19 +1856,16 @@ Title for the publish settings view */ "Publish" = "Публикуване"; -/* A short phrase indicating a post is due to be immedately published. - Label for a button that moves a page to the published folder, publishing with the current date/time. */ +/* A short phrase indicating a post is due to be immedately published. */ "Publish Immediately" = "Публикуване веднага"; /* Label for a button that publishes the post - Label for an option that moves a publishes a post immediately Title of button allowing the user to immediately publish the post they are editing. */ "Publish Now" = "Публикуване"; /* Name for the status of a published post. Period Stats 'Published' header - Title of the published filter. This filter shows a list of posts that the user has published. - Title of the published header in search list. */ + Title of the published filter. This filter shows a list of posts that the user has published. */ "Published" = "Публикувано"; /* Published on [date] */ @@ -2022,8 +1933,7 @@ /* Label for selecting the related posts options */ "Related Posts" = "Подобни публикации"; -/* Add asset to media picker list - Alert button to confirm a plugin to be removed +/* Alert button to confirm a plugin to be removed Button label when removing a blog Label for the Remove Feature Image icon. Tapping will show a confirmation screen for removing the feature image from the post. Remove Action @@ -2079,9 +1989,6 @@ /* Setting: WordPress.com Surveys */ "Research" = "Изследване"; -/* Title of the reset button */ -"Reset" = "Нулиране"; - /* Screen title. Resize and crop an image. */ "Resize & Crop" = "Преоразмеряване и изрязване"; @@ -2102,12 +2009,9 @@ Button title, displayed when media has failed to upload. Allows the user to try the upload again. Button title. Retries uploading a post. If a user taps the button with this label, the action that evinced this error view will be retried. - Label for a button that attempts to re-upload a page that previously failed to upload. - Label for the retry post upload button. Tapping attempts to upload the post again. Opens the media library . Retries the upload of a user's post. Retry updating User's Role - Retry uploading the post. Retry. Action Retry. Verb – retry a failed media upload. The Jetpack view button title used when an error occurred @@ -2116,9 +2020,6 @@ User action to retry media upload. */ "Retry" = "Отново"; -/* User action to retry media upload. */ -"Retry Upload" = "Нов опит за качване"; - /* No comment provided by engineer. */ "Retry?" = "Отново?"; @@ -2155,11 +2056,7 @@ /* Title of button allowing users to change the status of the post they are currently editing to Draft. */ "Save as Draft" = "Запазване като чернова"; -/* Text displayed in HUD when a media item's metadata (title, etc) is saved successfully. */ -"Saved!" = "Запазена!"; - /* Menus save button title while it is saving a Menu. - Text displayed in HUD while a media item's metadata (title, etc) is being saved. Text displayed in HUD while a post is being saved as a draft. */ "Saving..." = "Запазване..."; @@ -2236,7 +2133,6 @@ /* Accessibility label for share buttons in nav bars Button label to share a post Button label to share a web page - Share the post. Shares the comment URL Spoken accessibility label Title for a button that allows the user to share their answer to the prompt. @@ -2384,8 +2280,7 @@ Title of Start Over settings page */ "Start Over" = "Започване отначало"; -/* Label for post stats option. Tapping displays statistics for a post. - Noun. Abbreviation of Statistics. Name of the Stats feature +/* Noun. Abbreviation of Statistics. Name of the Stats feature Noun. Abbv. of Statistics. Links to a blog's Stats screen. Stats 3D Touch Shortcut Stats window title @@ -2415,7 +2310,7 @@ Theme Support action title */ "Support" = "Помощ"; -/* Button used to switch site */ +/* Title for back button that leads to the site picker screen. */ "Switch Site" = "Превключване на сайта"; /* Accessibility Identifier for the H1 Aztec Style */ @@ -2473,8 +2368,7 @@ /* Title of a button style */ "Text Only" = "Само текст"; -/* Button title - The button's title text to send a 2FA code via SMS text message. */ +/* Button title */ "Text me a code instead" = "Изпращане на код чрез SMS"; /* Message of alert when theme activation succeeds */ @@ -2547,7 +2441,7 @@ /* People: Invitation Error */ "The user already has the specified role. Please, try assigning a different role." = "Потребителят вече има тази роля. Опитайте да назначите друга роля."; -/* Error message informing a user about an invalid password. */ +/* No comment provided by engineer. */ "The username or password stored in the app may be out of date. Please re-enter your password in the settings and try again." = "Потребителското име или паролата, запазени в приложението, може да не са актуални. Моля, въведете отново паролата в настройките и опитайте отново."; /* Title of alert when theme activation succeeds */ @@ -2598,9 +2492,6 @@ /* Explaining to the user there was an error trying to obtain the current location of the user. */ "There was a problem when trying to access your location. Please try again later." = "Възникна проблем докато се опитвахме да получим вашето местоположение. Моля опитайте по-късно."; -/* Explaining to the user there was an generic error accesing media. */ -"There was a problem when trying to access your media. Please try again later." = "Възникна грешка при достъпване на вашите медийни файлове. Моля, опитайте по-късно."; - /* Text displayed when there is a failure loading the plan list */ "There was an error loading plans" = "Възникна грешка при зареждане на плановете"; @@ -2613,9 +2504,6 @@ /* An error message display if the users device does not have a camera input available */ "This app needs permission to access the Camera to capture new media, please change the privacy settings if you wish to allow this." = "Това приложение се нуждае от разрешение да използва камерата, за да може да заснема. Моля, променете вашите настройки за поверителност ако искате да разрешите това."; -/* Explaining to the user why the app needs access to the device media library. */ -"This app needs permission to access your device media library in order to add photos and\/or video to your posts. Please change the privacy settings if you wish to allow this." = "Това приложение се нуждае от права да достъпва вашите медийни файлове, за да може да се добавят снимки и\/или видео в публикациите ви. Моля, променете вашите настройки за поверителност ако искате да разрешите това."; - /* Message shown when the reader finds no posts for the chosen site */ "This site has not posted anything yet. Try back later." = "Този сайт няма публикации. Моля опитайте по-късно."; @@ -2675,8 +2563,7 @@ /* Label displaying total number of WordPress.com followers. %@ is the total. */ "Total WordPress.com Followers: %@" = "Общо абонати в WordPress.com: %@"; -/* Accessibility label for trash button to delete items from the user's media library - Accessibility label for trash buttons in nav bars +/* Accessibility label for trash buttons in nav bars Trashes a comment Trashes the comment */ "Trash" = "Кошче"; @@ -2713,9 +2600,6 @@ /* Menus label for describing which menu the location uses in the header. */ "USES" = "ИЗПОЛЗВА"; -/* An error message. */ -"Unable to Connect" = "Неуспешно свързване"; - /* Title of a prompt saying the app needs an internet connection before it can load posts */ "Unable to Load Posts" = "Неуспешно зареждане на публикации"; @@ -2743,8 +2627,6 @@ Button title. Reverts the previous notification operation Revert an operation Revert enabling notification after successfully subcribing to the comments for the post. - The title of an 'undo' button. Tapping the button moves a trashed page out of the trash folder. - The title of an 'undo' button. Tapping the button moves a trashed post out of the trash folder. Undo action */ "Undo" = "Връщане"; @@ -2848,15 +2730,10 @@ /* Push Authentication Alert Title */ "Verify Log In" = "Потвърждение на влизането"; -/* Accessibility label for video thumbnails in the media collection view. The parameter is the creation date of the video. */ -"Video, %@" = "Видео, %@"; - /* Period Stats 'Videos' header */ "Videos" = "Видео клипове"; /* Button title. Displays a summary / sharing screen for a specific post. - Label for a button that opens the page when tapped. - Label for the view post button. Tapping displays the post as it appears on the web. Opens the post epilogue screen to allow sharing / viewing of a post. Theme View action title Verb. The screen title shown when viewing a post inside the app. */ @@ -2955,9 +2832,6 @@ /* Settings for a Wordpress Blog */ "WordPress Blog" = "WordPress Blog"; -/* Name for the WordPress Media Library */ -"WordPress Media" = "WordPress медия"; - /* Accessibility label for selecting an image or video from the user's WordPress media library on formatting toolbar. */ "WordPress Media Library" = "Медийна библиотека на WordPress"; @@ -3060,15 +2934,9 @@ /* Age between dates equaling one hour. */ "an hour" = "1 час"; -/* Label displayed on audio media items. */ -"audio" = "аудио"; - /* (placeholder) Help the user enter a URL into the field */ "http:\/\/my-site-address (URL)" = "http:\/\/my-site-address (Адрес)"; -/* Label displayed on image media items. */ -"image" = "изображение"; - /* This text is used when the user is configuring the iOS widget to suggest them to select the site to configure the widget for */ "ios-widget.gpCwrM" = "Select Site"; diff --git a/WordPress/Resources/cs.lproj/Localizable.strings b/WordPress/Resources/cs.lproj/Localizable.strings index d049a5d483bd..c439feda32b6 100644 --- a/WordPress/Resources/cs.lproj/Localizable.strings +++ b/WordPress/Resources/cs.lproj/Localizable.strings @@ -152,9 +152,6 @@ /* Difference label for Period Overview stat, indicating change from previous period. Ex: +99.9K (5%) */ "%@%@ (%@%%)" = "%1$@%2$@ (%3$@%%)"; -/* Accessibility label for a post in the post list. The parameters are the title, and date respectively. For example, \"Let it Go, 1 hour ago.\" */ -"%@, %@." = "%1$@, %2$@."; - /* Accessibility value for a Stats' Posting Activity Month if the user selected a day with posts. The first parameter is day (e.g. November 2019). The second parameter is the number of posts. */ "%@. %d posts." = "%1$@. %2$d příspěvky."; @@ -210,9 +207,6 @@ /* Age between dates over one year. */ "%d years" = "%d roky"; -/* Max image size in pixels (e.g. 300x300px) */ -"%dx%dpx" = "%1$dx%2$dpx"; - /* One menu area available in the theme */ "%i menu area in this theme" = "%i oblast pro menu v šabloně"; @@ -472,13 +466,6 @@ translators: Block name. %s: The localized block name */ /* Label for the Activity Type filter button when there are more than 1 activity type selected */ "Activity Type (%1$d)" = "Typ aktivit (%1$d)"; -/* Accessibility label for add button to add items to the user's media library - Remove asset from media picker list */ -"Add" = "Přidat"; - -/* Action for Media Picker to indicate selection of media. The argument in the string represents the number of elements (as numeric digits) selected */ -"Add %@" = "Přidat %@"; - /* No comment provided by engineer. */ "Add Block After" = "Přidat blok po"; @@ -569,9 +556,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Add menu item to children" = "Přidat položku menu pro děti"; -/* Accessibility hint for add button to add items to the user's media library */ -"Add new media" = "Přidat nová média"; - /* Screen reader text for Menus button that adds a new menu to a site. */ "Add new menu" = "Přidat nové menu"; @@ -634,9 +618,6 @@ translators: Block name. %s: The localized block name */ /* Option to select the Airmail app when logging in with magic links */ "Airmail" = "Letecká pošta"; -/* Description of albums in the photo libraries */ -"Albums" = "Alba"; - /* Image alignment option title. Title of the screen for choosing an image's alignment. */ "Alignment" = "Zarovnání"; @@ -714,9 +695,6 @@ translators: Block name. %s: The localized block name */ Label for the alt for a media asset (image) */ "Alt Text" = "Alternativní text"; -/* No comment provided by engineer. */ -"Alternatively, you can detach and edit these blocks separately by tapping “Detach patterns”." = "Alternativně můžete tyto bloky oddělit a upravit samostatně klepnutím na „Převést na běžné bloky“."; - /* Instruction text to explain to help users type their password instead of using magic link login option. */ "Alternatively, you may enter the password for this account." = "Případně můžete zadat heslo pro tento účet."; @@ -864,15 +842,9 @@ translators: Block name. %s: The localized block name */ /* Message prompting the user to confirm that they want to disconnect Jetpack from the site. */ "Are you sure you want to disconnect Jetpack from the site?" = "Opravdu chcete odpojit Jetpack od webové stránky?"; -/* Message prompting the user to confirm that they want to permanently delete a group of media items. */ -"Are you sure you want to permanently delete these items?" = "Opravdu chcete trvale odstranit vybrané položky?"; - /* Message prompting the user to confirm that they want to permanently delete a media item. Should match Calypso. */ "Are you sure you want to permanently delete this item?" = "Opravdu chcete tuto položku trvale smazat?"; -/* Message of the confirmation alert when deleting a page from the trash. */ -"Are you sure you want to permanently delete this page?" = "Jste si jistí, že chcete natrvalo smazat tuto stránku?"; - /* Message of the confirmation alert when deleting a post from the trash. */ "Are you sure you want to permanently delete this post?" = "Opravdu chcete trvale smazat tento příspěvek?"; @@ -904,9 +876,6 @@ translators: Block name. %s: The localized block name */ /* Title of message shown when user taps submit for review. */ "Are you sure you want to submit for review?" = "Opravdu chcete odeslat ke kontrole?"; -/* Message of the trash page confirmation alert. */ -"Are you sure you want to trash this page?" = "Jste si jistí, že chcete vyhodit tuto stránku?"; - /* Message of the trash confirmation alert. */ "Are you sure you want to trash this post?" = "Opravdu chcete odstranit tento příspěvek?"; @@ -947,9 +916,6 @@ translators: Block name. %s: The localized block name */ /* translators: accessibility text. Empty Audio caption. */ "Audio caption. Empty" = "Zvukový titulek. Prázdný"; -/* Accessibility label for audio items in the media collection view. The parameter is the creation date of the audio. */ -"Audio, %@" = "Audio, %@"; - /* No comment provided by engineer. */ "Authenticating" = "Ověřuji"; @@ -1217,9 +1183,6 @@ translators: Block name. %s: The localized block name */ /* Label for the post author in the post detail. */ "By " = "od "; -/* Accessibility label for the post author in the post list. The parameter is the author name. For example, \"By Elsa.\" */ -"By %@." = "Od %@."; - /* Legal disclaimer for logging in. The underscores _..._ denote underline. */ "By continuing, you agree to our _Terms of Service_." = "Pokračováním vyjadřujete souhlas s našimi _podmínkami služby_."; @@ -1239,8 +1202,7 @@ translators: Block name. %s: The localized block name */ /* Label for size of media while it's being calculated. */ "Calculating..." = "Výpočet..."; -/* Accessibility label for taking an image or video with the camera on formatting toolbar. - Accessibility label for the camera tile in the collection view */ +/* Accessibility label for taking an image or video with the camera on formatting toolbar. */ "Camera" = "Fotoaparát"; /* Title of an alert informing the user the camera permission for the app is disabled and its needed to proceed */ @@ -1291,10 +1253,7 @@ translators: Block name. %s: The localized block name */ Cancels an Action Cancels an alert. Cancels the mark all as read action. - Dismiss the post action sheet Editing GIF alert cancel action button. - Label for a cancel button - Label for the auto-upload cancelation button in the post list. Tapping will prevent the app from auto-uploading the post. Menus cancel button for deleting a menu. Menus cancel button within text bar while editing items. Menus: Cancel button title for canceling an edited menu item. @@ -1311,10 +1270,6 @@ translators: Block name. %s: The localized block name */ Verb. Title for Jetpack Restore cancel button. */ "Cancel" = "Zrušit"; -/* Label for the Post List option that cancels automatic uploading of a post. - Media Library option to cancel an in-progress or failed upload. */ -"Cancel Upload" = "Zrušit nahrávání"; - /* Menus alert button title to cancel discarding changes and not createa a new menu. Menus alert button title to cancel discarding changes and not select a new menu Menus alert button title to cancel discarding changes and not select a new menu location */ @@ -1373,9 +1328,6 @@ translators: Block name. %s: The localized block name */ Main title */ "Change Password" = "Změnit heslo"; -/* Title of button that takes user to the system Settings section for the app */ -"Change Settings" = "Změnit nastavení"; - /* Change Username title. */ "Change Username" = "Změnit uživatelské jméno"; @@ -1515,9 +1467,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Choose file" = "Vyberte soubor"; -/* Menu option for selecting media from the device's photo library. */ -"Choose from My Device" = "Vyberte ze seznamu moje zařízení"; - /* Explanatory text for Homepage Settings homepage type selection. */ "Choose from a homepage that displays your latest posts (classic blog) or a fixed \/ static page." = "Vyberte si z domovské stránky, která zobrazuje vaše nejnovější příspěvky (klasický blog), nebo pevné \/ statické stránky."; @@ -1718,9 +1667,6 @@ translators: Block name. %s: The localized block name */ /* Community & Non-Profit site intent topic */ "Community & Non-Profit" = "Komunita a neziskové organizace"; -/* Accessibility indication that the current Post List style is currently Compact. */ -"Compact" = "Kompaktní"; - /* The action is completed */ "Completed" = "Dokončeno"; @@ -1906,10 +1852,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Copied block" = "Zkopírovaný blok"; -/* Copy the post url and paste anywhere in phone - Label for page copy link. Tapping copy the url of page */ -"Copy Link" = "Kopírovat odkaz"; - /* Copy link to oomment button title */ "Copy Link to Comment" = "Zkopírovat odkaz na komentář"; @@ -2018,9 +1960,6 @@ translators: Block name. %s: The localized block name */ /* Error title displayed when unable to close user account. */ "Couldn’t close account automatically" = "Účet se nepodařilo zavřít automaticky"; -/* Message to show while media data source is finding the number of items available. */ -"Counting media items..." = "Počítám mediální soubory"; - /* Period Stats 'Countries' header */ "Countries" = "Země"; @@ -2268,7 +2207,6 @@ translators: Block name. %s: The localized block name */ /* Delete Delete button title for the warning shown to the user when he refuses to re-login when the authToken is missing. Title for button that permanently deletes a media item (photo / video) - Title for button that permanently deletes one or more media items (photos / videos) Title of a delete button Title of the trash confirmation alert. */ "Delete" = "Smazat"; @@ -2276,15 +2214,11 @@ translators: Block name. %s: The localized block name */ /* Menus confirmation button for deleting a menu. */ "Delete Menu" = "Smazat menu"; -/* Delete option in the confirmation alert when deleting a page from the trash. - Delete option in the confirmation alert when deleting a post from the trash. - Label for a button permanently deletes a page. - Label for the delete post option. Tapping permanently deletes a post. +/* Delete option in the confirmation alert when deleting a post from the trash. Title for button on the comment details page that deletes the comment when tapped. */ "Delete Permanently" = "Smazat trvale"; -/* Title of the confirmation alert when deleting a page from the trash. - Title of the confirmation alert when deleting a post from the trash. */ +/* Title of the confirmation alert when deleting a post from the trash. */ "Delete Permanently?" = "Trvale smazat?"; /* Button label for deleting the current site @@ -2397,7 +2331,6 @@ translators: Block name. %s: The localized block name */ /* Accessibility label for button to dismiss a bottom sheet Accessibility label for the transparent space above the login dialog which acts as a button to dismiss the dialog. Accessibility label for the transparent space above the signup dialog which acts as a button to dismiss the dialog. - Action to show on alert when view asset fails. Dismiss a view. Verb */ "Dismiss" = "Skrýt"; @@ -2415,12 +2348,6 @@ translators: Block name. %s: The localized block name */ User's Display Name */ "Display Name" = "Zobrazované jméno"; -/* Accessibility label for other media items in the media collection view. The parameter is the creation date of the document. */ -"Document, %@" = "Dokument, %@"; - -/* Accessibility label for other media items in the media collection view. The parameter is the filename file. */ -"Document: %@" = "Dokument: %@"; - /* Message shown when all tours have been completed */ "Doesn't it feel good to cross things off a list?" = "není to dobrý pocit, když věci odškrtnete ze seznamu?"; @@ -2581,8 +2508,7 @@ translators: Block name. %s: The localized block name */ /* Description of a Quick Start Tour */ "Draft and publish a post." = "Navrhněte a publikujte svůj první příspěvek."; -/* Title of the drafts filter. This filter shows a list of draft posts. - Title of the drafts header in search list. */ +/* Title of the drafts filter. This filter shows a list of draft posts. */ "Drafts" = "Koncepty"; /* No comment provided by engineer. */ @@ -2594,10 +2520,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Drag to adjust focal point" = "Potáhnutím upravíte ohnisko"; -/* Label for page duplicate option. Tapping creates a copy of the page. - Label for post duplicate option. Tapping creates a copy of the post. */ -"Duplicate" = "Duplikovat"; - /* No comment provided by engineer. */ "Duplicate block" = "Duplicitní blok"; @@ -2607,13 +2529,9 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Each block has its own settings. To find them, tap on a block. Its settings will appear on the toolbar at the bottom of the screen." = "Každý blok má své vlastní nastavení. Chcete-li je najít, klepněte na blok. Jeho nastavení se zobrazí na panelu nástrojů ve spodní části obrazovky."; -/* Accessibility label for edit button to enable multi selection mode in the user's media library - Edit the post. - Editing GIF alert default action button. +/* Editing GIF alert default action button. Edits a Comment Edits the comment - Label for a button that opens the Edit Page view controller - Label for the edit post button. Tapping displays the editor. User action to edit media details. Verb, edit a comment */ "Edit" = "Úpravy"; @@ -2621,9 +2539,6 @@ translators: Block name. %s: The localized block name */ /* Title for the edit more button section */ "Edit \"More\" button" = "Upravit \"více\" tlačítek"; -/* Button that displays the media editor to the user */ -"Edit %@" = "Upravit %@"; - /* Blocklist Keyword Edition Title */ "Edit Blocklist Word" = "Upravit seznam zablokovaných slov"; @@ -2810,9 +2725,6 @@ translators: Block name. %s: The localized block name */ /* Secondary message shown when there are no domains that match the user entered text. */ "Enter different words above and we'll look for an address that matches it." = "Výše zadejte různá slova a my vyhledáme adresu, která jí odpovídá."; -/* Accessibility hint for edit button to enable multi selection mode in the user's media library */ -"Enter edit mode to enable multi select to delete" = "Aktivujte režim úprav a povolte smazání vícenásobným výběrem"; - /* (placeholder) Help enter WordPress password Placeholder of a field to type a password to protect the post. */ "Enter password" = "Zadejte heslo"; @@ -2968,9 +2880,6 @@ translators: Block name. %s: The localized block name */ /* Short title telling the user they will receive a blogging reminder every day of the week. */ "Every day at %@" = "Každý den v %@"; -/* Label for the post author filter. This filter shows posts for all users on the blog. */ -"Everyone" = "Všichni"; - /* Example story title description */ "Example story title" = "Příklad názvu příběhu"; @@ -2980,9 +2889,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Excerpt length (words)" = "Délka výňatku (slova)"; -/* Accessibility label for a post's excerpt in the post list. The parameter is the post excerpt. For example, \"Excerpt. This is the first paragraph.\" */ -"Excerpt. %@." = "Výňatek. %@."; - /* Should be the same as the text displayed if the user clicks the (i) in Calypso. */ "Excerpts are optional hand-crafted summaries of your content." = "Výpisky jsou volitelná shrnutí vašeho obsahu."; @@ -2992,8 +2898,7 @@ translators: Block name. %s: The localized block name */ /* Accessibility Label for the exit full screen button on the full screen comment reply mode */ "Exit Full Screen" = "Ukončit celou obrazovku"; -/* Accessibility indication that the current Post List style is currently Expanded. - Screen reader text to represent the expanded state of a UI control */ +/* Screen reader text to represent the expanded state of a UI control */ "Expanded" = "Rozšířený"; /* Accessibility hint */ @@ -3043,9 +2948,6 @@ translators: Block name. %s: The localized block name */ The action failed */ "Failed" = "Selhalo"; -/* Error title when picked media cannot be imported into stories. */ -"Failed Media Export" = "Export médií se nezdařil"; - /* Message for mark all as read success notice */ "Failed marking Notifications as read" = "Oznámení se nepodařilo označit jako přečtené"; @@ -3564,8 +3466,7 @@ translators: Block name. %s: The localized block name */ "Home" = "Úvodní stránka"; /* Title for setting which shows the current page assigned as a site's homepage - Title for the homepage section in site settings screen - Title of the Homepage Badge */ + Title for the homepage section in site settings screen */ "Homepage" = "Domovská stránka"; /* Label for Homepage Settings site settings section @@ -3662,9 +3563,6 @@ translators: Block name. %s: The localized block name */ /* Hint for image title on image settings. */ "Image title" = "Nadpis obrázku"; -/* Accessibility label for image thumbnails in the media collection view. The parameter is the creation date of the image. */ -"Image, %@" = "Obrázek, %@"; - /* Undated post time label */ "Immediately" = "Okamžitě"; @@ -4132,9 +4030,6 @@ translators: Block name. %s: The localized block name */ Settings: Comments Approval settings */ "Links in comments" = "Odkazy v komentářích"; -/* The accessibility label for the list style button in the Post List. */ -"List style" = "Styl seznamu"; - /* Title of the screen that load selected the revisions. */ "Load" = "Načítání"; @@ -4150,18 +4045,12 @@ translators: Block name. %s: The localized block name */ /* Text displayed while loading the activity feed for a site */ "Loading Backups..." = "Načítání záloh..."; -/* Phrase to show when the user has searched for GIFs and they are being loaded. */ -"Loading GIFs..." = "Načítání GIFů..."; - /* Menus label text displayed while menus are loading */ "Loading Menus..." = "Načítání menu..."; /* Text displayed while loading site People. */ "Loading People..." = "Načítání lidí ..."; -/* Phrase to show when the user has searched for images and they are being loaded. */ -"Loading Photos..." = "Načítání fotografií..."; - /* Text displayed while loading plans details */ "Loading Plan..." = "Nahrávám plán..."; @@ -4222,8 +4111,7 @@ translators: Block name. %s: The localized block name */ /* Local Services site intent topic */ "Local Services" = "Místní služby"; -/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. - Title of the Local Changes Badge */ +/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. */ "Local changes" = "Místní změny"; /* Title for alert when a generic error happened when trying to find the location of the device */ @@ -4384,7 +4272,6 @@ translators: Block name. %s: The localized block name */ "Max Video Upload Size" = "Maximální velikost nahraného videa"; /* Accessibility label for the Me button in My Site. - Label for the post author filter. This filter shows posts only authored by the current user. Me page title Noun. Title. Links to the Me screen. The accessibility value of the me tab. @@ -4392,9 +4279,7 @@ translators: Block name. %s: The localized block name */ "Me" = "Já"; /* Noun. Title. Links to the blog's Media library. - Tab bar title for the Media tab in Media Picker The menu item to select during a guided tour. - Title for Media Library section of the app. Title for the media section in site settings screen Title label for the media settings section in the app settings */ "Media" = "Media"; @@ -4406,13 +4291,6 @@ translators: Block name. %s: The localized block name */ /* Label for size of media cache in the app. */ "Media Cache Size" = "Velikost cache médií"; -/* Title for alert when access to media capture is not granted */ -"Media Capture" = "Zachytit médii"; - -/* Title for alert when a generic error happened when loading media - Title for alert when access to the media library is not granted by the user */ -"Media Library" = "Knihovna médií"; - /* Title for action sheet with media options. */ "Media Options" = "Nastavení médií"; @@ -4435,9 +4313,6 @@ translators: Block name. %s: The localized block name */ /* translators: %s: block title e.g: \"Paragraph\". */ "Media options" = "Nastavení médií"; -/* Alert title when there is issues loading an asset to preview. */ -"Media preview failed." = "Náhled médií selhal."; - /* Alert displayed to the user when multiple media items have uploaded successfully. */ "Media uploaded (%ld files)" = "Nahraná média (%ld soubory)"; @@ -4475,9 +4350,6 @@ translators: Block name. %s: The localized block name */ Label for the share message field on the post settings. */ "Message" = "Zpráva"; -/* Title of section containing image / video metadata such as size and file type */ -"Metadata" = "Metadata"; - /* Option to select the Microsft Outlook app when logging in with magic links */ "Microsoft Outlook" = "Microsoft Outlook"; @@ -4497,13 +4369,11 @@ translators: Block name. %s: The localized block name */ "Months and Years" = "Měsíce a Roky"; /* Accessibility label for more button in dashboard quick start card. - Accessibility label for the More button in Page List. Accessibility label for the More button in Post List (compact view). Accessibility label for the More button on formatting toolbar. Accessibility label for the More button on Reader Cell Accessibility label for the More button on Reader's post details - Action button to display more available options - Label for the more post button. Tapping displays an action sheet with post options. */ + Action button to display more available options */ "More" = "Více"; /* Action button to display more available options @@ -4561,15 +4431,8 @@ translators: Block name. %s: The localized block name */ /* Screen reader text for button that will move the menu item */ "Move menu item" = "Přesunout položku menu"; -/* Label for a button that moves a page to the draft folder - Label for an option that moves a post to the draft folder */ -"Move to Draft" = "Přesunout mezi koncepty"; - -/* Label for a button that moves a page to the trash folder - Label for a option that moves a post to the trash folder - Title for button on the comment details page that moves the comment to trash when tapped. +/* Title for button on the comment details page that moves the comment to trash when tapped. Trash option in the trash confirmation alert. - Trash option in the trash page confirmation alert. Trashes the comment */ "Move to Trash" = "Přesunout do koše"; @@ -4601,7 +4464,8 @@ translators: Block name. %s: The localized block name */ Title of My Site tab */ "My Site" = "Moje weby"; -/* Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ +/* Title for site picker screen. + Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ "My Sites" = "Moje weby"; /* Siri Suggestion to open My Sites */ @@ -4848,9 +4712,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title for the view when there aren't any Activities to display in the Activity Log for a given filter. */ "No matching events found." = "Nebyly nalezeny žádné odpovídající události."; -/* Message displayed when no results are returned from a media library search. Should match Calypso. - Phrase to show when the user search for images but there are no result to show. - Phrase to show when the user searches for GIFs but there are no result to show. */ +/* Message displayed when no results are returned from a media library search. Should match Calypso. */ "No media matching your search" = "Vašemu vyhledávání neodpovídají žádná média"; /* Menus text shown when no menus were available for loading the Menus editor. */ @@ -4868,8 +4730,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Displayed in the Notifications Tab as a title, when there are no notifications */ "No notifications yet" = "Zatím nejsou žádná oznámení"; -/* Displayed when the user is searching the pages list and there are no matching pages - Text displayed when there's no matching with the text search */ +/* Text displayed when there's no matching with the text search */ "No pages matching your search" = "Vyhledávání neodpovídají žádné stránky"; /* Text displayed when search for plugins returns no results */ @@ -4890,9 +4751,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Message shown whent the reader finds no posts for the chosen tag */ "No posts have been made recently with this tag." = "Žádné příspěvku nebyly v nedávné době označené tímto štítkem."; -/* Displayed when the user is searching the posts list and there are no matching posts */ -"No posts matching your search" = "Vyhledávání neodpovídají žádné příspěvky"; - /* Accessibility value for a Stats' Posting Activity Month if there are no posts. */ "No posts." = "Žádné příspěvky."; @@ -4990,9 +4848,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* A message title */ "Nothing liked yet" = "Zatím se nic nelíbilo"; -/* Default message for empty media picker */ -"Nothing to show" = "Nic k vidění"; - /* Notifications Details Accessibility Identifier */ "Notification Details Table" = "Detailní tabulka notifikací"; @@ -5052,7 +4907,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Alert dismissal title Button title. Acknowledges a prompt. Button title. An acknowledgement of the message displayed in a prompt. - Confirmation of action Default action Dismisses the alert Marks all notifications as read. @@ -5114,9 +4968,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* No comment provided by engineer. */ "Only show excerpt" = "Zobrazit pouze výňatek"; -/* Message telling the user that they've only enabled limited photo library permissions for the app. */ -"Only the selected photos you've given access to are available." = "Dostupné jsou pouze vybrané fotografie, ke kterým jste udělili přístup."; - /* An informal exclaimation that means `something went wrong`. Title for the view when there's an error loading a comment. Title for the view when there's an error loading Activity Log @@ -5151,9 +5002,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title of a button that opens the apps settings in the system Settings.app */ "Open Settings" = "Otevřít nastavení"; -/* Editor button to swich the media picker from quick mode to full picker */ -"Open full media picker" = "Otevřít úplný výběr médií"; - /* No comment provided by engineer. */ "Open in Safari" = "Otevřít v Safari"; @@ -5251,15 +5099,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title shown when selecting a post type of Page from the Share Extension. */ "Page" = "Stránka"; -/* Prompts the user that a restored page was moved to the drafts list. */ -"Page Restored to Drafts" = "Stránka obnovena do konceptů"; - -/* Prompts the user that a restored page was moved to the published list. */ -"Page Restored to Published" = "Stránka obnovena do publikovaných"; - -/* Prompts the user that a restored page was moved to the scheduled list. */ -"Page Restored to Scheduled" = "Stránka obnovena do plánovaných"; - /* Name of the button to open the page settings The title of the Page Settings screen. */ "Page Settings" = "Nastavení stránky"; @@ -5276,9 +5115,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of notification displayed when a page has failed to upload. */ "Page failed to upload" = "Stránku se nepodařilo nahrát"; -/* A short message explaining that a page was moved to the trash bin. */ -"Page moved to trash." = "Stránka přesunuta do koše."; - /* Title of notification displayed when a page has been successfully saved as a draft. */ "Page pending review" = "Stránka čekající na schválení"; @@ -5350,8 +5186,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title of pending Comments filter. */ "Pending" = "Čekající na schválení"; -/* Name for the status of a post pending review. - Title of the Pending Review Badge */ +/* Name for the status of a post pending review. */ "Pending review" = "Čeká na schválení"; /* Noun. Title of the people management feature. @@ -5380,12 +5215,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Photography site intent topic */ "Photography" = "Fotografie"; -/* Tab bar title for the Photos tab in Media Picker */ -"Photos" = "Fotky"; - -/* Subtitle for placeholder in Free Photos. The company name 'Pexels' should always be written as it is. */ -"Photos provided by Pexels" = "Fotografie poskytla společnost Pexels"; - /* Title for selecting a new username in the site creation flow. */ "Pick username" = "Vyberte uživatelské jméno"; @@ -5471,9 +5300,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Instructional text shown when requesting the user's password for a login initiated via Sign In with Apple */ "Please enter the password for your WordPress.com account to log in with your Apple ID." = "Chcete-li se přihlásit pomocí svého Apple ID, zadejte heslo ke svému účtu WordPress.com."; -/* Instruction text on the two-factor screen. */ -"Please enter the verification code from your authenticator app, or tap the link below to receive a code via SMS." = "Zadejte ověřovací kód z aplikace ověřovatele nebo klepnutím na odkaz níže obdržíte kód prostřednictvím SMS."; - /* Popup message to ask for user credentials (fields shown below). */ "Please enter your credentials" = "Zadejte prosím své přihlašovací údaje"; @@ -5567,15 +5393,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ The post formats available for the post. Should be the same as in core WP. */ "Post Format" = "Formát příspěvku"; -/* Prompts the user that a restored post was moved to the drafts list. */ -"Post Restored to Drafts" = "Příspěvek byl obnoven jako koncept"; - -/* Prompts the user that a restored post was moved to the published list. */ -"Post Restored to Published" = "Příspěvek byl obnoven jako Publikovaný"; - -/* Prompts the user that a restored post was moved to the scheduled list. */ -"Post Restored to Scheduled" = "Příspěvek byl obnoven jako Naplánovaný"; - /* Name of the button to open the post settings The title of the Post Settings screen. */ "Post Settings" = "Nastavení příspěvku"; @@ -5595,9 +5412,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of notification displayed when a post has failed to upload. */ "Post failed to upload" = "Nahrávání příspěvku se nezdařilo"; -/* A short message explaining that a post was moved to the trash bin. */ -"Post moved to trash." = "Příspěvek byl přesunut do koše."; - /* Title of notification displayed when a post has been successfully saved as a draft. */ "Post pending review" = "Příspěvek čeká na schválení"; @@ -5656,9 +5470,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Period Stats 'Posts and Pages' header */ "Posts and Pages" = "Příspěvky a stránky"; -/* Title of the Posts Page Badge */ -"Posts page" = "Stránka příspěvků"; - /* Message informing the user that their static homepage for posts was set successfully */ "Posts page successfully updated" = "Stránka příspěvků byla úspěšně aktualizována"; @@ -5671,9 +5482,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* A message explaining the Posts I Like feature in the reader */ "Posts that you like will appear here." = "Zde se objeví příspěvky, které se vám líbí."; -/* Subtitle for placeholder in Tenor picker. `The company name 'Tenor' should always be written as it is. */ -"Powered by Tenor" = "Běží na Tenor"; - /* Browse premium themes selection title */ "Premium" = "Premium"; @@ -5692,18 +5500,12 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title for screen to preview a static content. */ "Preview" = "Náhled"; -/* Action for Media Picker to preview the selected media items. The argument in the string represents the number of elements (as numeric digits) selected */ -"Preview %@" = "Náhled %@"; - /* Title for web preview device switching button */ "Preview Device" = "Náhled zařízení"; /* Title on display preview error */ "Preview Unavailable" = "Náhled není k dispozici"; -/* Accessibility label for media item preview for user's viewing an item in their media library */ -"Preview media" = "Náhled média"; - /* No comment provided by engineer. */ "Preview page" = "Náhled stránky"; @@ -5747,8 +5549,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Privacy notice for California users" = "Oznámení o ochraně osobních údajů pro uživatele Kalifornie"; /* Name for the status of a post that is marked private. - Privacy setting for posts set to 'Private'. Should be the same as in core WP. - Title of the Private Badge */ + Privacy setting for posts set to 'Private'. Should be the same as in core WP. */ "Private" = "Soukromé"; /* No comment provided by engineer. */ @@ -5798,12 +5599,10 @@ translators: %s: Select control button label e.g. \"Button width\" */ Label for the publish date button. */ "Publish Date" = "Datum publikování"; -/* A short phrase indicating a post is due to be immedately published. - Label for a button that moves a page to the published folder, publishing with the current date/time. */ +/* A short phrase indicating a post is due to be immedately published. */ "Publish Immediately" = "Publikovat okamžitě"; /* Label for a button that publishes the post - Label for an option that moves a publishes a post immediately Title of button allowing the user to immediately publish the post they are editing. */ "Publish Now" = "Publikovat"; @@ -5821,8 +5620,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Name for the status of a published post. Period Stats 'Published' header - Title of the published filter. This filter shows a list of posts that the user has published. - Title of the published header in search list. */ + Title of the published filter. This filter shows a list of posts that the user has published. */ "Published" = "Publikováno"; /* Precedes the name of the blog just posted on */ @@ -5961,8 +5759,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of the completion screen of the Blogging Reminders Settings screen when the reminders are removed. */ "Reminders removed" = "Připomenutí odstraněny!"; -/* Add asset to media picker list - Alert button to confirm a plugin to be removed +/* Alert button to confirm a plugin to be removed Button label when removing a blog Label for the Remove Feature Image icon. Tapping will show a confirmation screen for removing the feature image from the post. Remove Action @@ -6112,9 +5909,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of secondary button on alert prompting verify their accounts while attempting to publish */ "Resend" = "Přeposlat"; -/* Title of the reset button */ -"Reset" = "Obnovit"; - /* Accessibility label for the reset activity type button */ "Reset Activity Type filter" = "Obnovit filtr typu aktivity"; @@ -6169,12 +5963,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ Button title, displayed when media has failed to upload. Allows the user to try the upload again. Button title. Retries uploading a post. If a user taps the button with this label, the action that evinced this error view will be retried. - Label for a button that attempts to re-upload a page that previously failed to upload. - Label for the retry post upload button. Tapping attempts to upload the post again. Opens the media library . Retries the upload of a user's post. Retry updating User's Role - Retry uploading the post. Retry. Action Retry. Verb – retry a failed media upload. The Jetpack view button title used when an error occurred @@ -6186,9 +5977,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Button title that triggers a scan */ "Retry Scan" = "Opakovat skenování"; -/* User action to retry media upload. */ -"Retry Upload" = "Opakovat nahrávání"; - /* User action to retry all failed media uploads. */ "Retry all" = "Zopakujte všechno"; @@ -6286,9 +6074,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Accessibility label for the 'Save Post' button when a post has been saved. */ "Saved Post" = "Uložený příspěvek"; -/* Text displayed in HUD when a media item's metadata (title, etc) is saved successfully. */ -"Saved!" = "Uloženo!"; - /* Accessibility hint for the 'Save Post' button. */ "Saves this post for later." = "Uloží tento příspěvek na později."; @@ -6299,7 +6084,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Saving post…" = "Ukládání příspěvku..."; /* Menus save button title while it is saving a Menu. - Text displayed in HUD while a media item's metadata (title, etc) is being saved. Text displayed in HUD while a post is being saved as a draft. */ "Saving..." = "Ukládání..."; @@ -6387,21 +6171,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* No comment provided by engineer. */ "Search or type URL" = "Vyhledejte nebo zadejte URL"; -/* Text displayed when the search controller will be presented */ -"Search pages" = "Hledat stránku"; - -/* Text displayed when the search controller will be presented */ -"Search posts" = "Hledat příspěvky"; - /* No comment provided by engineer. */ "Search settings" = "Nastavení vyhledávání"; -/* Title for placeholder in Tenor picker */ -"Search to find GIFs to add to your Media Library!" = "Vyhledejte GIF a přidejte jej do knihovny médií!"; - -/* Title for placeholder in Free Photos */ -"Search to find free photos to add to your Media Library!" = "Vyhledejte fotografie zdarma, které chcete přidat do knihovny médií!"; - /* Menus search bar placeholder text. */ "Search..." = "Hledání..."; @@ -6472,9 +6244,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Register Domain - Domain contact information field placeholder for Country */ "Select Country" = "Vybrat zemi"; -/* Title of button that allows the user to select more photos to access within the app */ -"Select More" = "Vybrat více"; - /* Blog Picker's Title */ "Select Site" = "Vybrat web"; @@ -6496,9 +6265,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Register domain - Title for the Choose domain button of Suggested domains screen */ "Select domain" = "Vybrat doménu"; -/* Accessibility hint for actions when displaying media items. */ -"Select media." = "Vybrat soubory"; - /* Accessibility label for selecting paragraph style button on formatting toolbar. */ "Select paragraph style" = "Vyberte styl odstavce"; @@ -6602,19 +6368,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label for connected service in Publicize stat. */ "Service" = "Služba"; -/* Label for a button that opens the Set Parent options view controller - Navigation title displayed on the navigation bar */ +/* Navigation title displayed on the navigation bar */ "Set Parent" = "Nastavit nadřazené"; /* No comment provided by engineer. */ "Set as Featured Image" = "Použít jako náhledový obrázek"; -/* Label for a button that sets the selected page as the site's Homepage */ -"Set as Homepage" = "Nastavit jako hlavní stránku"; - -/* Label for a button that sets the selected page as the site's Posts page */ -"Set as Posts Page" = "Nastavit jako stránku příspěvků"; - /* Notice confirming that an image has been set as the post's featured image. */ "Set as featured image" = "Použít jako náhledový obrázek"; @@ -6658,7 +6417,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Accessibility label for share buttons in nav bars Button label to share a post Button label to share a web page - Share the post. Shares the comment URL Spoken accessibility label Title for a button that allows the user to share their answer to the prompt. @@ -7044,8 +6802,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Name of setting configured when a site uses a static page as its homepage */ "Static Homepage" = "Statická domovská stránka"; -/* Label for post stats option. Tapping displays statistics for a post. - Noun. Abbreviation of Statistics. Name of the Stats feature +/* Noun. Abbreviation of Statistics. Name of the Stats feature Noun. Abbv. of Statistics. Links to a blog's Stats screen. Stats 3D Touch Shortcut Stats window title @@ -7076,9 +6833,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label text that defines a post marked as sticky */ "Sticky" = "Zvýrazněný"; -/* Accessibility label for a sticky post in the post list. */ -"Sticky." = "Připnuté."; - /* User action to stop upload. */ "Stop upload" = "Zastavit nahrání"; @@ -7135,7 +6889,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ Theme Support action title */ "Support" = "Podpora"; -/* Button used to switch site */ +/* Title for back button that leads to the site picker screen. */ "Switch Site" = "Přepnout web"; /* Switches the Editor to HTML Mode */ @@ -7220,9 +6974,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label explaining why users might want to add tags. */ "Tags help tell readers what a post is about. Separate different tags with commas." = "Štítky pomáhají čtenářům říct, o čem je příspěvek. Jednotlivé značky oddělte čárkami."; -/* Menu option for taking an image or video with the device's camera. */ -"Take Photo or Video" = "Vybrat fotku nebo video"; - /* No comment provided by engineer. */ "Take a Photo" = "Udělat fotku"; @@ -7290,12 +7041,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Accessibility hint */ "Tap to select the previous period" = "Klepnutím vyberte předchozí období"; -/* Accessibility hint for button used to switch site */ -"Tap to switch to another site, or add a new site" = "Klepnutím přepnete na jiný web nebo přidáte nový web"; - -/* Accessibility hint for media item preview for user's viewing an item in their media library */ -"Tap to view media in full screen" = "Klepnutím zobrazíte média na celé obrazovce"; - /* Accessibility hint for a button that opens a new view with more details. */ "Tap to view more details." = "Klepnutím zobrazíte další podrobnosti."; @@ -7341,8 +7086,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* No comment provided by engineer. */ "Text formatting controls are located within the toolbar positioned above the keyboard while editing a text block" = "Ovládací prvky formátování textu se při úpravě textového bloku nacházejí na panelu nástrojů nad klávesnicí"; -/* Button title - The button's title text to send a 2FA code via SMS text message. */ +/* Button title */ "Text me a code instead" = "Pošlete mi raději kód."; /* Message of alert when theme activation succeeds */ @@ -7372,9 +7116,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Error message shown to a user who is trying to share to Facebook but does not have any available Facebook Pages. */ "The Facebook connection cannot find any Pages. Publicize cannot connect to Facebook Profiles, only published Pages." = "Připojení k Facebooku nemůže najít žádné stránky. Publicita se nemůže připojit k profilům Facebooku, pouze k publikovaným stránkám."; -/* Message shown when a GIF failed to load while trying to add it to the Media library. */ -"The GIF could not be added to the Media Library." = "GIF nelze přidat do knihovny médií."; - /* Description shown when a user logs in with Google but no matching WordPress.com account is found */ "The Google account \"%@\" doesn't match any account on WordPress.com" = "Účet Google \"%@\" neodpovídá žádnému účtu na WordPress.com"; @@ -7502,7 +7243,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Error message shown when user attempts to remove the site owner. */ "The user you are trying to remove is the owner of this site. Please contact support for assistance." = "Uživatel, kterého se pokoušíte odebrat, je vlastníkem tohoto webu. Požádejte o pomoc podporu."; -/* Error message informing a user about an invalid password. */ +/* No comment provided by engineer. */ "The username or password stored in the app may be out of date. Please re-enter your password in the settings and try again." = "Uživatelské jméno nebo heslo uložené v aplikaci může být zastaralé. Zadejte znovu heslo v nastavení a zkuste to znovu."; /* Message shown when a video failed to load while trying to add it to the Media library. */ @@ -7570,9 +7311,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* A short error message letting the user know about a problem displaying a post. */ "There was a problem displaying this post." = "Došlo k potížím při zobrazování tohoto příspěvku."; -/* Error message displayed when the Media Library is unable to load a full sized preview of an item. */ -"There was a problem loading the media item." = "Vyskytl se problém s načtením mediální položky."; - /* The loading view subtitle displayed when an error occurred */ "There was a problem loading your data, refresh your page to try again." = "Při načítání dat došlo k potížím. Obnovte stránku a zkuste to znovu."; @@ -7585,9 +7323,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Explaining to the user there was an error trying to obtain the current location of the user. */ "There was a problem when trying to access your location. Please try again later." = "Došlo k chybě při pokusu načíst vaší polohu. Zkuste to později znovu."; -/* Explaining to the user there was an generic error accesing media. */ -"There was a problem when trying to access your media. Please try again later." = "Došlo k potížím při pokusu o přístup k médiím. Zkuste to znovu později."; - /* Message for stories unknown error. */ "There was a problem with the Stories editor. If the problem persists you can contact us via the Me > Help & Support screen." = "Došlo k potížím při začátku sledování webu. Pokud problém přetrvává můžete nás kontaktovat prostřednictvím > Pomoc a podpora. "; @@ -7658,9 +7393,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* A description informing the user in order to proceed with this feature we will need camera permissions, and how to enable it. */ "This app needs permission to access the Camera to scan login codes, tap on the Open Settings button to enable it." = "Tato aplikace potřebuje oprávnění pro přístup k fotoaparátu, aby mohla skenovat přihlašovací kódy, klepnutím na tlačítko Otevřít nastavení ji povolte."; -/* Explaining to the user why the app needs access to the device media library. */ -"This app needs permission to access your device media library in order to add photos and\/or video to your posts. Please change the privacy settings if you wish to allow this." = "Tato aplikace vyžaduje povolení pro přístupu ke knihovnu médií v zařízení, aby bylo možné přidat fotografie a\/nebo videa do příspěvků. Pokud si to přejete, změňte nastavení soukromí"; - /* No comment provided by engineer. */ "This color combination may be hard for people to read. Try using a brighter background color and\/or a darker text color." = "Tato barevná kombinace může být špatně čitelná. Zkuste použít jasnější barvu pozadí a\/nebo tmavší barvu textu."; @@ -7899,18 +7631,11 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* No comment provided by engineer. */ "Transform block…" = "Změnit blok..."; -/* Accessibility label for trash button to delete items from the user's media library - Accessibility label for trash buttons in nav bars +/* Accessibility label for trash buttons in nav bars Trashes a comment Trashes the comment */ "Trash" = "Odstranit"; -/* Accessibility hint for trash button to delete items from the user's media library */ -"Trash selected media" = "Odstraňte vybraná média do koše"; - -/* Title of the trash page confirmation alert. */ -"Trash this page?" = "Smazat tuto stránku?"; - /* Title of the trash confirmation alert. */ "Trash this post?" = "Smazat tento příspěvek?"; @@ -8025,9 +7750,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Shown when a user logs in with Google but it subsequently fails to work as login to WordPress.com */ "Unable To Connect" = "Připojení se nezdařilo"; -/* An error message. */ -"Unable to Connect" = "Připojení se nezdařilo"; - /* Title for stories unknown error. */ "Unable to Create Stories Editor" = "Nelze vytvořit editor příběhů"; @@ -8043,9 +7765,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* An error message shown when there is an issue creating new invite links. */ "Unable to create new invite links." = "Nelze vytvořit nové odkazy na pozvánky."; -/* Text displayed in HUD if there was an error attempting to delete a group of media items. */ -"Unable to delete all media items." = "Nelze smazat všechny mediální položky."; - /* Text displayed in HUD if there was an error attempting to delete a media item. */ "Unable to delete media item." = "Nelze smazat mediální položku."; @@ -8109,12 +7828,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Message displayed when sharing a link to the downloadable backup fails. */ "Unable to share link" = "Odkaz nelze sdílet"; -/* Message that appears when a user tries to trash a page while their device is offline. */ -"Unable to trash pages while offline. Please try again later." = "V režimu offline nelze stránky vyhodit do koše. Prosím zkuste to znovu později."; - -/* Message that appears when a user tries to trash a post while their device is offline. */ -"Unable to trash posts while offline. Please try again later." = "V režimu offline nelze příspěvky vyhodit do koše. Prosím zkuste to znovu později."; - /* Notice title when turning site notifications off fails. */ "Unable to turn off site notifications" = "Nelze vypnout notifikace webu"; @@ -8187,8 +7900,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ Button title. Reverts the previous notification operation Revert an operation Revert enabling notification after successfully subcribing to the comments for the post. - The title of an 'undo' button. Tapping the button moves a trashed page out of the trash folder. - The title of an 'undo' button. Tapping the button moves a trashed post out of the trash folder. Undo action */ "Undo" = "Zpět"; @@ -8228,9 +7939,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title for Unknown HTML Editor */ "Unknown HTML" = "Neznámé HTML"; -/* Label to use when creation date from media asset is not know. */ -"Unknown creation date" = "Neznámé datum vytvoření"; - /* No comment provided by engineer. */ "Unknown error" = "Neznámá chyba"; @@ -8468,15 +8176,10 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of an alert informing users that the video they are trying to select is not allowed. */ "Video not uploaded" = "Video soubor nebyl nahrán"; -/* Accessibility label for video thumbnails in the media collection view. The parameter is the creation date of the video. */ -"Video, %@" = "Video, %@"; - /* Period Stats 'Videos' header */ "Videos" = "Videa"; /* Button title. Displays a summary / sharing screen for a specific post. - Label for a button that opens the page when tapped. - Label for the view post button. Tapping displays the post as it appears on the web. Opens the post epilogue screen to allow sharing / viewing of a post. Theme View action title Verb. The screen title shown when viewing a post inside the app. */ @@ -8992,9 +8695,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Siri Suggestion to open Support */ "WordPress Help" = "Nápověda WordPress"; -/* Name for the WordPress Media Library */ -"WordPress Media" = "WordPress média"; - /* Accessibility label for selecting an image or video from the user's WordPress media library on formatting toolbar. */ "WordPress Media Library" = "WordPress knihovna médií"; @@ -9303,9 +9003,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Share extension error dialog text. */ "Your account does not have permission to upload media to this site. The Site Administrator can change these permissions." = "Váš účet nemá oprávnění k nahrávání médií na tuto stránku. Správce stránky může tyto oprávnění změnit."; -/* Explaining to the user why the app needs access to the device media library. */ -"Your app is not authorized to access media library due to active restrictions such as parental controls. Please check your parental control settings in this device." = "Vaše aplikace nemá oprávnění k přístupu do knihovny médií kvůli aktivním omezením, jako jsou rodičovská kontrola. Zkontrolujte nastavení rodičovského zámku v tomto zařízení."; - /* Title for the Jetpack Backup Complete message. */ "Your backup is now available for download" = "Vaše záloha je nyní k dispozici ke stažení"; @@ -9324,9 +9021,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of the site address section in the Domains Dashboard. */ "Your free WordPress.com address is" = "Vaše bezplatná adresa na WordPress.com je"; -/* Error message when picked media cannot be imported into stories. */ -"Your media could not be exported. If the problem persists you can contact us via the Me > Help & Support screen." = "Vaše média se nepodařilo exportovat. Pokud problém přetrvává, můžete nás kontaktovat prostřednictvím obrazovky Já > Nápověda a podpora."; - /* Details about recently acquired domain on domain credit redemption success screen */ "Your new domain %@ is being set up. It may take up to 30 minutes for your domain to start working." = "Vaše nová doména %@ se nastavuje. Může trvat až 30 minut, než vaše doména začne fungovat."; @@ -9444,9 +9138,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Age between dates equaling one hour. */ "an hour" = "hodina"; -/* Label displayed on audio media items. */ -"audio" = "audio"; - /* translators: displays audio file extension. e.g. MP3 audio file */ "audio file" = "zvukový soubor"; @@ -9590,9 +9281,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* (placeholder) Help the user enter a URL into the field */ "http:\/\/my-site-address (URL)" = "http:\/\/moje-webova-adresa (URL)"; -/* Label displayed on image media items. */ -"image" = "obrázky"; - /* Sentence to justify why the app is asking permission from the user to use their camera. */ "infoplist.NSCameraUsageDescription" = "Pořizovat fotky a videa a umožnit použití ve vašich příspěvcích. "; @@ -9839,9 +9527,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Indicating that referrer was marked as spam */ "marked as spam" = "Označeno jako spam"; -/* Verb. User action to dismiss error alert when failing to load media item. */ -"mediaItemTable.errorAlert.dismissButton" = "Odmítnout"; - /* Verb. Button title. Tapping dismisses a prompt. */ "mediaLibrary.retryOptionsAlert.dismissButton" = "Odmítnout"; @@ -9857,9 +9542,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* The title in the Delete WordPress screen */ "migration.deleteWordpress.title" = "Už nepotřebujete WordPress aplikaci"; -/* Primary button title in the migration done screen. */ -"migration.done.actions.primary.title" = "Dokončit"; - /* Highlighted text in the footer of the migration done screen. */ "migration.done.footer.highlighted" = "Odstraňte WordPress aplikaci"; @@ -9932,12 +9614,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* This is one of the buttons we display when prompting the user for a review */ "notifications.appRatings.sendFeedback.yes.buttonTitle" = "Odeslat zpětnou vazbu"; -/* Word separating the current index from the total amount. I.e.: 7 of 9 */ -"of" = "z"; - -/* Label displayed on media items that are not video, image, or audio. */ -"other" = "ostatní"; - /* No comment provided by engineer. */ "password" = "heslo"; @@ -10163,9 +9839,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Displayed in the confirmation alert when marking unread notifications as read. */ "unread" = "nepřečtené"; -/* Label displayed on video media items. */ -"video" = "video"; - /* Portion of a message for Jetpack users that have multisite WP installation, thus Restore is not available. This part is a link, colored with a different color. */ "visit our documentation page" = "navštivte naši stránku s dokumentací"; diff --git a/WordPress/Resources/cy.lproj/Localizable.strings b/WordPress/Resources/cy.lproj/Localizable.strings index 2c36ed7ef616..003ec220025d 100644 --- a/WordPress/Resources/cy.lproj/Localizable.strings +++ b/WordPress/Resources/cy.lproj/Localizable.strings @@ -51,9 +51,6 @@ /* Age between dates over one year. */ "%d years" = "%d blwyddyn"; -/* Max image size in pixels (e.g. 300x300px) */ -"%dx%dpx" = "%1$dx%2$dpx"; - /* One menu area available in the theme */ "%i menu area in this theme" = "%i un ardal dewislen yn y thema hon"; @@ -134,9 +131,6 @@ /* Title for the advanced section in site settings screen */ "Advanced" = "Uwch"; -/* Description of albums in the photo libraries */ -"Albums" = "Albumau"; - /* Image alignment option title. Title of the screen for choosing an image's alignment. */ "Alignment" = "Aliniad"; @@ -226,9 +220,6 @@ /* Menus confirmation text for confirming if a user wants to delete a menu. */ "Are you sure you want to delete the menu?" = "Ydych chi'n siŵr eich bod eisiau dileu'r ddewislen?"; -/* Accessibility label for audio items in the media collection view. The parameter is the creation date of the audio. */ -"Audio, %@" = "Sain, %@"; - /* No comment provided by engineer. */ "Authenticating" = "Dilysu"; @@ -302,8 +293,7 @@ /* Title for a list of different button styles. */ "Button Style" = "Arddull Botwm"; -/* Accessibility label for taking an image or video with the camera on formatting toolbar. - Accessibility label for the camera tile in the collection view */ +/* Accessibility label for taking an image or video with the camera on formatting toolbar. */ "Camera" = "Camera"; /* Title of an alert letting the user know */ @@ -345,10 +335,7 @@ Cancels an Action Cancels an alert. Cancels the mark all as read action. - Dismiss the post action sheet Editing GIF alert cancel action button. - Label for a cancel button - Label for the auto-upload cancelation button in the post list. Tapping will prevent the app from auto-uploading the post. Menus cancel button for deleting a menu. Menus cancel button within text bar while editing items. Menus: Cancel button title for canceling an edited menu item. @@ -579,9 +566,6 @@ /* No comment provided by engineer. */ "Couldn't Connect" = "Methu Cysylltu"; -/* Message to show while media data source is finding the number of items available. */ -"Counting media items..." = "Cyfrif eitemau cyfrwng..."; - /* Period Stats 'Countries' header */ "Countries" = "Gwledydd"; @@ -642,7 +626,6 @@ /* Delete Delete button title for the warning shown to the user when he refuses to re-login when the authToken is missing. Title for button that permanently deletes a media item (photo / video) - Title for button that permanently deletes one or more media items (photos / videos) Title of a delete button Title of the trash confirmation alert. */ "Delete" = "Dileu"; @@ -650,10 +633,7 @@ /* Menus confirmation button for deleting a menu. */ "Delete Menu" = "Dileu Dewislen"; -/* Delete option in the confirmation alert when deleting a page from the trash. - Delete option in the confirmation alert when deleting a post from the trash. - Label for a button permanently deletes a page. - Label for the delete post option. Tapping permanently deletes a post. +/* Delete option in the confirmation alert when deleting a post from the trash. Title for button on the comment details page that deletes the comment when tapped. */ "Delete Permanently" = "Dileu'n Barhol"; @@ -713,7 +693,6 @@ /* Accessibility label for button to dismiss a bottom sheet Accessibility label for the transparent space above the login dialog which acts as a button to dismiss the dialog. Accessibility label for the transparent space above the signup dialog which acts as a button to dismiss the dialog. - Action to show on alert when view asset fails. Dismiss a view. Verb */ "Dismiss" = "Cau"; @@ -722,9 +701,6 @@ User's Display Name */ "Display Name" = "Enw Dangos"; -/* Accessibility label for other media items in the media collection view. The parameter is the filename file. */ -"Document: %@" = "Dogfen: %@"; - /* Noun. Title. Links to the Domains screen. */ "Domains" = "Parthau"; @@ -749,13 +725,9 @@ /* Name for the status of a draft post. */ "Draft" = "Drafft"; -/* Accessibility label for edit button to enable multi selection mode in the user's media library - Edit the post. - Editing GIF alert default action button. +/* Editing GIF alert default action button. Edits a Comment Edits the comment - Label for a button that opens the Edit Page view controller - Label for the edit post button. Tapping displays the editor. User action to edit media details. Verb, edit a comment */ "Edit" = "Golygu"; @@ -834,9 +806,6 @@ /* Text displayed in HUD after attempting to save a draft post and an error occurred. */ "Error occurred\nduring saving" = "Digwyddodd gwall\/n\nwrth gadw"; -/* Label for the post author filter. This filter shows posts for all users on the blog. */ -"Everyone" = "Pawb"; - /* Placeholder text for the tagline of a site */ "Explain what this site is about." = "Esboniwch bwrpas y wefan."; @@ -996,9 +965,6 @@ /* Hint for image title on image settings. */ "Image title" = "Teitl delwedd"; -/* Accessibility label for image thumbnails in the media collection view. The parameter is the creation date of the image. */ -"Image, %@" = "Delwedd, %@"; - /* Undated post time label */ "Immediately" = "Ar unwaith"; @@ -1227,7 +1193,6 @@ "Max Image Upload Size" = "Maint mwyaf Llwytho Delwedd"; /* Accessibility label for the Me button in My Site. - Label for the post author filter. This filter shows posts only authored by the current user. Me page title Noun. Title. Links to the Me screen. The accessibility value of the me tab. @@ -1235,20 +1200,11 @@ "Me" = "Fi"; /* Noun. Title. Links to the blog's Media library. - Tab bar title for the Media tab in Media Picker The menu item to select during a guided tour. - Title for Media Library section of the app. Title for the media section in site settings screen Title label for the media settings section in the app settings */ "Media" = "Cyfrwng"; -/* Title for alert when access to media capture is not granted */ -"Media Capture" = "Cipio Cyfrwng"; - -/* Title for alert when a generic error happened when loading media - Title for alert when access to the media library is not granted by the user */ -"Media Library" = "Llyfrgell Cyfrwng"; - /* Message to indicate progress of uploading media to server */ "Media Uploading" = "Llwytho Cyfryngau"; @@ -1278,24 +1234,15 @@ "Months and Years" = "Misoedd a Blynyddoedd"; /* Accessibility label for more button in dashboard quick start card. - Accessibility label for the More button in Page List. Accessibility label for the More button in Post List (compact view). Accessibility label for the More button on formatting toolbar. Accessibility label for the More button on Reader Cell Accessibility label for the More button on Reader's post details - Action button to display more available options - Label for the more post button. Tapping displays an action sheet with post options. */ + Action button to display more available options */ "More" = "Rhagor"; -/* Label for a button that moves a page to the draft folder - Label for an option that moves a post to the draft folder */ -"Move to Draft" = "Symud i'r Drafft"; - -/* Label for a button that moves a page to the trash folder - Label for a option that moves a post to the trash folder - Title for button on the comment details page that moves the comment to trash when tapped. +/* Title for button on the comment details page that moves the comment to trash when tapped. Trash option in the trash confirmation alert. - Trash option in the trash page confirmation alert. Trashes the comment */ "Move to Trash" = "Symud i'r Sbwriel"; @@ -1303,7 +1250,8 @@ My Profile view title */ "My Profile" = "Fy Mhroffil"; -/* Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ +/* Title for site picker screen. + Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ "My Sites" = "Gwefannau"; /* 'Need help?' button label, links off to the WP for iOS FAQ. */ @@ -1433,7 +1381,6 @@ Alert dismissal title Button title. Acknowledges a prompt. Button title. An acknowledgement of the message displayed in a prompt. - Confirmation of action Default action Dismisses the alert Marks all notifications as read. @@ -1535,18 +1482,6 @@ Title shown when selecting a post type of Page from the Share Extension. */ "Page" = "Tudalen"; -/* Prompts the user that a restored page was moved to the drafts list. */ -"Page Restored to Drafts" = "Wedi Adfer Tudalen i'r Drafftiau"; - -/* Prompts the user that a restored page was moved to the published list. */ -"Page Restored to Published" = "Wedi Adfer Tudalen i'r Cyhoeddwyd"; - -/* Prompts the user that a restored page was moved to the scheduled list. */ -"Page Restored to Scheduled" = "Wedi Adfer Tudalen i'r Amserlenwyd"; - -/* A short message explaining that a page was moved to the trash bin. */ -"Page moved to trash." = "Symudwyd y dudalen i'r sbwriel."; - /* Noun. Title. Links to the blog's Pages screen. The item to select during a guided tour. This is the section title @@ -1579,8 +1514,7 @@ Title of pending Comments filter. */ "Pending" = "Dan Ystyriaeth"; -/* Name for the status of a post pending review. - Title of the Pending Review Badge */ +/* Name for the status of a post pending review. */ "Pending review" = "Disgwyl adolygiad"; /* Noun. Title of the people management feature. @@ -1644,18 +1578,6 @@ The post formats available for the post. Should be the same as in core WP. */ "Post Format" = "Fformat Cofnod"; -/* Prompts the user that a restored post was moved to the drafts list. */ -"Post Restored to Drafts" = "Wedi Adfer Cofnod i'r Drafftiau"; - -/* Prompts the user that a restored post was moved to the published list. */ -"Post Restored to Published" = "Cofnod wedi ei Adfer i Cyhoeddwyd"; - -/* Prompts the user that a restored post was moved to the scheduled list. */ -"Post Restored to Scheduled" = "Cofnod wedi ei Adfer i Amserlennwyd"; - -/* A short message explaining that a post was moved to the trash bin. */ -"Post moved to trash." = "Mae'r cofnod wedi ei symud i'r sbwriel."; - /* Insights 'Posting Activity' header Title for stats Posting Activity view. */ "Posting Activity" = "Gweithgaredd Cofnodi"; @@ -1700,8 +1622,7 @@ "Privacy Policy" = "Polisi Preifatrwydd"; /* Name for the status of a post that is marked private. - Privacy setting for posts set to 'Private'. Should be the same as in core WP. - Title of the Private Badge */ + Privacy setting for posts set to 'Private'. Should be the same as in core WP. */ "Private" = "Preifat"; /* Message for the warning shown to the user when he refuses to re-login when the authToken is missing. */ @@ -1721,14 +1642,12 @@ Title for the publish settings view */ "Publish" = "Cyhoeddi"; -/* A short phrase indicating a post is due to be immedately published. - Label for a button that moves a page to the published folder, publishing with the current date/time. */ +/* A short phrase indicating a post is due to be immedately published. */ "Publish Immediately" = "Cyhoeddi'n Syth"; /* Name for the status of a published post. Period Stats 'Published' header - Title of the published filter. This filter shows a list of posts that the user has published. - Title of the published header in search list. */ + Title of the published filter. This filter shows a list of posts that the user has published. */ "Published" = "Cyhoeddwyd"; /* Published on [date] */ @@ -1796,8 +1715,7 @@ /* Label for selecting the related posts options */ "Related Posts" = "Cofnodion sy'n Perthyn"; -/* Add asset to media picker list - Alert button to confirm a plugin to be removed +/* Alert button to confirm a plugin to be removed Button label when removing a blog Label for the Remove Feature Image icon. Tapping will show a confirmation screen for removing the feature image from the post. Remove Action @@ -1850,9 +1768,6 @@ /* Setting: WordPress.com Surveys */ "Research" = "Ymchwil"; -/* Title of the reset button */ -"Reset" = "Ailosod"; - /* Screen title. Resize and crop an image. */ "Resize & Crop" = "Newid Maint a Thocio"; @@ -1870,12 +1785,9 @@ Button title, displayed when media has failed to upload. Allows the user to try the upload again. Button title. Retries uploading a post. If a user taps the button with this label, the action that evinced this error view will be retried. - Label for a button that attempts to re-upload a page that previously failed to upload. - Label for the retry post upload button. Tapping attempts to upload the post again. Opens the media library . Retries the upload of a user's post. Retry updating User's Role - Retry uploading the post. Retry. Action Retry. Verb – retry a failed media upload. The Jetpack view button title used when an error occurred @@ -1884,9 +1796,6 @@ User action to retry media upload. */ "Retry" = "Ceisiwch eto"; -/* User action to retry media upload. */ -"Retry Upload" = "Ailgynnig llwytho"; - /* No comment provided by engineer. */ "Retry?" = "Ceisio eto?"; @@ -1920,11 +1829,7 @@ /* Button shown if there are unsaved changes and the author is trying to move away from the post. */ "Save Draft" = "Cadw'r Drafft"; -/* Text displayed in HUD when a media item's metadata (title, etc) is saved successfully. */ -"Saved!" = "Cadwyd!"; - /* Menus save button title while it is saving a Menu. - Text displayed in HUD while a media item's metadata (title, etc) is being saved. Text displayed in HUD while a post is being saved as a draft. */ "Saving..." = "Wrthi'n Cadw..."; @@ -1955,9 +1860,6 @@ /* Blog Picker's Title */ "Select Site" = "Dewiswch wefan"; -/* Accessibility hint for actions when displaying media items. */ -"Select media." = "Dewis cyfrwng"; - /* Instructional text about the Sharing feature. */ "Select the account you would like to authorize. Note that your posts will be automatically shared to the selected account." = "Dewiswch y cyfrif yr hoffech ei awdurdodi. Sylwch y bydd eich cofnodion yn cael eu rhannu yn awtomatig i'r cyfrif penodol."; @@ -2001,7 +1903,6 @@ /* Accessibility label for share buttons in nav bars Button label to share a post Button label to share a web page - Share the post. Shares the comment URL Spoken accessibility label Title for a button that allows the user to share their answer to the prompt. @@ -2152,8 +2053,7 @@ Title of Start Over settings page */ "Start Over" = "Cychwyn Eto"; -/* Label for post stats option. Tapping displays statistics for a post. - Noun. Abbreviation of Statistics. Name of the Stats feature +/* Noun. Abbreviation of Statistics. Name of the Stats feature Noun. Abbv. of Statistics. Links to a blog's Stats screen. Stats 3D Touch Shortcut Stats window title @@ -2180,7 +2080,7 @@ Theme Support action title */ "Support" = "Cefnogaeth"; -/* Button used to switch site */ +/* Title for back button that leads to the site picker screen. */ "Switch Site" = "Newid Gwefan"; /* Menu item label for linking a specific tag. @@ -2290,7 +2190,7 @@ /* People: Invitation Error */ "The user already has the specified role. Please, try assigning a different role." = "Mae gan y defnyddiwr rôl sydd eisoes wedi'i bennu. Ceisiwch neilltuo rôl gwahanol."; -/* Error message informing a user about an invalid password. */ +/* No comment provided by engineer. */ "The username or password stored in the app may be out of date. Please re-enter your password in the settings and try again." = "Gall fod yr enw defnyddiwr neu gyfrinair sy'n cael ei gadw yn y app yn hen. Rhowch eich cyfrinair eto yn y gosodiadau a cheisio eto."; /* Title of alert when theme activation succeeds */ @@ -2335,9 +2235,6 @@ /* Explaining to the user there was an error trying to obtain the current location of the user. */ "There was a problem when trying to access your location. Please try again later." = "Bu anhawster wrth geisio cael mynediad i'ch lleoliad. Ceisiwch eto."; -/* Explaining to the user there was an generic error accesing media. */ -"There was a problem when trying to access your media. Please try again later." = "Bu anhawster wrth geisio cael mynediad i'ch cyfrwng. Ceisiwch eto."; - /* Text displayed when there is a failure loading the plan list */ "There was an error loading plans" = "Bu gwall wrth lwytho'r cynlluniau"; @@ -2347,9 +2244,6 @@ /* An error message display if the users device does not have a camera input available */ "This app needs permission to access the Camera to capture new media, please change the privacy settings if you wish to allow this." = "Mae'r ap angen caniatâd i gael mynediad at y Camera i gipio cyfryngau newydd, newidiwch y gosodiadau preifatrwydd os ydych yn dymuno caniatáu hyn."; -/* Explaining to the user why the app needs access to the device media library. */ -"This app needs permission to access your device media library in order to add photos and\/or video to your posts. Please change the privacy settings if you wish to allow this." = "Mae'r ap angen caniatâd i gael mynediad i lyfrgell cyfryngau eich dyfais er mwyn ychwanegu lluniau a\/neu fideo i'ch cofnodion. Newidiwch y gosodiadau preifatrwydd os ydych yn dymuno caniatáu hyn."; - /* Message displayed in Media Library if the user attempts to edit a media asset (image / video) after it has been deleted. */ "This media item has been deleted." = "Mae'r eitem cyfrwng hwn wedi ei ddileu."; @@ -2409,8 +2303,7 @@ /* Label displaying total number of WordPress.com followers. %@ is the total. */ "Total WordPress.com Followers: %@" = "Cyfanswm Dilynwyr WordPress.com: %@"; -/* Accessibility label for trash button to delete items from the user's media library - Accessibility label for trash buttons in nav bars +/* Accessibility label for trash buttons in nav bars Trashes a comment Trashes the comment */ "Trash" = "Dileu"; @@ -2447,9 +2340,6 @@ /* Menus label for describing which menu the location uses in the header. */ "USES" = "DEFNYDD"; -/* An error message. */ -"Unable to Connect" = "Methu Cysylltu"; - /* Title of a prompt saying the app needs an internet connection before it can load posts */ "Unable to Load Posts" = "Methu Llwytho Cofnodion"; @@ -2480,8 +2370,6 @@ Button title. Reverts the previous notification operation Revert an operation Revert enabling notification after successfully subcribing to the comments for the post. - The title of an 'undo' button. Tapping the button moves a trashed page out of the trash folder. - The title of an 'undo' button. Tapping the button moves a trashed post out of the trash folder. Undo action */ "Undo" = "Dadwneud"; @@ -2496,9 +2384,6 @@ Unknown Tag Name */ "Unknown" = "Anhysbys"; -/* Label to use when creation date from media asset is not know. */ -"Unknown creation date" = "Dyddiad creu anhysbys"; - /* No comment provided by engineer. */ "Unknown error" = "Gwall anhysbys"; @@ -2581,15 +2466,10 @@ /* Push Authentication Alert Title */ "Verify Log In" = "Dilysu Mewngofnodi"; -/* Accessibility label for video thumbnails in the media collection view. The parameter is the creation date of the video. */ -"Video, %@" = "Fideo, %@"; - /* Period Stats 'Videos' header */ "Videos" = "Fideos"; /* Button title. Displays a summary / sharing screen for a specific post. - Label for a button that opens the page when tapped. - Label for the view post button. Tapping displays the post as it appears on the web. Opens the post epilogue screen to allow sharing / viewing of a post. Theme View action title Verb. The screen title shown when viewing a post inside the app. */ diff --git a/WordPress/Resources/da.lproj/Localizable.strings b/WordPress/Resources/da.lproj/Localizable.strings index 89c826412b7f..d6dcddee2441 100644 --- a/WordPress/Resources/da.lproj/Localizable.strings +++ b/WordPress/Resources/da.lproj/Localizable.strings @@ -32,10 +32,6 @@ /* No comment provided by engineer. */ "Activity Logs" = "Aktivitetslogs"; -/* Accessibility label for add button to add items to the user's media library - Remove asset from media picker list */ -"Add" = "Tilføj"; - /* The title on the add category screen */ "Add a Category" = "Tilføj kategori"; @@ -51,9 +47,6 @@ /* Title for the advanced section in site settings screen */ "Advanced" = "Avanceret"; -/* Description of albums in the photo libraries */ -"Albums" = "Albummer"; - /* Image alignment option title. Title of the screen for choosing an image's alignment. */ "Alignment" = "Justering"; @@ -155,10 +148,7 @@ Cancels an Action Cancels an alert. Cancels the mark all as read action. - Dismiss the post action sheet Editing GIF alert cancel action button. - Label for a cancel button - Label for the auto-upload cancelation button in the post list. Tapping will prevent the app from auto-uploading the post. Menus cancel button for deleting a menu. Menus cancel button within text bar while editing items. Menus: Cancel button title for canceling an edited menu item. @@ -266,10 +256,6 @@ /* Part of a prompt suggesting that there is more content for the user to read. */ "Continue reading" = "Læs videre"; -/* Copy the post url and paste anywhere in phone - Label for page copy link. Tapping copy the url of page */ -"Copy Link" = "Kopier link"; - /* Title of an prompt letting the user know there was a problem saving. */ "Could Not Save Changes" = "Ændringer kunne ikke gemmes"; @@ -334,15 +320,11 @@ /* Delete Delete button title for the warning shown to the user when he refuses to re-login when the authToken is missing. Title for button that permanently deletes a media item (photo / video) - Title for button that permanently deletes one or more media items (photos / videos) Title of a delete button Title of the trash confirmation alert. */ "Delete" = "Slet"; -/* Delete option in the confirmation alert when deleting a page from the trash. - Delete option in the confirmation alert when deleting a post from the trash. - Label for a button permanently deletes a page. - Label for the delete post option. Tapping permanently deletes a post. +/* Delete option in the confirmation alert when deleting a post from the trash. Title for button on the comment details page that deletes the comment when tapped. */ "Delete Permanently" = "Slet permanent"; @@ -386,7 +368,6 @@ /* Accessibility label for button to dismiss a bottom sheet Accessibility label for the transparent space above the login dialog which acts as a button to dismiss the dialog. Accessibility label for the transparent space above the signup dialog which acts as a button to dismiss the dialog. - Action to show on alert when view asset fails. Dismiss a view. Verb */ "Dismiss" = "Afvis"; @@ -411,13 +392,9 @@ /* Name for the status of a draft post. */ "Draft" = "Kladde"; -/* Accessibility label for edit button to enable multi selection mode in the user's media library - Edit the post. - Editing GIF alert default action button. +/* Editing GIF alert default action button. Edits a Comment Edits the comment - Label for a button that opens the Edit Page view controller - Label for the edit post button. Tapping displays the editor. User action to edit media details. Verb, edit a comment */ "Edit" = "Rediger"; @@ -481,9 +458,6 @@ /* Title of error dialog when removing a site owner fails. */ "Error removing %@" = "Fejl ved fjernelse af %@"; -/* Label for the post author filter. This filter shows posts for all users on the blog. */ -"Everyone" = "Alle"; - /* Label for the excerpt field. Should be the same as WP core. */ "Excerpt" = "Uddrag"; @@ -726,7 +700,6 @@ "Mark Read" = "Marker som læst"; /* Accessibility label for the Me button in My Site. - Label for the post author filter. This filter shows posts only authored by the current user. Me page title Noun. Title. Links to the Me screen. The accessibility value of the me tab. @@ -734,17 +707,11 @@ "Me" = "Mig"; /* Noun. Title. Links to the blog's Media library. - Tab bar title for the Media tab in Media Picker The menu item to select during a guided tour. - Title for Media Library section of the app. Title for the media section in site settings screen Title label for the media settings section in the app settings */ "Media" = "Medier"; -/* Title for alert when a generic error happened when loading media - Title for alert when access to the media library is not granted by the user */ -"Media Library" = "Mediebibliotek"; - /* Title for action sheet with media options. */ "Media Options" = "Medieindstillinger"; @@ -755,9 +722,6 @@ /* Medium image size. Should be the same as in core WP. */ "Medium" = "Medium"; -/* Title of section containing image / video metadata such as size and file type */ -"Metadata" = "Metadata"; - /* Title of Months stats filter. */ "Months" = "Måneder"; @@ -765,29 +729,24 @@ "Months and Years" = "Måneder og år"; /* Accessibility label for more button in dashboard quick start card. - Accessibility label for the More button in Page List. Accessibility label for the More button in Post List (compact view). Accessibility label for the More button on formatting toolbar. Accessibility label for the More button on Reader Cell Accessibility label for the More button on Reader's post details - Action button to display more available options - Label for the more post button. Tapping displays an action sheet with post options. */ + Action button to display more available options */ "More" = "Mere"; /* Action button to display more available options Label for the More Options area in post settings. Should use the same translation as core WP. */ "More Options" = "Flere indstillinger"; -/* Label for a button that moves a page to the draft folder - Label for an option that moves a post to the draft folder */ -"Move to Draft" = "Flyt til kladder"; - /* Generic name for the detail screen for specific site - used for spotlight indexing on iOS. Note: this is only used if we cannot determine a name chances of this being used are small. The accessibility value of the my site tab. Title of My Site tab */ "My Site" = "Mit websted"; -/* Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ +/* Title for site picker screen. + Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ "My Sites" = "Mine websteder"; /* 'Need help?' button label, links off to the WP for iOS FAQ. */ @@ -865,7 +824,6 @@ Alert dismissal title Button title. Acknowledges a prompt. Button title. An acknowledgement of the message displayed in a prompt. - Confirmation of action Default action Dismisses the alert Marks all notifications as read. @@ -932,9 +890,6 @@ Other Sites Notification Settings Title */ "Other Sites" = "Andre websteder"; -/* A short message explaining that a page was moved to the trash bin. */ -"Page moved to trash." = "Side flyttet til papirkurv."; - /* Title of notification displayed when a page has been successfully updated. */ "Page updated" = "Side opdateret"; @@ -965,16 +920,12 @@ Title of pending Comments filter. */ "Pending" = "Afventer godkendelse"; -/* Name for the status of a post pending review. - Title of the Pending Review Badge */ +/* Name for the status of a post pending review. */ "Pending review" = "Afventer godkendelse"; /* Label for date periods. */ "Period" = "Periode"; -/* Tab bar title for the Photos tab in Media Picker */ -"Photos" = "Foto"; - /* No comment provided by engineer. */ "Please enter a site address." = "Indtast venligst en webstedsadresse."; @@ -1012,15 +963,6 @@ The post formats available for the post. Should be the same as in core WP. */ "Post Format" = "Indlægsformat"; -/* Prompts the user that a restored post was moved to the drafts list. */ -"Post Restored to Drafts" = "Indlæg genskabt under Kladder"; - -/* Prompts the user that a restored post was moved to the published list. */ -"Post Restored to Published" = "Indlæg genskab under Udgivet"; - -/* A short message explaining that a post was moved to the trash bin. */ -"Post moved to trash." = "Indlæg flyttet til papirkurv."; - /* Title of camera permissions screen */ "Post to WordPress" = "Udgiv på WordPress"; @@ -1054,8 +996,7 @@ "Privacy Policy" = "Privatlivspolitik"; /* Name for the status of a post that is marked private. - Privacy setting for posts set to 'Private'. Should be the same as in core WP. - Title of the Private Badge */ + Privacy setting for posts set to 'Private'. Should be the same as in core WP. */ "Private" = "Privat"; /* Privacy setting for posts set to 'Public' (default). Should be the same as in core WP. */ @@ -1069,14 +1010,12 @@ Title for the publish settings view */ "Publish" = "Udgiv"; -/* A short phrase indicating a post is due to be immedately published. - Label for a button that moves a page to the published folder, publishing with the current date/time. */ +/* A short phrase indicating a post is due to be immedately published. */ "Publish Immediately" = "Udgiv med det samme"; /* Name for the status of a published post. Period Stats 'Published' header - Title of the published filter. This filter shows a list of posts that the user has published. - Title of the published header in search list. */ + Title of the published filter. This filter shows a list of posts that the user has published. */ "Published" = "Udgivet"; /* Published on [date] */ @@ -1108,8 +1047,7 @@ The loading view button title displayed when an error occurred */ "Refresh" = "Genindlæs"; -/* Add asset to media picker list - Alert button to confirm a plugin to be removed +/* Alert button to confirm a plugin to be removed Button label when removing a blog Label for the Remove Feature Image icon. Tapping will show a confirmation screen for removing the feature image from the post. Remove Action @@ -1144,9 +1082,6 @@ /* The app successfully sent a comment */ "Reply Sent!" = "Svar sendt!"; -/* Title of the reset button */ -"Reset" = "Nulstil"; - /* Title displayed for restore action. Title for button allowing user to restore their Jetpack site Title for Jetpack Restore Complete screen @@ -1168,12 +1103,9 @@ Button title, displayed when media has failed to upload. Allows the user to try the upload again. Button title. Retries uploading a post. If a user taps the button with this label, the action that evinced this error view will be retried. - Label for a button that attempts to re-upload a page that previously failed to upload. - Label for the retry post upload button. Tapping attempts to upload the post again. Opens the media library . Retries the upload of a user's post. Retry updating User's Role - Retry uploading the post. Retry. Action Retry. Verb – retry a failed media upload. The Jetpack view button title used when an error occurred @@ -1204,11 +1136,7 @@ /* Button shown if there are unsaved changes and the author is trying to move away from the post. */ "Save Draft" = "Gem kladde"; -/* Text displayed in HUD when a media item's metadata (title, etc) is saved successfully. */ -"Saved!" = "Gemt!"; - /* Menus save button title while it is saving a Menu. - Text displayed in HUD while a media item's metadata (title, etc) is being saved. Text displayed in HUD while a post is being saved as a draft. */ "Saving..." = "Gemmer..."; @@ -1246,7 +1174,6 @@ /* Accessibility label for share buttons in nav bars Button label to share a post Button label to share a web page - Share the post. Shares the comment URL Spoken accessibility label Title for a button that allows the user to share their answer to the prompt. @@ -1343,8 +1270,7 @@ /* Standard post format label */ "Standard" = "Standard"; -/* Label for post stats option. Tapping displays statistics for a post. - Noun. Abbreviation of Statistics. Name of the Stats feature +/* Noun. Abbreviation of Statistics. Name of the Stats feature Noun. Abbv. of Statistics. Links to a blog's Stats screen. Stats 3D Touch Shortcut Stats window title @@ -1364,7 +1290,7 @@ Theme Support action title */ "Support" = "Support"; -/* Button used to switch site */ +/* Title for back button that leads to the site picker screen. */ "Switch Site" = "Skift websted"; /* Label for selecting the blogs tags @@ -1415,7 +1341,7 @@ /* No comment provided by engineer. */ "The site at %@ uses WordPress %@. We recommend to update to the latest version, or at least %@" = "Webstedet på %1$@ benytter WordPress %2$@. Vi anbefaler, at du opdaterer til seneste version, eller mindst %3$@"; -/* Error message informing a user about an invalid password. */ +/* No comment provided by engineer. */ "The username or password stored in the app may be out of date. Please re-enter your password in the settings and try again." = "Brugernavnet eller kodeordet, der er gemt i app'en, er ikke korrekt. Prøv venligst at genindtaste dit kodeord under indstillinger og prøv igen."; /* Noun. Name of the Themes feature @@ -1463,8 +1389,7 @@ /* Label displaying total number of WordPress.com followers. %@ is the total. */ "Total WordPress.com Followers: %@" = "Antal følgere på WordPress.com: %@"; -/* Accessibility label for trash button to delete items from the user's media library - Accessibility label for trash buttons in nav bars +/* Accessibility label for trash buttons in nav bars Trashes a comment Trashes the comment */ "Trash" = "Slet"; @@ -1487,9 +1412,6 @@ /* URL text field placeholder */ "URL" = "URL"; -/* An error message. */ -"Unable to Connect" = "Kunne ikke forbinde"; - /* Title of a prompt saying the app needs an internet connection before it can load posts */ "Unable to Load Posts" = "Kunne ikke hente indlæg"; @@ -1565,8 +1487,6 @@ "Videos" = "Videoer"; /* Button title. Displays a summary / sharing screen for a specific post. - Label for a button that opens the page when tapped. - Label for the view post button. Tapping displays the post as it appears on the web. Opens the post epilogue screen to allow sharing / viewing of a post. Theme View action title Verb. The screen title shown when viewing a post inside the app. */ @@ -1681,9 +1601,6 @@ /* (placeholder) Help the user enter a URL into the field */ "http:\/\/my-site-address (URL)" = "http:\/\/adresse-til-websted (URL)"; -/* Label displayed on image media items. */ -"image" = "billede"; - /* This text is used when the user is configuring the iOS widget to suggest them to select the site to configure the widget for */ "ios-widget.gpCwrM" = "Select Site"; diff --git a/WordPress/Resources/de.lproj/Localizable.strings b/WordPress/Resources/de.lproj/Localizable.strings index a964a10d2d21..a6e78cdad650 100644 --- a/WordPress/Resources/de.lproj/Localizable.strings +++ b/WordPress/Resources/de.lproj/Localizable.strings @@ -1,4 +1,4 @@ -/* Translation-Revision-Date: 2023-10-18 14:54:09+0000 */ +/* Translation-Revision-Date: 2024-01-04 00:31:04+0000 */ /* Plural-Forms: nplurals=2; plural=n != 1; */ /* Generator: GlotPress/4.0.0-alpha.11 */ /* Language: de */ @@ -50,11 +50,11 @@ /* Singular button title to Like a comment. %1$d is a placeholder for the number of Likes. Singular format string for view title displaying the number of post likes. %1$d is the number of likes. */ -"%1$d Like" = "%1$d Gefällt mir"; +"%1$d Like" = "%1$d Like"; /* Plural button title to Like a comment. %1$d is a placeholder for the number of Likes. Plural format string for view title displaying the number of post likes. %1$d is the number of likes. */ -"%1$d Likes" = "%1$d Gefällt mir"; +"%1$d Likes" = "%1$d Likes"; /* Singular format string for displaying the number of users that answered the blogging prompt. */ "%1$d answer" = "%1$d Antwort"; @@ -152,9 +152,6 @@ /* Difference label for Period Overview stat, indicating change from previous period. Ex: +99.9K (5%) */ "%@%@ (%@%%)" = "%1$@%2$@ (%3$@%%)"; -/* Accessibility label for a post in the post list. The parameters are the title, and date respectively. For example, \"Let it Go, 1 hour ago.\" */ -"%@, %@." = "%1$@, %2$@."; - /* Accessibility value for a Stats' Posting Activity Month if the user selected a day with posts. The first parameter is day (e.g. November 2019). The second parameter is the number of posts. */ "%@. %d posts." = "%1$@. %2$d Beiträge."; @@ -210,9 +207,6 @@ /* Age between dates over one year. */ "%d years" = "%d Jahre"; -/* Max image size in pixels (e.g. 300x300px) */ -"%dx%dpx" = "%1$d × %2$d Pixel"; - /* One menu area available in the theme */ "%i menu area in this theme" = "%i Menübereich in diesem Theme"; @@ -271,6 +265,9 @@ translators: Block name. %s: The localized block name */ /* translators: %s: social link name e.g: \"Instagram\". */ "%s social icon" = "Social-Media-Symbol für %s"; +/* translators: displayed right after the classic block is converted to blocks. %s: The localized classic block name */ +"'%s' block converted to blocks" = "Block „%s“ in Blöcke umgewandelt"; + /* translators: Missing block alert title. %s: The localized block name */ "'%s' is not fully-supported" = "„%s“ wird nicht vollständig unterstützt"; @@ -478,13 +475,6 @@ translators: Block name. %s: The localized block name */ /* Label for the Activity Type filter button when there are more than 1 activity type selected */ "Activity Type (%1$d)" = "Aktivitätstyp (%1$d)"; -/* Accessibility label for add button to add items to the user's media library - Remove asset from media picker list */ -"Add" = "Hinzufügen"; - -/* Action for Media Picker to indicate selection of media. The argument in the string represents the number of elements (as numeric digits) selected */ -"Add %@" = "%@ hinzufügen"; - /* No comment provided by engineer. */ "Add Block After" = "Block dahinter hinzufügen"; @@ -581,9 +571,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Add menu item to children" = "Bei untergeordneten Einträgen Menüeintrag hinzufügen"; -/* Accessibility hint for add button to add items to the user's media library */ -"Add new media" = "Neue Medien hinzufügen"; - /* Screen reader text for Menus button that adds a new menu to a site. */ "Add new menu" = "Neues Menü hinzufügen"; @@ -649,9 +636,6 @@ translators: Block name. %s: The localized block name */ /* Option to select the Airmail app when logging in with magic links */ "Airmail" = "Airmail"; -/* Description of albums in the photo libraries */ -"Albums" = "Alben"; - /* Image alignment option title. Title of the screen for choosing an image's alignment. */ "Alignment" = "Ausrichtung"; @@ -665,6 +649,9 @@ translators: Block name. %s: The localized block name */ Title of the drafts filter. This filter shows a list of draft posts. */ "All" = "Alle"; +/* Information about redeeming domain credit on site dashboard. */ +"All WordPress.com annual plans include a custom domain name. Register your free domain now." = "Alle WordPress.com-Jahrestarife enthalten einen individuellen Domainnamen. Registriere jetzt deine kostenlose Domain."; + /* Footer of the free domain registration section for a paid plan. Information about redeeming domain credit on site dashboard. */ "All WordPress.com plans include a custom domain name. Register your free premium domain now." = "Alle WordPress.com-Tarife beinhalten einen individuellen Domain-Namen. Registriere jetzt deine kostenlose Premium-Domain."; @@ -730,10 +717,13 @@ translators: Block name. %s: The localized block name */ "Alt Text" = "Alt-Text"; /* No comment provided by engineer. */ -"Alternatively, you can detach and edit these blocks separately by tapping “Detach patterns”." = "Alternativ kannst du diese Blöcke loslösen und separat bearbeiten, indem du auf „Vorlagen loslösen“ tippst."; +"Alternatively, you can convert the content to blocks." = "Alternativ kannst du den Inhalt in Blöcke umwandeln."; /* No comment provided by engineer. */ -"Alternatively, you can detach and edit this block separately by tapping “Detach pattern”." = "Alternativ kannst du diesen Block loslösen und separat bearbeiten, indem du auf „Vorlage loslösen“ tippst."; +"Alternatively, you can detach and edit this block separately by tapping “Detach”." = "Alternativ kannst du diesen Block trennen und separat bearbeiten, indem du auf „Loslösen“ tippst."; + +/* translators: Alternative option included in a warning related to having blocks deeply nested. */ +"Alternatively, you can flatten the content by ungrouping the block." = "Alternativ kannst du die Tiefe des Inhalts reduzieren, indem du die Gruppierung des Blocks aufhebst."; /* Instruction text to explain to help users type their password instead of using magic link login option. */ "Alternatively, you may enter the password for this account." = "Alternativ kannst du das Passwort für dieses Konto eingeben."; @@ -882,15 +872,9 @@ translators: Block name. %s: The localized block name */ /* Message prompting the user to confirm that they want to disconnect Jetpack from the site. */ "Are you sure you want to disconnect Jetpack from the site?" = "Bist du sicher, dass du Jetpack von der Website trennen möchtest?"; -/* Message prompting the user to confirm that they want to permanently delete a group of media items. */ -"Are you sure you want to permanently delete these items?" = "Bist Du sicher, dass du die ausgewählten Objekte permanent löschen möchtest?"; - /* Message prompting the user to confirm that they want to permanently delete a media item. Should match Calypso. */ "Are you sure you want to permanently delete this item?" = "Bist du sicher, dass du dieses Element endgültig löschen möchtest?"; -/* Message of the confirmation alert when deleting a page from the trash. */ -"Are you sure you want to permanently delete this page?" = "Bist du sicher, dass du diese Seite endgültig löschen möchtest?"; - /* Message of the confirmation alert when deleting a post from the trash. */ "Are you sure you want to permanently delete this post?" = "Bist du sicher, dass du diesen Beitrag endgültig löschen möchtest?"; @@ -922,9 +906,6 @@ translators: Block name. %s: The localized block name */ /* Title of message shown when user taps submit for review. */ "Are you sure you want to submit for review?" = "Bist du sicher, dass du den Inhalt zur Überprüfung einreichen möchtest?"; -/* Message of the trash page confirmation alert. */ -"Are you sure you want to trash this page?" = "Bist du sicher, dass du diese Seite in den Papierkorb verschieben möchtest?"; - /* Message of the trash confirmation alert. */ "Are you sure you want to trash this post?" = "Willst du diesen Artikel wirklich löschen?"; @@ -965,9 +946,6 @@ translators: Block name. %s: The localized block name */ /* translators: accessibility text. Empty Audio caption. */ "Audio caption. Empty" = "Audiountertitel. Leer"; -/* Accessibility label for audio items in the media collection view. The parameter is the creation date of the audio. */ -"Audio, %@" = "Audio, %@"; - /* No comment provided by engineer. */ "Authenticating" = "Authentifiziere"; @@ -1178,10 +1156,7 @@ translators: Block name. %s: The localized block name */ "Blocks menu" = "Block-Menü"; /* translators: Warning related to having blocks deeply nested. %d: The deepest nesting level. */ -"Blocks nested deeper than %d levels may not render properly in the mobile editor. For this reason, we recommend flattening the content by ungrouping the block or editing the block using the web editor." = "Blöcke, die tiefer als %d Ebenen verschachtelt sind, werden im mobilen Editor möglicherweise nicht richtig dargestellt. Aus diesem Grund empfehlen wir, die Tiefe des Inhalts zu reduzieren, indem du die Gruppierung des Blocks aufhebst oder den Block mit dem Webeditor bearbeitest."; - -/* translators: Warning related to having blocks deeply nested. %d: The deepest nesting level. */ -"Blocks nested deeper than %d levels may not render properly in the mobile editor. For this reason, we recommend flattening the content by ungrouping the block or editing the block using your web browser." = "Blöcke, die tiefer als %d Ebenen verschachtelt sind, werden im mobilen Editor möglicherweise nicht richtig dargestellt. Aus diesem Grund empfehlen wir, die Tiefe des Inhalts zu reduzieren, indem du die Gruppierung des Blocks aufhebst oder den Block mit deinem Webbrowser bearbeitest."; +"Blocks nested deeper than %d levels may not render properly in the mobile editor." = "Blöcke, die tiefer als %d Ebenen verschachtelt sind, werden im mobilen Editor möglicherweise nicht richtig dargestellt."; /* Title of a button that displays the WordPress.com blog */ "Blog" = "Blog"; @@ -1257,10 +1232,7 @@ translators: Block name. %s: The localized block name */ "Button position" = "Button-Position"; /* Label for the post author in the post detail. */ -"By " = "Von"; - -/* Accessibility label for the post author in the post list. The parameter is the author name. For example, \"By Elsa.\" */ -"By %@." = "Von %@."; +"By " = "Von "; /* Legal disclaimer for logging in. The underscores _..._ denote underline. */ "By continuing, you agree to our _Terms of Service_." = "Wenn du fortfährst, stimmst du unseren _Geschäftsbedingungen_ zu."; @@ -1281,8 +1253,7 @@ translators: Block name. %s: The localized block name */ /* Label for size of media while it's being calculated. */ "Calculating..." = "Berechne..."; -/* Accessibility label for taking an image or video with the camera on formatting toolbar. - Accessibility label for the camera tile in the collection view */ +/* Accessibility label for taking an image or video with the camera on formatting toolbar. */ "Camera" = "Kamera"; /* Title of an alert informing the user the camera permission for the app is disabled and its needed to proceed */ @@ -1333,10 +1304,7 @@ translators: Block name. %s: The localized block name */ Cancels an Action Cancels an alert. Cancels the mark all as read action. - Dismiss the post action sheet Editing GIF alert cancel action button. - Label for a cancel button - Label for the auto-upload cancelation button in the post list. Tapping will prevent the app from auto-uploading the post. Menus cancel button for deleting a menu. Menus cancel button within text bar while editing items. Menus: Cancel button title for canceling an edited menu item. @@ -1353,10 +1321,6 @@ translators: Block name. %s: The localized block name */ Verb. Title for Jetpack Restore cancel button. */ "Cancel" = "Abbrechen"; -/* Label for the Post List option that cancels automatic uploading of a post. - Media Library option to cancel an in-progress or failed upload. */ -"Cancel Upload" = "Upload abbrechen"; - /* Menus alert button title to cancel discarding changes and not createa a new menu. Menus alert button title to cancel discarding changes and not select a new menu Menus alert button title to cancel discarding changes and not select a new menu location */ @@ -1415,9 +1379,6 @@ translators: Block name. %s: The localized block name */ Main title */ "Change Password" = "Passwort ändern"; -/* Title of button that takes user to the system Settings section for the app */ -"Change Settings" = "Einstellungen ändern"; - /* Change Username title. */ "Change Username" = "Benutzernamen ändern"; @@ -1557,9 +1518,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Choose file" = "Datei auswählen"; -/* Menu option for selecting media from the device's photo library. */ -"Choose from My Device" = "Von meinem Gerät wählen"; - /* Explanatory text for Homepage Settings homepage type selection. */ "Choose from a homepage that displays your latest posts (classic blog) or a fixed \/ static page." = "Wähle eine Homepage, die deine neuesten Beiträge anzeigt (klassischer Blog), oder eine feste\/statische Seite."; @@ -1689,7 +1647,7 @@ translators: Block name. %s: The localized block name */ "Comment" = "Kommentar"; /* Title for the `comment likes` setting */ -"Comment Likes" = "„Gefällt mir“ bei Kommentaren"; +"Comment Likes" = "Kommentar-Likes"; /* Message displayed when approving a comment succeeds. */ "Comment approved." = "Kommentar genehmigt."; @@ -1760,9 +1718,6 @@ translators: Block name. %s: The localized block name */ /* Community & Non-Profit site intent topic */ "Community & Non-Profit" = "Community und gemeinnützige Organisationen"; -/* Accessibility indication that the current Post List style is currently Compact. */ -"Compact" = "Kompakt"; - /* The action is completed */ "Completed" = "Abgeschlossen"; @@ -1948,10 +1903,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Copied block" = "Kopierter Block"; -/* Copy the post url and paste anywhere in phone - Label for page copy link. Tapping copy the url of page */ -"Copy Link" = "Link kopieren"; - /* Copy link to oomment button title */ "Copy Link to Comment" = "Link in Kommentar kopieren"; @@ -2060,9 +2011,6 @@ translators: Block name. %s: The localized block name */ /* Error title displayed when unable to close user account. */ "Couldn’t close account automatically" = "Konto konnte nicht automatisch geschlossen werden"; -/* Message to show while media data source is finding the number of items available. */ -"Counting media items..." = "Medien werden gezählt ..."; - /* Period Stats 'Countries' header */ "Countries" = "Länder"; @@ -2313,7 +2261,6 @@ translators: Block name. %s: The localized block name */ /* Delete Delete button title for the warning shown to the user when he refuses to re-login when the authToken is missing. Title for button that permanently deletes a media item (photo / video) - Title for button that permanently deletes one or more media items (photos / videos) Title of a delete button Title of the trash confirmation alert. */ "Delete" = "Löschen"; @@ -2321,15 +2268,11 @@ translators: Block name. %s: The localized block name */ /* Menus confirmation button for deleting a menu. */ "Delete Menu" = "Menü löschen"; -/* Delete option in the confirmation alert when deleting a page from the trash. - Delete option in the confirmation alert when deleting a post from the trash. - Label for a button permanently deletes a page. - Label for the delete post option. Tapping permanently deletes a post. +/* Delete option in the confirmation alert when deleting a post from the trash. Title for button on the comment details page that deletes the comment when tapped. */ "Delete Permanently" = "Endgültig löschen"; -/* Title of the confirmation alert when deleting a page from the trash. - Title of the confirmation alert when deleting a post from the trash. */ +/* Title of the confirmation alert when deleting a post from the trash. */ "Delete Permanently?" = "Unwiderruflich löschen?"; /* Button label for deleting the current site @@ -2448,7 +2391,6 @@ translators: Block name. %s: The localized block name */ /* Accessibility label for button to dismiss a bottom sheet Accessibility label for the transparent space above the login dialog which acts as a button to dismiss the dialog. Accessibility label for the transparent space above the signup dialog which acts as a button to dismiss the dialog. - Action to show on alert when view asset fails. Dismiss a view. Verb */ "Dismiss" = "Verwerfen"; @@ -2466,12 +2408,6 @@ translators: Block name. %s: The localized block name */ User's Display Name */ "Display Name" = "Anzeigename"; -/* Accessibility label for other media items in the media collection view. The parameter is the creation date of the document. */ -"Document, %@" = "Dokument: %@"; - -/* Accessibility label for other media items in the media collection view. The parameter is the filename file. */ -"Document: %@" = "Dokument: %@"; - /* Message shown when all tours have been completed */ "Doesn't it feel good to cross things off a list?" = "Fühlt es sich nicht gut an, Dinge von einer Liste zu streichen?"; @@ -2632,8 +2568,7 @@ translators: Block name. %s: The localized block name */ /* Description of a Quick Start Tour */ "Draft and publish a post." = "Entwirf und veröffentliche einen Beitrag."; -/* Title of the drafts filter. This filter shows a list of draft posts. - Title of the drafts header in search list. */ +/* Title of the drafts filter. This filter shows a list of draft posts. */ "Drafts" = "Entwürfe"; /* No comment provided by engineer. */ @@ -2645,10 +2580,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Drag to adjust focal point" = "Ziehen, um Fokuspunkt anzupassen"; -/* Label for page duplicate option. Tapping creates a copy of the page. - Label for post duplicate option. Tapping creates a copy of the post. */ -"Duplicate" = "Duplizieren"; - /* No comment provided by engineer. */ "Duplicate block" = "Block duplizieren"; @@ -2661,13 +2592,9 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Each block has its own settings. To find them, tap on a block. Its settings will appear on the toolbar at the bottom of the screen." = "Jeder Block hat eigene Einstellungen. Tippe auf den jeweiligen Block, um sie zu finden. Die Einstellungen werden in der Werkzeugleiste unten auf dem Bildschirm angezeigt."; -/* Accessibility label for edit button to enable multi selection mode in the user's media library - Edit the post. - Editing GIF alert default action button. +/* Editing GIF alert default action button. Edits a Comment Edits the comment - Label for a button that opens the Edit Page view controller - Label for the edit post button. Tapping displays the editor. User action to edit media details. Verb, edit a comment */ "Edit" = "Bearbeiten"; @@ -2675,9 +2602,6 @@ translators: Block name. %s: The localized block name */ /* Title for the edit more button section */ "Edit \"More\" button" = "\"Mehr\"-Button bearbeiten"; -/* Button that displays the media editor to the user */ -"Edit %@" = "%@ bearbeiten"; - /* Blocklist Keyword Edition Title */ "Edit Blocklist Word" = "Wort für Sperrliste bearbeiten"; @@ -2870,9 +2794,6 @@ translators: Block name. %s: The localized block name */ /* Secondary message shown when there are no domains that match the user entered text. */ "Enter different words above and we'll look for an address that matches it." = "Gib oben andere Wörter ein und wir suchen nach einer passenden Adresse."; -/* Accessibility hint for edit button to enable multi selection mode in the user's media library */ -"Enter edit mode to enable multi select to delete" = "Öffne den Bearbeitungsmodus, um die Mehrfachauswahl zum Löschen zu aktivieren"; - /* (placeholder) Help enter WordPress password Placeholder of a field to type a password to protect the post. */ "Enter password" = "Passwort eingeben"; @@ -3028,9 +2949,6 @@ translators: Block name. %s: The localized block name */ /* Short title telling the user they will receive a blogging reminder every day of the week. */ "Every day at %@" = "Jeden Tag um %@"; -/* Label for the post author filter. This filter shows posts for all users on the blog. */ -"Everyone" = "Jeder"; - /* Example story title description */ "Example story title" = "Beispieltitel für Story"; @@ -3040,9 +2958,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Excerpt length (words)" = "Länge des Textauszugs (in Wörtern)"; -/* Accessibility label for a post's excerpt in the post list. The parameter is the post excerpt. For example, \"Excerpt. This is the first paragraph.\" */ -"Excerpt. %@." = "Textauszug. %@."; - /* Should be the same as the text displayed if the user clicks the (i) in Calypso. */ "Excerpts are optional hand-crafted summaries of your content." = "Auszüge sind optionale, manuell angefertigte Zusammenfassungen deines Inhalts."; @@ -3052,8 +2967,7 @@ translators: Block name. %s: The localized block name */ /* Accessibility Label for the exit full screen button on the full screen comment reply mode */ "Exit Full Screen" = "Vollbild verlassen"; -/* Accessibility indication that the current Post List style is currently Expanded. - Screen reader text to represent the expanded state of a UI control */ +/* Screen reader text to represent the expanded state of a UI control */ "Expanded" = "Erweitert"; /* Accessibility hint */ @@ -3103,9 +3017,6 @@ translators: Block name. %s: The localized block name */ The action failed */ "Failed" = "Fehlgeschlagen"; -/* Error title when picked media cannot be imported into stories. */ -"Failed Media Export" = "Medienexport fehlgeschlagen"; - /* Message for mark all as read success notice */ "Failed marking Notifications as read" = "Benachrichtigungen konnten nicht als gelesen markiert werden"; @@ -3307,6 +3218,12 @@ translators: Block name. %s: The localized block name */ /* An example tag used in the login prologue screens. */ "Football" = "American Football"; +/* translators: Recommendation included in a warning related to having blocks deeply nested. */ +"For this reason, we recommend editing the block using the web editor." = "Aus diesem Grund empfehlen wir, den Block im Webeditor zu bearbeiten."; + +/* translators: Recommendation included in a warning related to having blocks deeply nested. */ +"For this reason, we recommend editing the block using your web browser." = "Aus diesem Grund empfehlen wir, den Block in deinem Webbrowser zu bearbeiten."; + /* Register Domain - Domain contact information section header description */ "For your convenience, we have pre-filled your WordPress.com contact information. Please review to be sure it’s the correct information you want to use for this domain." = "Wir haben bereits deine WordPress.com-Kontaktinformationen für dich ausgefüllt. Überprüfe bitte, ob wir die Informationen, die du für diese Domain verwenden möchtest, korrekt eingegeben haben."; @@ -3624,8 +3541,7 @@ translators: Block name. %s: The localized block name */ "Home" = "Startseite"; /* Title for setting which shows the current page assigned as a site's homepage - Title for the homepage section in site settings screen - Title of the Homepage Badge */ + Title for the homepage section in site settings screen */ "Homepage" = "Homepage"; /* Label for Homepage Settings site settings section @@ -3722,9 +3638,6 @@ translators: Block name. %s: The localized block name */ /* Hint for image title on image settings. */ "Image title" = "Bildtitel"; -/* Accessibility label for image thumbnails in the media collection view. The parameter is the creation date of the image. */ -"Image, %@" = "Bild, %@"; - /* Undated post time label */ "Immediately" = "Sofort"; @@ -4148,7 +4061,7 @@ translators: Block name. %s: The localized block name */ Text for the 'like' button. Tapping removes the 'liked' status from a post. Title of the Likes Reader tab Today's Stats 'Likes' label */ -"Likes" = "Gefällt mir"; +"Likes" = "Likes"; /* Setting: indicates if Comment Likes will be notified */ "Likes on my comments" = "Likes zu meinen Kommentaren"; @@ -4210,9 +4123,6 @@ translators: Block name. %s: The localized block name */ Settings: Comments Approval settings */ "Links in comments" = "Links in Kommentaren"; -/* The accessibility label for the list style button in the Post List. */ -"List style" = "Listen-Stil"; - /* Title of the screen that load selected the revisions. */ "Load" = "Laden"; @@ -4228,18 +4138,12 @@ translators: Block name. %s: The localized block name */ /* Text displayed while loading the activity feed for a site */ "Loading Backups..." = "Backups werden geladen …"; -/* Phrase to show when the user has searched for GIFs and they are being loaded. */ -"Loading GIFs..." = "GIFs laden ..."; - /* Menus label text displayed while menus are loading */ "Loading Menus..." = "Menüs werden geladen ..."; /* Text displayed while loading site People. */ "Loading People..." = "Personen werden geladen ..."; -/* Phrase to show when the user has searched for images and they are being loaded. */ -"Loading Photos..." = "Fotos laden ..."; - /* Text displayed while loading plans details */ "Loading Plan..." = "Lade Tarif ..."; @@ -4300,8 +4204,7 @@ translators: Block name. %s: The localized block name */ /* Local Services site intent topic */ "Local Services" = "Lokale Dienstleistungen"; -/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. - Title of the Local Changes Badge */ +/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. */ "Local changes" = "Lokale Änderungen"; /* Title for alert when a generic error happened when trying to find the location of the device */ @@ -4465,7 +4368,6 @@ translators: Block name. %s: The localized block name */ "Max Video Upload Size" = "Max. Videogröße für Upload"; /* Accessibility label for the Me button in My Site. - Label for the post author filter. This filter shows posts only authored by the current user. Me page title Noun. Title. Links to the Me screen. The accessibility value of the me tab. @@ -4473,9 +4375,7 @@ translators: Block name. %s: The localized block name */ "Me" = "Ich"; /* Noun. Title. Links to the blog's Media library. - Tab bar title for the Media tab in Media Picker The menu item to select during a guided tour. - Title for Media Library section of the app. Title for the media section in site settings screen Title label for the media settings section in the app settings */ "Media" = "Medien"; @@ -4487,13 +4387,6 @@ translators: Block name. %s: The localized block name */ /* Label for size of media cache in the app. */ "Media Cache Size" = "Größe des Medien-Cache"; -/* Title for alert when access to media capture is not granted */ -"Media Capture" = "Aufnehmen von Medien"; - -/* Title for alert when a generic error happened when loading media - Title for alert when access to the media library is not granted by the user */ -"Media Library" = "Mediathek"; - /* Title for action sheet with media options. */ "Media Options" = "Medien-Optionen"; @@ -4516,9 +4409,6 @@ translators: Block name. %s: The localized block name */ /* translators: %s: block title e.g: \"Paragraph\". */ "Media options" = "Medien-Optionen"; -/* Alert title when there is issues loading an asset to preview. */ -"Media preview failed." = "Bei der Medienvorschau ist ein Fehler aufgetreten."; - /* Alert displayed to the user when multiple media items have uploaded successfully. */ "Media uploaded (%ld files)" = "Hochgeladene Medien (%ld Dateien)"; @@ -4556,9 +4446,6 @@ translators: Block name. %s: The localized block name */ Label for the share message field on the post settings. */ "Message" = "Nachricht"; -/* Title of section containing image / video metadata such as size and file type */ -"Metadata" = "Metadaten"; - /* Option to select the Microsft Outlook app when logging in with magic links */ "Microsoft Outlook" = "Microsoft Outlook"; @@ -4578,13 +4465,11 @@ translators: Block name. %s: The localized block name */ "Months and Years" = "Monate und Jahre"; /* Accessibility label for more button in dashboard quick start card. - Accessibility label for the More button in Page List. Accessibility label for the More button in Post List (compact view). Accessibility label for the More button on formatting toolbar. Accessibility label for the More button on Reader Cell Accessibility label for the More button on Reader's post details - Action button to display more available options - Label for the more post button. Tapping displays an action sheet with post options. */ + Action button to display more available options */ "More" = "Mehr"; /* Action button to display more available options @@ -4642,15 +4527,8 @@ translators: Block name. %s: The localized block name */ /* Screen reader text for button that will move the menu item */ "Move menu item" = "Menüeintrag verschieben"; -/* Label for a button that moves a page to the draft folder - Label for an option that moves a post to the draft folder */ -"Move to Draft" = "Verschiebe zu Entwürfe"; - -/* Label for a button that moves a page to the trash folder - Label for a option that moves a post to the trash folder - Title for button on the comment details page that moves the comment to trash when tapped. +/* Title for button on the comment details page that moves the comment to trash when tapped. Trash option in the trash confirmation alert. - Trash option in the trash page confirmation alert. Trashes the comment */ "Move to Trash" = "In Papierkorb verschieben"; @@ -4682,7 +4560,8 @@ translators: Block name. %s: The localized block name */ Title of My Site tab */ "My Site" = "Meine Webseite"; -/* Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ +/* Title for site picker screen. + Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ "My Sites" = "Meine Websites"; /* Siri Suggestion to open My Sites */ @@ -4932,9 +4811,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title for the view when there aren't any Activities to display in the Activity Log for a given filter. */ "No matching events found." = "Keine passenden Ereignisse gefunden."; -/* Message displayed when no results are returned from a media library search. Should match Calypso. - Phrase to show when the user search for images but there are no result to show. - Phrase to show when the user searches for GIFs but there are no result to show. */ +/* Message displayed when no results are returned from a media library search. Should match Calypso. */ "No media matching your search" = "Keine Medien entsprechen deiner Suche"; /* Menus text shown when no menus were available for loading the Menus editor. */ @@ -4952,8 +4829,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Displayed in the Notifications Tab as a title, when there are no notifications */ "No notifications yet" = "Es gibt noch keine Benachrichtigungen"; -/* Displayed when the user is searching the pages list and there are no matching pages - Text displayed when there's no matching with the text search */ +/* Text displayed when there's no matching with the text search */ "No pages matching your search" = "Keine Seiten entsprechen deiner Suche"; /* Text displayed when search for plugins returns no results */ @@ -4974,9 +4850,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Message shown whent the reader finds no posts for the chosen tag */ "No posts have been made recently with this tag." = "In letzter Zeit wurden keine Beiträge mit diesem Schlagwort veröffentlicht."; -/* Displayed when the user is searching the posts list and there are no matching posts */ -"No posts matching your search" = "Keine Beiträge entsprechen deiner Suche"; - /* Accessibility value for a Stats' Posting Activity Month if there are no posts. */ "No posts." = "Keine Beiträge."; @@ -5077,9 +4950,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* A message title */ "Nothing liked yet" = "Noch nichts mit „Gefällt mir“ markiert"; -/* Default message for empty media picker */ -"Nothing to show" = "Es gibt nichts zu zeigen."; - /* Notifications Details Accessibility Identifier */ "Notification Details Table" = "Tabelle für Benachrichtigungsdetails"; @@ -5139,7 +5009,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Alert dismissal title Button title. Acknowledges a prompt. Button title. An acknowledgement of the message displayed in a prompt. - Confirmation of action Default action Dismisses the alert Marks all notifications as read. @@ -5201,9 +5070,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* No comment provided by engineer. */ "Only show excerpt" = "Nur Textauszug anzeigen"; -/* Message telling the user that they've only enabled limited photo library permissions for the app. */ -"Only the selected photos you've given access to are available." = "Es sind nur die ausgewählten Fotos verfügbar, für die du den Zugriff freigegeben hast."; - /* An informal exclaimation that means `something went wrong`. Title for the view when there's an error loading a comment. Title for the view when there's an error loading Activity Log @@ -5238,9 +5104,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title of a button that opens the apps settings in the system Settings.app */ "Open Settings" = "Einstellungen öffnen"; -/* Editor button to swich the media picker from quick mode to full picker */ -"Open full media picker" = "Vollständige Medienauswahl öffnen"; - /* No comment provided by engineer. */ "Open in Safari" = "In Safari öffnen"; @@ -5280,6 +5143,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Divider on initial auth view separating auth options. */ "Or" = "Oder"; +/* Instruction text for other forms of two-factor auth methods. */ +"Or choose another form of authentication." = "Oder wähle eine andere Authentifizierungsmethode aus."; + /* Label for button to log in using site address. Underscores _..._ denote underline. */ "Or log in by _entering your site address_." = "Oder melde dich an, indem du _deine Website-Adresse eingibst_."; @@ -5338,15 +5204,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title shown when selecting a post type of Page from the Share Extension. */ "Page" = "Seite"; -/* Prompts the user that a restored page was moved to the drafts list. */ -"Page Restored to Drafts" = "Seite unter „Entwürfe“ wiederhergestellt"; - -/* Prompts the user that a restored page was moved to the published list. */ -"Page Restored to Published" = "Seite unter „Veröffentlicht“ wiederhergestellt"; - -/* Prompts the user that a restored page was moved to the scheduled list. */ -"Page Restored to Scheduled" = "Seite unter „Geplant“ wiederhergestellt"; - /* Name of the button to open the page settings The title of the Page Settings screen. */ "Page Settings" = "Seiten-Einstellungen"; @@ -5363,9 +5220,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of notification displayed when a page has failed to upload. */ "Page failed to upload" = "Seite konnte nicht hochgeladen werden"; -/* A short message explaining that a page was moved to the trash bin. */ -"Page moved to trash." = "Seite in den Papierkorb verschoben."; - /* Title of notification displayed when a page has been successfully saved as a draft. */ "Page pending review" = "Überprüfung der Seite steht aus"; @@ -5437,8 +5291,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title of pending Comments filter. */ "Pending" = "Ausstehend"; -/* Name for the status of a post pending review. - Title of the Pending Review Badge */ +/* Name for the status of a post pending review. */ "Pending review" = "Ausstehender Review"; /* Noun. Title of the people management feature. @@ -5467,12 +5320,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Photography site intent topic */ "Photography" = "Fotografie"; -/* Tab bar title for the Photos tab in Media Picker */ -"Photos" = "Fotos"; - -/* Subtitle for placeholder in Free Photos. The company name 'Pexels' should always be written as it is. */ -"Photos provided by Pexels" = "Fotos bereitgestellt von Pexels"; - /* Title for selecting a new username in the site creation flow. */ "Pick username" = "Wähle einen Benutzernamen aus"; @@ -5565,7 +5412,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Please enter the password for your WordPress.com account to log in with your Apple ID." = "Bitte gib das Passwort für dein WordPress.com-Konto ein, um dich mit deiner Apple-ID anzumelden."; /* Instruction text on the two-factor screen. */ -"Please enter the verification code from your authenticator app, or tap the link below to receive a code via SMS." = "Gib bitte den Verifizierungscode aus deiner Authenticator-App ein oder tippe auf den unten stehenden Link, um einen Code per SMS zu erhalten."; +"Please enter the verification code from your authenticator app." = "Gib bitte den Verifizierungscode aus deiner Authenticator-App ein."; /* Popup message to ask for user credentials (fields shown below). */ "Please enter your credentials" = "Bitte Zugangsdaten eingeben"; @@ -5660,15 +5507,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ The post formats available for the post. Should be the same as in core WP. */ "Post Format" = "Artikel-Formatvorlage"; -/* Prompts the user that a restored post was moved to the drafts list. */ -"Post Restored to Drafts" = "Beitrag als Entwurf wiederhergestellt"; - -/* Prompts the user that a restored post was moved to the published list. */ -"Post Restored to Published" = "Beitrag als veröffentlicht wiederhergestellt"; - -/* Prompts the user that a restored post was moved to the scheduled list. */ -"Post Restored to Scheduled" = "Beitrag als geplant wiederhergestellt"; - /* Name of the button to open the post settings The title of the Post Settings screen. */ "Post Settings" = "Beitragseinstellungen"; @@ -5688,9 +5526,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of notification displayed when a post has failed to upload. */ "Post failed to upload" = "Beitrag konnte nicht hochgeladen werden"; -/* A short message explaining that a post was moved to the trash bin. */ -"Post moved to trash." = "Beitrag in den Papierkorb verschoben."; - /* Title of notification displayed when a post has been successfully saved as a draft. */ "Post pending review" = "Überprüfung des Beitrags steht aus"; @@ -5749,9 +5584,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Period Stats 'Posts and Pages' header */ "Posts and Pages" = "Beiträge und Seiten"; -/* Title of the Posts Page Badge */ -"Posts page" = "Beitragsseite"; - /* Message informing the user that their static homepage for posts was set successfully */ "Posts page successfully updated" = "Beitragsseite wurde erfolgreich aktualisiert"; @@ -5764,9 +5596,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* A message explaining the Posts I Like feature in the reader */ "Posts that you like will appear here." = "Beiträge, die dir gefallen, erscheinen hier."; -/* Subtitle for placeholder in Tenor picker. `The company name 'Tenor' should always be written as it is. */ -"Powered by Tenor" = "Bereitgestellt von Tenor"; - /* Browse premium themes selection title */ "Premium" = "Premium"; @@ -5785,18 +5614,12 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title for screen to preview a static content. */ "Preview" = "Vorschau"; -/* Action for Media Picker to preview the selected media items. The argument in the string represents the number of elements (as numeric digits) selected */ -"Preview %@" = "Vorschau von %@"; - /* Title for web preview device switching button */ "Preview Device" = "Vorschau-Gerät"; /* Title on display preview error */ "Preview Unavailable" = "Vorschau nicht verfügbar"; -/* Accessibility label for media item preview for user's viewing an item in their media library */ -"Preview media" = "Medien-Vorschau anzeigen"; - /* No comment provided by engineer. */ "Preview page" = "Seitenvorschau"; @@ -5843,8 +5666,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Privacy notice for California users" = "Datenschutzhinweis für Benutzer in Kalifornien"; /* Name for the status of a post that is marked private. - Privacy setting for posts set to 'Private'. Should be the same as in core WP. - Title of the Private Badge */ + Privacy setting for posts set to 'Private'. Should be the same as in core WP. */ "Private" = "Privat"; /* No comment provided by engineer. */ @@ -5894,12 +5716,10 @@ translators: %s: Select control button label e.g. \"Button width\" */ Label for the publish date button. */ "Publish Date" = "Veröffentlichungsdatum"; -/* A short phrase indicating a post is due to be immedately published. - Label for a button that moves a page to the published folder, publishing with the current date/time. */ +/* A short phrase indicating a post is due to be immedately published. */ "Publish Immediately" = "Sofort veröffentlichen"; /* Label for a button that publishes the post - Label for an option that moves a publishes a post immediately Title of button allowing the user to immediately publish the post they are editing. */ "Publish Now" = "Jetzt veröffentlichen"; @@ -5917,8 +5737,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Name for the status of a published post. Period Stats 'Published' header - Title of the published filter. This filter shows a list of posts that the user has published. - Title of the published header in search list. */ + Title of the published filter. This filter shows a list of posts that the user has published. */ "Published" = "Veröffentlicht"; /* Precedes the name of the blog just posted on */ @@ -6060,8 +5879,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of the completion screen of the Blogging Reminders Settings screen when the reminders are removed. */ "Reminders removed" = "Entfernte Erinnerungen"; -/* Add asset to media picker list - Alert button to confirm a plugin to be removed +/* Alert button to confirm a plugin to be removed Button label when removing a blog Label for the Remove Feature Image icon. Tapping will show a confirmation screen for removing the feature image from the post. Remove Action @@ -6214,9 +6032,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of secondary button on alert prompting verify their accounts while attempting to publish */ "Resend" = "Erneut senden"; -/* Title of the reset button */ -"Reset" = "Zurücksetzen"; - /* Accessibility label for the reset activity type button */ "Reset Activity Type filter" = "Aktivitätstyp-Filter zurücksetzen"; @@ -6271,12 +6086,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ Button title, displayed when media has failed to upload. Allows the user to try the upload again. Button title. Retries uploading a post. If a user taps the button with this label, the action that evinced this error view will be retried. - Label for a button that attempts to re-upload a page that previously failed to upload. - Label for the retry post upload button. Tapping attempts to upload the post again. Opens the media library . Retries the upload of a user's post. Retry updating User's Role - Retry uploading the post. Retry. Action Retry. Verb – retry a failed media upload. The Jetpack view button title used when an error occurred @@ -6288,9 +6100,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Button title that triggers a scan */ "Retry Scan" = "Scan erneut versuchen"; -/* User action to retry media upload. */ -"Retry Upload" = "Upload erneut starten"; - /* User action to retry all failed media uploads. */ "Retry all" = "Alles erneut versuchen"; @@ -6388,9 +6197,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Accessibility label for the 'Save Post' button when a post has been saved. */ "Saved Post" = "Gespeicherter Beitrag"; -/* Text displayed in HUD when a media item's metadata (title, etc) is saved successfully. */ -"Saved!" = "Gespeichert!"; - /* Accessibility hint for the 'Save Post' button. */ "Saves this post for later." = "Speichert diesen Beitrag für später."; @@ -6401,7 +6207,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Saving post…" = "Beitrag wird gespeichert …"; /* Menus save button title while it is saving a Menu. - Text displayed in HUD while a media item's metadata (title, etc) is being saved. Text displayed in HUD while a post is being saved as a draft. */ "Saving..." = "Wird gespeichert …"; @@ -6492,21 +6297,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* No comment provided by engineer. */ "Search or type URL" = "URL suchen oder eingeben"; -/* Text displayed when the search controller will be presented */ -"Search pages" = "Seiten suchen"; - -/* Text displayed when the search controller will be presented */ -"Search posts" = "Beiträge durchsuchen"; - /* No comment provided by engineer. */ "Search settings" = "Sucheinstellungen"; -/* Title for placeholder in Tenor picker */ -"Search to find GIFs to add to your Media Library!" = "Suche nach GIFs, die du deiner Mediathek hinzufügen kannst!"; - -/* Title for placeholder in Free Photos */ -"Search to find free photos to add to your Media Library!" = "Suche nach kostenlosen Fotos, die du deiner Mediathek hinzufügen kannst!"; - /* Menus search bar placeholder text. */ "Search..." = "Suchen ..."; @@ -6577,9 +6370,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Register Domain - Domain contact information field placeholder for Country */ "Select Country" = "Land wählen"; -/* Title of button that allows the user to select more photos to access within the app */ -"Select More" = "Mehr auswählen"; - /* Blog Picker's Title */ "Select Site" = "Website auswählen"; @@ -6601,9 +6391,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Register domain - Title for the Choose domain button of Suggested domains screen */ "Select domain" = "Domain auswählen"; -/* Accessibility hint for actions when displaying media items. */ -"Select media." = "Dateien auswählen"; - /* Accessibility label for selecting paragraph style button on formatting toolbar. */ "Select paragraph style" = "Absatz-Stil auswählen"; @@ -6707,19 +6494,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label for connected service in Publicize stat. */ "Service" = "Dienst"; -/* Label for a button that opens the Set Parent options view controller - Navigation title displayed on the navigation bar */ +/* Navigation title displayed on the navigation bar */ "Set Parent" = "Übergeordnete festlegen"; /* No comment provided by engineer. */ "Set as Featured Image" = "Als Beitragsbild festlegen "; -/* Label for a button that sets the selected page as the site's Homepage */ -"Set as Homepage" = "Als Homepage festlegen"; - -/* Label for a button that sets the selected page as the site's Posts page */ -"Set as Posts Page" = "Als Beitragsseite festlegen"; - /* Notice confirming that an image has been set as the post's featured image. */ "Set as featured image" = "Als Beitragsbild festlegen"; @@ -6763,7 +6543,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Accessibility label for share buttons in nav bars Button label to share a post Button label to share a web page - Share the post. Shares the comment URL Spoken accessibility label Title for a button that allows the user to share their answer to the prompt. @@ -7149,8 +6928,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Name of setting configured when a site uses a static page as its homepage */ "Static Homepage" = "Statische Seite"; -/* Label for post stats option. Tapping displays statistics for a post. - Noun. Abbreviation of Statistics. Name of the Stats feature +/* Noun. Abbreviation of Statistics. Name of the Stats feature Noun. Abbv. of Statistics. Links to a blog's Stats screen. Stats 3D Touch Shortcut Stats window title @@ -7181,9 +6959,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label text that defines a post marked as sticky */ "Sticky" = "Oben gehalten"; -/* Accessibility label for a sticky post in the post list. */ -"Sticky." = "Oben gehalten."; - /* User action to stop upload. */ "Stop upload" = "Hochladen beenden"; @@ -7240,7 +7015,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ Theme Support action title */ "Support" = "Support"; -/* Button used to switch site */ +/* Title for back button that leads to the site picker screen. */ "Switch Site" = "Website wechseln"; /* Switches the Editor to HTML Mode */ @@ -7328,9 +7103,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label explaining why users might want to add tags. */ "Tags help tell readers what a post is about. Separate different tags with commas." = "Schlagwörter helfen Lesern zu verstehen, worum es in einem Beitrag geht. Trenne verschiedene Schlagwörter mit Kommas."; -/* Menu option for taking an image or video with the device's camera. */ -"Take Photo or Video" = "Foto oder Video aufnehmen"; - /* No comment provided by engineer. */ "Take a Photo" = "Nimm ein Foto auf"; @@ -7401,12 +7173,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Accessibility hint */ "Tap to select the previous period" = "Antippen, um die vorherige Periode auszuwählen"; -/* Accessibility hint for button used to switch site */ -"Tap to switch to another site, or add a new site" = "Tippen, um zu einer anderen Website zu wechseln oder eine neue Website hinzuzufügen"; - -/* Accessibility hint for media item preview for user's viewing an item in their media library */ -"Tap to view media in full screen" = "Tippen, um Medien im Vollbildmodus zu öffnen"; - /* Accessibility hint for a button that opens a new view with more details. */ "Tap to view more details." = "Tippen, um mehr Details anzuzeigen."; @@ -7452,10 +7218,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* No comment provided by engineer. */ "Text formatting controls are located within the toolbar positioned above the keyboard while editing a text block" = "Die Textformatierungskontrollen findest du beim Bearbeiten eines Textblocks in der Toolbar über der Tastatur."; -/* Button title - The button's title text to send a 2FA code via SMS text message. */ +/* Button title */ "Text me a code instead" = "Code per SMS senden"; +/* The button's title text to send a 2FA code via SMS text message. */ +"Text me a code via SMS" = "Code per SMS zusenden"; + /* Message of alert when theme activation succeeds */ "Thanks for choosing %@ by %@" = "Danke, dass du dich für %1$@ von %2$@ entschieden hast."; @@ -7483,9 +7251,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Error message shown to a user who is trying to share to Facebook but does not have any available Facebook Pages. */ "The Facebook connection cannot find any Pages. Publicize cannot connect to Facebook Profiles, only published Pages." = "Die Facebook-Verbindung findet keine Seiten. Publicize kann keine Verbindung zu Facebook-Profilen herstellen, sondern nur zu öffentlichen Seiten."; -/* Message shown when a GIF failed to load while trying to add it to the Media library. */ -"The GIF could not be added to the Media Library." = "Die GIF-Datei konnte nicht zur Mediathek hinzugefügt werden."; - /* Description shown when a user logs in with Google but no matching WordPress.com account is found */ "The Google account \"%@\" doesn't match any account on WordPress.com" = "Das Google-Konto „%@“ stimmt mit keinem WordPress.com-Konto überein."; @@ -7518,7 +7283,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "The best way to become a better writer is to build a writing habit and share with others - that’s where Prompts come in!" = "Wenn du deine Schreibkünste verbessern willst, kreiere deinen eigenen Schreibstil und teile ihn mit anderen – und dabei helfen dir unsere Themenvorschläge!"; /* No comment provided by engineer. */ -"The certificate for this server is invalid. You might be connecting to a server that is pretending to be “%@” which could put your confidential information at risk.\n\nWould you like to trust the certificate anyway?" = "Das Zertifikat für diesen Server ist ungültig. Du könntest zu einem Server verbinden, der nur vorgibt \"%@\" zu sein, was ein Sicherheitsrisiko darstellt.\n\nMöchtest Du dem Zertifikat trotzdem vertrauen?"; +"The certificate for this server is invalid. You might be connecting to a server that is pretending to be “%@” which could put your confidential information at risk.\n\nWould you like to trust the certificate anyway?" = "Das Zertifikat für diesen Server ist ungültig. Möglicherweise stellst du eine Verbindung zu einem Server her, der vorgibt, „%@“ zu sein, wodurch deine vertraulichen Daten gefährdet werden könnten.\n\nMöchtest du dem Zertifikat trotzdem vertrauen?"; /* Message informing the user that posts page cannot be edited */ "The content of your latest posts page is automatically generated and cannot be edited." = "Der Inhalt der Seite mit deinen letzten Beiträgen wird automatisch generiert und kann nicht bearbeitet werden."; @@ -7613,7 +7378,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Error message shown when user attempts to remove the site owner. */ "The user you are trying to remove is the owner of this site. Please contact support for assistance." = "Der Benutzer, den du zu entfernen versuchst, ist der Betreiber dieser Website. Bitte kontaktiere den Support, um Unterstützung zu erhalten."; -/* Error message informing a user about an invalid password. */ +/* No comment provided by engineer. */ "The username or password stored in the app may be out of date. Please re-enter your password in the settings and try again." = "Der Zugangsname und das Passwort, die in der App gespeichert waren, sind wohl nicht mehr aktuell. Bitte gib dein Passwort in den Einstellungen noch einmal ein."; /* Message shown when a video failed to load while trying to add it to the Media library. */ @@ -7681,9 +7446,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* A short error message letting the user know about a problem displaying a post. */ "There was a problem displaying this post." = "Beim Anzeigen dieses Beitrags ist ein Fehler aufgetreten."; -/* Error message displayed when the Media Library is unable to load a full sized preview of an item. */ -"There was a problem loading the media item." = "Beim Laden des Medienelements ist ein Fehler aufgetreten."; - /* The loading view subtitle displayed when an error occurred */ "There was a problem loading your data, refresh your page to try again." = "Beim Laden deiner Daten ist ein Fehler aufgetreten. Aktualisiere deine Seite und versuche es erneut."; @@ -7696,9 +7458,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Explaining to the user there was an error trying to obtain the current location of the user. */ "There was a problem when trying to access your location. Please try again later." = "Es gab ein Problem beim Versuch, auf deinen Standort zuzugreifen. Versuche es später erneut."; -/* Explaining to the user there was an generic error accesing media. */ -"There was a problem when trying to access your media. Please try again later." = "Beim Versuch, auf deine Medien zuzugreifen, ist ein Problem aufgetreten. Versuche es später erneut."; - /* Message for stories unknown error. */ "There was a problem with the Stories editor. If the problem persists you can contact us via the Me > Help & Support screen." = "Es ist ein Problem mit dem Story-Editor aufgetreten. Wenn das Problem weiterhin auftritt, kannst du uns über „Ich > Hilfe & Support“ kontaktieren."; @@ -7769,9 +7528,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* A description informing the user in order to proceed with this feature we will need camera permissions, and how to enable it. */ "This app needs permission to access the Camera to scan login codes, tap on the Open Settings button to enable it." = "Diese App braucht eine Berechtigung, um zum Scannen von Anmeldecodes auf die Kamera zugreifen zu dürfen. Tippe auf den Button „Einstellungen öffnen“, um die Berechtigung zu erteilen."; -/* Explaining to the user why the app needs access to the device media library. */ -"This app needs permission to access your device media library in order to add photos and\/or video to your posts. Please change the privacy settings if you wish to allow this." = "Diese App benötigt das Recht, auf die Mediathek deines Geräts zuzugreifen, um deinen Beiträgen Fotos und\/oder Videos hinzuzufügen. Wenn du dies zulassen möchtest, ändere bitte die Datenschutzeinstellungen."; - /* No comment provided by engineer. */ "This color combination may be hard for people to read. Try using a brighter background color and\/or a darker text color." = "Diese Farbkombination ist unter Umständen für manche Menschen schwer lesbar. Wähle eine hellere Hintergrundfarbe und\/oder eine dunklere Textfarbe."; @@ -7881,6 +7637,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Description of a Quick Start Tour */ "Time to finish setting up your site! Our checklist walks you through the next steps." = "Schließe nun die Einrichtung deiner Website ab! Unsere Checkliste leitet dich durch die nächsten Schritte."; +/* Error when the uses takes more than 1 minute to submit a security key. */ +"Time's up, but don't worry, your security is our priority. Please try again!" = "Die Zeit ist abgelaufen, aber mach dir keine Sorgen, deine Sicherheit steht bei uns an erster Stelle. Versuche es bitte noch einmal!"; + /* WordPress.com Marketing Footer Text */ "Tips for getting the most out of WordPress.com." = "Tipps, um das Meiste aus WordPress.com herauszukriegen."; @@ -8004,24 +7763,20 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title for the traffic section in site settings screen */ "Traffic" = "Traffic"; +/* Describes a domain that was transferred from elsewhere to wordpress.com */ +"Transferred Domain" = "Übertragene Domain"; + /* translators: %s: block title e.g: \"Paragraph\". */ "Transform %s to" = "%s umwandeln in "; /* No comment provided by engineer. */ "Transform block…" = "Block umwandeln …"; -/* Accessibility label for trash button to delete items from the user's media library - Accessibility label for trash buttons in nav bars +/* Accessibility label for trash buttons in nav bars Trashes a comment Trashes the comment */ "Trash" = "Entfernen"; -/* Accessibility hint for trash button to delete items from the user's media library */ -"Trash selected media" = "Ausgewählte Medien in den Papierkorb legen"; - -/* Title of the trash page confirmation alert. */ -"Trash this page?" = "Diese Seite in den Papierkorb verschieben?"; - /* Title of the trash confirmation alert. */ "Trash this post?" = "Diesen Beitrag in den Papierkorb verschieben?"; @@ -8139,9 +7894,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Shown when a user logs in with Google but it subsequently fails to work as login to WordPress.com */ "Unable To Connect" = "Verbindung kann nicht hergestellt werden"; -/* An error message. */ -"Unable to Connect" = "Keine Verbindung möglich"; - /* Title for stories unknown error. */ "Unable to Create Stories Editor" = "Editor zum Erstellen von Storys kann nicht geöffnet werden"; @@ -8157,9 +7909,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* An error message shown when there is an issue creating new invite links. */ "Unable to create new invite links." = "Neue Einladungslinks können nicht erstellt werden."; -/* Text displayed in HUD if there was an error attempting to delete a group of media items. */ -"Unable to delete all media items." = "Es konnten nicht alle Medienelemente gelöscht werden."; - /* Text displayed in HUD if there was an error attempting to delete a media item. */ "Unable to delete media item." = "Medienelement kann nicht gelöscht werden."; @@ -8223,12 +7972,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Message displayed when sharing a link to the downloadable backup fails. */ "Unable to share link" = "Link kann nicht geteilt werden"; -/* Message that appears when a user tries to trash a page while their device is offline. */ -"Unable to trash pages while offline. Please try again later." = "Seiten können offline nicht in den Papierkorb gelegt werden. Bitte versuche es später erneut."; - -/* Message that appears when a user tries to trash a post while their device is offline. */ -"Unable to trash posts while offline. Please try again later." = "Beiträge können offline nicht in den Papierkorb gelegt werden. Bitte versuche es später erneut."; - /* Notice title when turning site notifications off fails. */ "Unable to turn off site notifications" = "Website-Benachrichtigungen können nicht deaktiviert werden"; @@ -8301,8 +8044,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ Button title. Reverts the previous notification operation Revert an operation Revert enabling notification after successfully subcribing to the comments for the post. - The title of an 'undo' button. Tapping the button moves a trashed page out of the trash folder. - The title of an 'undo' button. Tapping the button moves a trashed post out of the trash folder. Undo action */ "Undo" = "Rückgängig"; @@ -8345,9 +8086,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title for Unknown HTML Editor */ "Unknown HTML" = "Unbekanntes HTML"; -/* Label to use when creation date from media asset is not know. */ -"Unknown creation date" = "Erstellungsdatum unbekannt"; - /* No comment provided by engineer. */ "Unknown error" = "Unbekannter Fehler"; @@ -8513,6 +8251,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of a row displayed on the debug screen used to configure the sandbox store use in the App. */ "Use Sandbox Store" = "Sandbox Store verwenden"; +/* The button's title text to use a security key. */ +"Use a security key" = "Sicherheitsschlüssel verwenden"; + /* Option to enable the block editor for new posts */ "Use block editor" = "Block-Editor benutzen"; @@ -8588,15 +8329,10 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of an alert informing users that the video they are trying to select is not allowed. */ "Video not uploaded" = "Video nicht hochgeladen"; -/* Accessibility label for video thumbnails in the media collection view. The parameter is the creation date of the video. */ -"Video, %@" = "Video, %@"; - /* Period Stats 'Videos' header */ "Videos" = "Videos"; /* Button title. Displays a summary / sharing screen for a specific post. - Label for a button that opens the page when tapped. - Label for the view post button. Tapping displays the post as it appears on the web. Opens the post epilogue screen to allow sharing / viewing of a post. Theme View action title Verb. The screen title shown when viewing a post inside the app. */ @@ -8711,6 +8447,10 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Message shown on screen while waiting for Google to finish its signup process. */ "Waiting for Google to complete…" = "Registrierung wird von Google abgeschlossen …"; +/* Text while the webauthn signature is being verified + Text while waiting for a security key challenge */ +"Waiting for security key" = "Warten auf Sicherheitsschlüssel"; + /* View title during the Google auth process. */ "Waiting..." = "Warten …"; @@ -9085,6 +8825,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* An error message shown when a wpcom user provides the wrong password. */ "Whoops, something went wrong and we couldn't log you in. Please try again!" = "Ups, aufgrund eines Fehlers konnten wir dich nicht anmelden. Versuche es bitte noch einmal!"; +/* Generic error on the 2FA screen */ +"Whoops, something went wrong. Please try again!" = "Ups, etwas ist schiefgelaufen. Versuche es bitte noch einmal!"; + +/* Error when the uses chooses an invalid security key on the 2FA screen. */ +"Whoops, that security key does not seem valid. Please try again with another one" = "Ups, dieser Sicherheitsschlüssel ist anscheinend nicht gültig. Versuche es bitte noch einmal mit einem anderen"; + /* Error message shown when an incorrect two factor code is provided. */ "Whoops, that's not a valid two-factor verification code. Double-check your code and try again!" = "Ups, das ist kein gültiger zweistufiger Verifizierungscode. Überprüfe deinen Code und versuche es noch einmal!"; @@ -9112,9 +8858,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Siri Suggestion to open Support */ "WordPress Help" = "WordPress-Hilfe"; -/* Name for the WordPress Media Library */ -"WordPress Media" = "WordPress-Medien"; - /* Accessibility label for selecting an image or video from the user's WordPress media library on formatting toolbar. */ "WordPress Media Library" = "WordPress-Mediathek"; @@ -9375,7 +9118,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "You received *50 likes* on your comment" = "Du hast *50 Likes* für deinen Kommentar bekommen"; /* Example Likes notification displayed in the prologue carousel of the app. Number of likes should marked with * characters and will be displayed as bold text. */ -"You received *50 likes* on your site today" = "Du hast *50 likes* on your site today dafür bekommen"; +"You received *50 likes* on your site today" = "Du hast heute *50 Likes* für deine Website erhalten"; /* Message displayed in popup when user has the option to load unsaved changes. is a placeholder for a new line, and the two %@ are placeholders for the date of last save on this device, and date of last autosave on another device, respectively. */ @@ -9429,9 +9172,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Share extension error dialog text. */ "Your account does not have permission to upload media to this site. The Site Administrator can change these permissions." = "Dein Konto verfügt nicht über die nötigen Rechte zum Hochladen von Medien auf diese Website. Der Website-Administrator kann diese Rechte ändern."; -/* Explaining to the user why the app needs access to the device media library. */ -"Your app is not authorized to access media library due to active restrictions such as parental controls. Please check your parental control settings in this device." = "Deine App ist aufgrund aktiver Einschränkungen wie z. B. einer Kindersicherung nicht berechtigt, auf die Medienbibliothek zuzugreifen. Bitte überprüfe die Einstellungen der Kindersicherung in diesem Gerät."; - /* Title for the Jetpack Backup Complete message. */ "Your backup is now available for download" = "Du kannst dein Backup nun herunterladen."; @@ -9450,9 +9190,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of the site address section in the Domains Dashboard. */ "Your free WordPress.com address is" = "Deine kostenlose WordPress.com-Adresse lautet"; -/* Error message when picked media cannot be imported into stories. */ -"Your media could not be exported. If the problem persists you can contact us via the Me > Help & Support screen." = "Deine Medien konnten nicht exportiert werden. If the problem persists you can contact us via the Me > Hilfe & Support."; - /* Details about recently acquired domain on domain credit redemption success screen */ "Your new domain %@ is being set up. It may take up to 30 minutes for your domain to start working." = "Deine neue Domain „%@“ wird eingerichtet. Es kann bis zu 30 Minuten dauern, bis deine Domain funktioniert."; @@ -9576,8 +9313,38 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* This is the string we display when prompting the user to review the WordPress app */ "appRatings.wordpress.prompt" = "Was ist deine Meinung zu WordPress?"; -/* Label displayed on audio media items. */ -"audio" = "Audio"; +/* Option to enable the optimization of images when uploading. */ +"appSettings.media.imageOptimizationRow" = "Bilder optimieren"; + +/* Indicates an image will use high quality when uploaded. */ +"appSettings.media.imageQuality.high" = "Hoch"; + +/* Indicates an image will use low quality when uploaded. */ +"appSettings.media.imageQuality.low" = "Niedrig"; + +/* Indicates an image will use maximum quality when uploaded. */ +"appSettings.media.imageQuality.maximum" = "Maximal"; + +/* Indicates an image will use medium quality when uploaded. */ +"appSettings.media.imageQuality.medium" = "Mittel"; + +/* The quality of image used when uploading */ +"appSettings.media.imageQuality.title" = "Qualität"; + +/* Title for the image quality settings option. */ +"appSettings.media.imageQualityRow" = "Bildqualität"; + +/* Message of an alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.message" = "Die Bildoptimierung schrumpft Bilder für schnelleres Hochladen.\n\nDiese Option ist standardmäßig aktiviert, aber du kannst jederzeit Änderungen in den App-Einstellungen vornehmen."; + +/* Title of an alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.title" = "Sollen Bilder weiterhin optimiert werden?"; + +/* Title of button for turning off image optimization, displayed in the alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.turnOff" = "Nein, deaktivieren"; + +/* Title of button for leaving on image optimization, displayed in the alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.turnOn" = "Ja, aktiviert lassen"; /* translators: displays audio file extension. e.g. MP3 audio file */ "audio file" = "Audiodatei"; @@ -9691,7 +9458,44 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "blogHeader.actionCopyURL" = "URL kopieren"; /* Context menu button title */ -"blogHeader.actionOpenInBrowser" = "Im Browser öffnen"; +"blogHeader.actionVisitSite" = "Zur Website"; + +/* Title for a button that, when tapped, shows more info about participating in Bloganuary. */ +"bloganuary.dashboard.card.button.learnMore" = "Mehr erfahren"; + +/* Short description for the Bloganuary event, shown right below the title. */ +"bloganuary.dashboard.card.description" = "Im Januar erhältst du Blog-Schreibanregungen vom Bloganuary, unserer Community-Challenge. Diese soll dir helfen, Bloggen im neuen Jahr zur Gewohnheit zu machen."; + +/* Title for the Bloganuary dashboard card while Bloganuary is running. */ +"bloganuary.dashboard.card.runningTitle" = "Der Bloganuary ist da!"; + +/* Title for the Bloganuary dashboard card. */ +"bloganuary.dashboard.card.title" = "Der Bloganuary kommt!"; + +/* Title of a button that calls the user to enable the Blogging Prompts feature. */ +"bloganuary.learnMore.modal.button.promptsDisabled" = "Blog-Schreibanregungen aktivieren"; + +/* Title of a button that will dismiss the Bloganuary modal when tapped. +Note that the word 'go' here should have a closer meaning to 'start' rather than 'move forward'. */ +"bloganuary.learnMore.modal.button.promptsEnabled" = "Auf geht's!"; + +/* The second line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.description.second" = "Veröffentliche deine Antwort."; + +/* The third line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.description.third" = "Lese die Antworten anderer Blogger, um dich inspirieren zu lassen und neue Kontakte herzustellen."; + +/* The first line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.descriptions.first" = "Lasse dich jeden Tag von einer neuen Schreibanregung inspirieren."; + +/* An additional piece of information shown in case the user has the Blogging Prompts feature disabled. */ +"bloganuary.learnMore.modal.footer.addition" = "Um am Bloganuary teilzunehmen, musst du die Blog-Schreibanregungen aktivieren."; + +/* An informative excerpt shown in a subtler tone. */ +"bloganuary.learnMore.modal.footer.text" = "Im Rahmen des Bloganuary werden dir über die täglichen Blog-Schreibanregungen Themen für den Januar geschickt."; + +/* The headline text of the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.headline" = "Unsere Schreib-Challenge dauert einen Monat – sei dabei"; /* Verb. Dismisses the blogging prompt notification. */ "bloggingPrompt.pushNotification.customActionDescription.dismiss" = "Verwerfen"; @@ -9720,6 +9524,12 @@ Example: Comment on Example: Reply to Pamela Nguyen */ "comment.header.subText.reply" = "Antwort auf %1$@"; +/* Error message informing a user about an invalid password. */ +"common.reEnterPasswordMessage" = "Der in der App gespeicherte Benutzername oder das Passwort ist unter Umständen veraltet. Bitte gib dein Passwort in den Einstellungen erneut ein und versuche es noch einmal."; + +/* An error message. */ +"common.unableToConnect" = "Keine Verbindung möglich"; + /* Footnote for the privacy compliance popover. */ "compliance.analytics.popover.footnote" = "Mithilfe dieser Cookies können wir die Leistung optimieren, indem wir Informationen darüber sammeln, wie Benutzer mit unseren Websites interagieren."; @@ -9870,50 +9680,92 @@ Example: Reply to Pamela Nguyen */ /* Title for a menu action in the context menu on the Jetpack install card. */ "domain.dashboard.card.menu.hide" = "Ausblenden"; -/* Domain Purchase Completion footer */ -"domain.purchase.preview.footer" = "Es kann bis zu 30 Minuten dauern, bis deine individuelle Domain funktioniert."; +/* Search domain - Title for the Suggested domains screen */ +"domain.management.addDomain.search.title" = "Nach einer Domain suchen"; -/* Domain Purchase Completion description (only for FREE domains). */ -"domain.purchase.preview.free.description" = "Als Nächstes helfen wir dir dabei, sie für Besucher verfügbar zu machen."; +/* Domain management buy domain card button title */ +"domain.management.buy.card.button.title" = "Domain erhalten"; -/* Domain Purchase Completion description (only for PAID domains). */ -"domain.purchase.preview.paid.description" = "Wir haben dir deinen Beleg per E-Mail gesendet. Als Nächstes helfen wir dir dabei, sie für alle bereit zu machen."; +/* Domain management buy domain card subtitle */ +"domain.management.buy.card.subtitle" = "Füge später eine Website hinzu."; -/* Reflects that site is live when domain purchase feature flag is ON. */ -"domain.purchase.preview.title" = "Super, deine Website ist live!"; +/* Domain management buy domain card title */ +"domain.management.buy.card.title" = "Kaufe lediglich eine Domain"; + +/* The expired label of the domain card in All Domains screen. */ +"domain.management.card.expired.label" = "Abgelaufen"; + +/* The renews label of the domain card in All Domains screen. */ +"domain.management.card.renews.label" = "Wird verlängert"; + +/* The empty state button title in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.button.title" = "Eine Domain finden"; + +/* The empty state description in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.description" = "Tippe unten, um die perfekte Domain zu finden."; -/* Status of a domain in `Action Required` state */ -"domain.status.action.required" = "Handlung erforderlich"; +/* The empty state title in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.title" = "Du hast keine Domains"; -/* Status of a domain in `Active` state */ -"domain.status.active" = "Aktiv"; +/* The empty state description in All Domains screen when an error occurs */ +"domain.management.error.empty.state.description" = "Beim Laden deiner Domains ist ein Fehler aufgetreten. Wenn das Problem weiterhin besteht, wende dich bitte an den Support."; -/* Status of a domain in `Complete Setup` state */ -"domain.status.complete.setup" = "Einrichtung abschließen"; +/* The empty state title in All Domains screen when an error occurs */ +"domain.management.error.empty.state.title" = "Etwas ist schiefgelaufen"; -/* Status of a domain in `Error` state */ -"domain.status.error" = "Fehler"; +/* The empty state button title in All Domains screen when an error occurs */ +"domain.management.error.state.button.title" = "Erneut versuchen"; -/* Status of a domain in `Expired` state */ -"domain.status.expired" = "Abgelaufen"; +/* The empty state description in All Domains screen when the user is offline */ +"domain.management.offline.empty.state.description" = "Bitte überprüfe deine Netzwerkverbindung und versuche es erneut."; -/* Status of a domain in `Expiring Soon` state */ -"domain.status.expiring.soon" = "Läuft bald ab"; +/* The empty state title in All Domains screen when the user is offline */ +"domain.management.offline.empty.state.title" = "Keine Internetverbindung"; -/* Status of a domain in `Failed` state */ -"domain.status.failed" = "Fehlgeschlagen"; +/* Domain management choose site card button title */ +"domain.management.purchase.footer" = "* Bei allen kostenpflichtigen Jahrestarifen ist ein Jahr lang eine kostenlose Domain enthalten"; -/* Status of a domain in `In Progress` state */ -"domain.status.in.progress" = "In Bearbeitung"; +/* Domain management purchase domain screen title */ +"domain.management.purchase.subtitle" = "Keine Sorge, du kannst später ganz einfach eine Website hinzufügen."; -/* Status of a domain in `Renew` state */ -"domain.status.renew" = "Verlängern"; +/* Domain management purchase domain screen title. */ +"domain.management.purchase.title" = "Entscheide, wie du deine Domain nutzen möchtest"; -/* Status of a domain in `Verify Email` state */ -"domain.status.verify.email" = "E-Mail-Adresse verifizieren"; +/* The search bar title in All Domains screen. */ +"domain.management.search-bar.title" = "Domains suchen"; -/* Status of a domain in `Verifying` state */ -"domain.status.verifying" = "Wird geprüft"; +/* The empty state description in All Domains screen when the are no domains matching the search criteria */ +"domain.management.search.empty.state.description" = "Es konnten keine Domains gefunden werden, die mit deiner Suche für „%@“ übereinstimmen"; + +/* The empty state title in All Domains screen when the are no domains matching the search criteria */ +"domain.management.search.empty.state.title" = "Es wurden keine passenden Domains gefunden"; + +/* Domain management choose site card button title */ +"domain.management.site.card.button.title" = "Website auswählen"; + +/* Domain management choose site card subtitle */ +"domain.management.site.card.footer" = "Kostenlose Domain im ersten Jahr*"; + +/* Domain management choose site card subtitle */ +"domain.management.site.card.subtitle" = "Mit einer Website verwenden, mit der du bereits begonnen hast."; + +/* Domain management choose site card title */ +"domain.management.site.card.title" = "Vorhandene WordPress.com-Website"; + +/* Domain Management Screen Title */ +"domain.management.title" = "Alle Domains"; + +/* Domain Purchase Completion footer */ +"domain.purchase.preview.footer" = "Es kann bis zu 30 Minuten dauern, bis deine individuelle Domain funktioniert."; + +/* Domain Purchase Completion description (only for FREE domains). */ +"domain.purchase.preview.free.description" = "Als Nächstes helfen wir dir dabei, sie für Besucher verfügbar zu machen."; + +/* Domain Purchase Completion description (only for PAID domains). */ +"domain.purchase.preview.paid.description" = "Wir haben dir deinen Beleg per E-Mail gesendet. Als Nächstes helfen wir dir dabei, sie für alle bereit zu machen."; + +/* Reflects that site is live when domain purchase feature flag is ON. */ +"domain.purchase.preview.title" = "Super, deine Website ist live!"; /* The 'Best Alternative' label under the domain name in 'Choose a domain' screen */ "domain.suggestions.row.best-alternative" = "Beste Alternative"; @@ -9936,12 +9788,24 @@ Example: Reply to Pamela Nguyen */ /* The text to display for paid domains in 'Site Creation > Choose a domain' screen */ "domain.suggestions.row.yearly" = "pro Jahr"; +/* Title for the checkout screen. */ +"domains.checkout.title" = "Kasse"; + /* Action shown in a bottom notice to dismiss it. */ "domains.failure.dismiss" = "Verwerfen"; /* Content show when the domain selection action fails. */ "domains.failure.title" = "Die Domain, die du hinzufügen möchtest, kann derzeit leider nicht über die Jetpack-App erworben werden."; +/* Title for the screen where the user can choose how to use the domain they're end up purchasing. */ +"domains.purchase.choice.title" = "Domain kaufen"; + +/* Back button title that navigates back to the search domains screen. */ +"domains.search.backButton.title" = "Suchen"; + +/* Title of screen where user chooses a site to connect to their selected domain */ +"domains.sitePicker.title" = "Website auswählen"; + /* No comment provided by engineer. */ "double-tap to change unit" = "Zum Wechseln der Einheit zweimal tippen"; @@ -9959,6 +9823,15 @@ Example: Reply to Pamela Nguyen */ Site Address placeholder */ "example.com" = "example.com"; +/* Title for confirmation navigation bar button item */ +"externalMediaPicker.add" = "Hinzufügen"; + +/* Bottom toolbar title in the selection mode */ +"externalMediaPicker.toolbarSelectItemsPrompt" = "Bilder auswählen"; + +/* Bottom toolbar title in the selection mode */ +"externalMediaPicker.toolbarViewSelected" = "Auswahl anzeigen (%@)"; + /* Title of screen the displays the details of an advertisement campaign. */ "feature.blaze.campaignDetails.title" = "Kampagnendetails"; @@ -10058,9 +9931,6 @@ Example: Reply to Pamela Nguyen */ /* (placeholder) Help the user enter a URL into the field */ "http:\/\/my-site-address (URL)" = "http:\/\/meine-website-adresse (URL)"; -/* Label displayed on image media items. */ -"image" = "Bild"; - /* Sentence to justify why the app is asking permission from the user to use their camera. */ "infoplist.NSCameraUsageDescription" = "Um Fotos oder Videos für deine Beiträge aufzunehmen."; @@ -10361,6 +10231,9 @@ Example: Reply to Pamela Nguyen */ /* Indicating that referrer was marked as spam */ "marked as spam" = "als Spam markiert"; +/* Products header text in Me Screen. */ +"me.products.header" = "Produkte"; + /* Title of error prompt shown when a sync fails. */ "media.syncFailed" = "Medien konnten nicht synchronisiert werden"; @@ -10373,18 +10246,24 @@ Example: Reply to Pamela Nguyen */ /* Message of an alert informing users that the video they are trying to select is not allowed. */ "mediaExporter.videoLimitExceededError" = "Für das Hochladen von Videos, die länger als 5 Minuten dauern, ist ein kostenpflichtiger Tarif erforderlich."; -/* Verb. User action to dismiss error alert when failing to load media item. */ -"mediaItemTable.errorAlert.dismissButton" = "Verwerfen"; - /* Accessibility hint for add button to add items to the user's media library */ "mediaLibrary.addButtonAccessibilityHint" = "Neue Medien hinzufügen"; /* Accessibility label for add button to add items to the user's media library */ "mediaLibrary.addButtonAccessibilityLabel" = "Hinzufügen"; +/* Button name in the more menu */ +"mediaLibrary.aspectRatioGrid" = "Bildformat Raster"; + +/* Context menu button */ +"mediaLibrary.buttonDelete" = "Löschen"; + /* Media screen navigation bar button Select title */ "mediaLibrary.buttonSelect" = "Auswählen"; +/* Context menu button */ +"mediaLibrary.buttonShare" = "Teilen"; + /* Verb. Button title. Tapping cancels an action. */ "mediaLibrary.deleteConfirmationCancel" = "Abbrechen"; @@ -10406,6 +10285,21 @@ Example: Reply to Pamela Nguyen */ /* Text displayed in HUD after successfully deleting a media item */ "mediaLibrary.deletionSuccessMessage" = "Gelöscht!"; +/* The name of the media filter */ +"mediaLibrary.filterAll" = "Alle"; + +/* The name of the media filter */ +"mediaLibrary.filterAudio" = "Audio"; + +/* The name of the media filter */ +"mediaLibrary.filterDocuments" = "Dokumente"; + +/* The name of the media filter */ +"mediaLibrary.filterImages" = "Bilder"; + +/* The name of the media filter */ +"mediaLibrary.filterVideos" = "Videos"; + /* User action to delete un-uploaded media. */ "mediaLibrary.retryOptionsAlert.delete" = "Löschen"; @@ -10418,6 +10312,12 @@ Example: Reply to Pamela Nguyen */ /* Message displayed when no results are returned from a media library search. Should match Calypso. */ "mediaLibrary.searchResultsEmptyTitle" = "Keine Medien entsprechen deiner Suche"; +/* Text displayed in HUD if there was an error attempting to share a group of media items. */ +"mediaLibrary.sharingFailureMessage" = "Die ausgewählten Elemente konnten nicht geteilt werden."; + +/* Button name in the more menu */ +"mediaLibrary.squareGrid" = "Quadratisches Raster"; + /* Media screen navigation title */ "mediaLibrary.title" = "Medien"; @@ -10439,6 +10339,12 @@ Example: Reply to Pamela Nguyen */ /* The title of the button to dismiss the alert shown when the picked media cannot be imported into stories. */ "mediaPicker.failedMediaExportAlert.dismissButton" = "Verwerfen"; +/* Error message when picked media cannot be imported into stories. */ +"mediaPicker.failedMediaExportAlert.message" = "Deine Medien konnten nicht exportiert werden. Wenn das Problem weiterhin auftritt, kannst du uns über „Ich > Hilfe & Support“ kontaktieren."; + +/* Error title when picked media cannot be imported into stories. */ +"mediaPicker.failedMediaExportAlert.title" = "Medienexport fehlgeschlagen"; + /* Message for alert when access to camera is not granted */ "mediaPicker.noCameraAccessMessage" = "Diese App benötigt das Recht, auf die Kamera zuzugreifen, um neue Medien aufzunehmen. Wenn du dies zulassen möchtest, ändere bitte die Datenschutzeinstellungen."; @@ -10472,6 +10378,12 @@ Example: Reply to Pamela Nguyen */ /* The name of the action in the context menu */ "mediaPicker.takeVideo" = "Video aufnehmen"; +/* Navigation title for media preview. Example: 1 of 3 */ +"mediaPreview.NofM" = "%1$@ von %2$@"; + +/* Max image size in pixels (e.g. 300x300px) */ +"mediaSizeSlider.valueFormat" = "%1$d×%2$d px"; + /* The description in the Delete WordPress screen */ "migration.deleteWordpress.description" = "Offenbar hast du die WordPress-App noch installiert."; @@ -10484,9 +10396,6 @@ Example: Reply to Pamela Nguyen */ /* The title in the Delete WordPress screen */ "migration.deleteWordpress.title" = "Du brauchst die WordPress-App nicht mehr auf deinem Gerät"; -/* Primary button title in the migration done screen. */ -"migration.done.actions.primary.title" = "Beenden"; - /* Footer for the migration done screen. */ "migration.done.footer" = "Wir empfehlen dir, die WordPress-App auf deinem Gerät zu deinstallieren, um Datenkonflikte zu vermeiden."; @@ -10496,6 +10405,9 @@ Example: Reply to Pamela Nguyen */ /* Primary description in the migration done screen. */ "migration.done.primaryDescription" = "Wir haben all deine Daten und Einstellungen übertragen. Alles ist genauso wie vorher."; +/* Secondary description (second paragraph) in the migration done screen. */ +"migration.done.secondaryDescription" = "Es ist an der Zeit, deine WordPress-Reise in der Jetpack-App fortzusetzen!"; + /* Title of the migration done screen. */ "migration.done.title" = "Danke für deinen Wechsel zu Jetpack!"; @@ -10544,6 +10456,9 @@ Example: Reply to Pamela Nguyen */ /* The title in the migration welcome screen */ "migration.welcome.title" = "Willkommen bei Jetpack!"; +/* Primary button title in the migration done screen. */ +"migrationDone.actions.primaryTitle" = "Los geht's"; + /* Description for the static screen displayed prompting users to switch the Jetpack app. */ "movedToJetpack.description" = "Die Jetpack-App verfügt über alle Funktionen der WordPress-App und bietet ab sofort exklusiven Zugriff auf Statistiken, den Reader, Benachrichtigungen und mehr."; @@ -10619,6 +10534,30 @@ Example: Reply to Pamela Nguyen */ /* Message title for when a user has no sites. */ "mySite.noSites.title" = "Du hast keine Websites"; +/* Menu title for the add site option */ +"mySite.siteActions.addSite" = "Website hinzufügen"; + +/* Button that reveals more site actions */ +"mySite.siteActions.button" = "Website-Aktionen"; + +/* Accessibility hint for button used to show more site actions */ +"mySite.siteActions.hint" = "Tippe, um weitere Website-Aktionen anzuzeigen"; + +/* Menu title for the personalize home option */ +"mySite.siteActions.personalizeHome" = "Startseite personalisieren"; + +/* Menu title for the change site icon option */ +"mySite.siteActions.siteIcon" = "Website-Icon ändern"; + +/* Menu title for the change site title option */ +"mySite.siteActions.siteTitle" = "Website-Titel ändern"; + +/* Menu title for the switch site option */ +"mySite.siteActions.switchSite" = "Website wechseln"; + +/* Menu title for the visit site option */ +"mySite.siteActions.visitSite" = "Zur Website"; + /* Dismiss button title. */ "noResultsViewController.dismissButton" = "Schließen"; @@ -10634,14 +10573,20 @@ Example: Reply to Pamela Nguyen */ /* This is one of the buttons we display when prompting the user for a review */ "notifications.appRatings.sendFeedback.yes.buttonTitle" = "Feedback senden"; -/* Word separating the current index from the total amount. I.e.: 7 of 9 */ -"of" = "von"; +/* Badge for page cells */ +"pageList.badgeHomepage" = "Homepage"; + +/* Badge for page cells */ +"pageList.badgeLocalChanges" = "Lokale Änderungen"; -/* Label displayed on media items that are not video, image, or audio. */ -"other" = "andere"; +/* Badge for page cells */ +"pageList.badgePendingReview" = "Ausstehende Überprüfung"; -/* Promote the page with Blaze. */ -"pages.blaze.actionTitle" = "Mit Blaze bewerben"; +/* Badge for page cells */ +"pageList.badgePosts" = "Beitragsseite"; + +/* Badge for page cells */ +"pageList.badgePrivate" = "Privat"; /* Subtitle of the theme template homepage cell */ "pages.template.subtitle" = "Deine Startseite verwendet ein Theme-Template und wird im Webeditor geöffnet."; @@ -10649,6 +10594,36 @@ Example: Reply to Pamela Nguyen */ /* Title of the theme template homepage cell */ "pages.template.title" = "Startseite"; +/* Message informing the user that their static homepage page was set successfully */ +"pages.updatePage.successTitle" = "Seite wurde erfolgreich aktualisiert"; + +/* Delete option in the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.actionTitle" = "Unwiderruflich löschen"; + +/* Message of the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.alertMessage" = "Bist du sicher, dass du diese Seite endgültig löschen möchtest?"; + +/* Title of the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.alertTitle" = "Unwiderruflich löschen?"; + +/* Menu option for filtering posts by everyone */ +"pagesList.pagesByEveryone" = "Seiten von allen"; + +/* Menu option for filtering posts by me */ +"pagesList.pagesByMe" = "Seiten von mir"; + +/* Trash option in the trash page confirmation alert. */ +"pagesList.trash.actionTitle" = "In den Papierkorb verschieben"; + +/* Message of the trash page confirmation alert. */ +"pagesList.trash.alertMessage" = "Bist du sicher, dass du diese Seite in den Papierkorb verschieben möchtest?"; + +/* Title of the trash page confirmation alert. */ +"pagesList.trash.alertTitle" = "Diese Seite in den Papierkorb verschieben?"; + +/* Cancels an Action */ +"pagesList.trash.cancel" = "Abbrechen"; + /* No comment provided by engineer. */ "password" = "Passwort"; @@ -10688,6 +10663,51 @@ Example: Reply to Pamela Nguyen */ /* Register Domain - Domain contact information field Phone */ "phone number" = "Telefonnummer"; +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.createdTimeAgo" = "Erstellt %@"; + +/* Status mesasge for post cells */ +"post.deletingPostPermanentlyStatusMessage" = "Der Beitrag wird gelöscht …"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.editedTimeAgo" = "Bearbeitet %@"; + +/* Status mesasge for post cells */ +"post.movingToTrashStatusMessage" = "Der Beitrag wird in den Papierkorb verschoben …"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.publishedTimeAgo" = "Veröffentlicht %@"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.scheduledForDate" = "Geplant %@"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.trashedTimeAgo" = "In den Papierkorb verschoben %@"; + +/* Accessibility label for the post author in the post list. The parameter is the author name. For example, \"By Elsa.\" */ +"postList.a11y.authorChunkFormat" = "Von %@."; + +/* Accessibility label for a post's excerpt in the post list. The parameter is the post excerpt. For example, \"Excerpt. This is the first paragraph.\" */ +"postList.a11y.exerptChunkFormat" = "Textauszug. %@."; + +/* Accessibility label for a sticky post in the post list. */ +"postList.a11y.sticky" = "Oben gehalten."; + +/* Accessibility label for a post in the post list. The first placeholder is the post title. The second placeholder is the date. */ +"postList.a11y.titleAndDateChunkFormat" = "%1$@, %2$@."; + +/* Title for the 'Trash' post list row swipe action */ +"postList.swipeActionDelete" = "Papierkorb"; + +/* Title for the 'Delete' post list row swipe action */ +"postList.swipeActionDeletePermanently" = "Löschen"; + +/* Title for the 'Share' post list row swipe action */ +"postList.swipeActionShare" = "Teilen"; + +/* Title for the 'View' post list row swipe action */ +"postList.swipeActionView" = "Anzeigen"; + /* User action to dismiss featured media options. */ "postSettings.featuredImageUploadActionSheet.dismiss" = "Schließen"; @@ -10706,9 +10726,84 @@ Example: Reply to Pamela Nguyen */ /* Button in Post Settings */ "postSettings.setFeaturedImageButton" = "Beitragsbild festlegen"; +/* Error message on post/page settings screen */ +"postSettings.updateFailedMessage" = "Aktualisierung der Beitragseinstellungen fehlgeschlagen"; + /* Promote the post with Blaze. */ "posts.blaze.actionTitle" = "Mit Blaze bewerben"; +/* Label for the Post List option that cancels automatic uploading of a post. */ +"posts.cancelUpload.actionTitle" = "Upload abbrechen"; + +/* Label for post comments option. Tapping displays comments for a post. */ +"posts.comments.actionTitle" = "Kommentare"; + +/* Label for the delete post option. Tapping permanently deletes a post. */ +"posts.delete.actionTitle" = "Unwiderruflich löschen"; + +/* Label for an option that moves a post to the draft folder */ +"posts.draft.actionTitle" = "In Entwurf verschieben"; + +/* Label for post duplicate option. Tapping creates a copy of the post. */ +"posts.duplicate.actionTitle" = "Duplizieren"; + +/* Opens a submenu for page attributes. */ +"posts.pageAttributes.actionTitle" = "Seiten-Attribute"; + +/* Label for the preview post button. Tapping displays the post as it appears on the web. */ +"posts.preview.actionTitle" = "Vorschau"; + +/* Label for an option that moves a publishes a post immediately */ +"posts.publish.actionTitle" = "Jetzt veröffentlichen"; + +/* Retry uploading the post. */ +"posts.retry.actionTitle" = "Erneut versuchen"; + +/* Set the selected page as the homepage. */ +"posts.setHomepage.actionTitle" = "Als Homepage festlegen"; + +/* Set the parent page for the selected page. */ +"posts.setParent.actionTitle" = "Übergeordnete festlegen"; + +/* Set the selected page as a posts page. */ +"posts.setPostsPage.actionTitle" = "Als Beitragsseite festlegen"; + +/* Set the selected page as a regular page. */ +"posts.setRegularPage.actionTitle" = "Als normale Seite festlegen"; + +/* Label for post settings option. Tapping displays settings for a post. */ +"posts.settings.actionTitle" = "Einstellungen"; + +/* Share the post. */ +"posts.share.actionTitle" = "Teilen"; + +/* Label for post stats option. Tapping displays statistics for a post. */ +"posts.stats.actionTitle" = "Statistiken"; + +/* Label for a option that moves a post to the trash folder */ +"posts.trash.actionTitle" = "In den Papierkorb verschieben"; + +/* Label for the view post button. Tapping displays the post as it appears on the web. */ +"posts.view.actionTitle" = "Anzeigen"; + +/* A short message explaining that a page was deleted permanently. */ +"postsList.deletePage.message" = "Seite dauerhaft gelöscht"; + +/* A short message explaining that a post was deleted permanently. */ +"postsList.deletePost.message" = "Beitrag dauerhaft gelöscht"; + +/* A short message explaining that a page was moved to the trash bin. */ +"postsList.movePageToTrash.message" = "Seite in den Papierkorb verschoben"; + +/* A short message explaining that a post was moved to the trash bin. */ +"postsList.movePostToTrash.message" = "Beitrag in den Papierkorb verschoben"; + +/* Menu option for filtering posts by everyone */ +"postsList.postsByEveryone" = "Beiträge von allen"; + +/* Menu option for filtering posts by me */ +"postsList.postsByMe" = "Beiträge von mir"; + /* Title for the button to subscribe to Jetpack Social on the remaining shares view */ "postsettings.social.remainingshares.subscribe" = "Jetzt registrieren, um mehr zu teilen"; @@ -10801,10 +10896,20 @@ Tapping on this row allows the user to edit the sharing message. */ Note: Since the display space is limited, a short or concise translation is preferred. */ "reader.detail.toolbar.comment.button" = "Kommentar"; +/* Title for the Like button in the Reader Detail toolbar. +This is shown when the user has not liked the post yet. +Note: Since the display space is limited, a short or concise translation is preferred. */ +"reader.detail.toolbar.like.button" = "Mit einem „Like“ markieren"; + /* Accessibility hint for the Like button state. The button shows that the user has not liked the post, but tapping on this button will add a Like to the post. */ "reader.detail.toolbar.like.button.a11y.hint" = "Markiert diesen Beitrag mit einem „Like“."; +/* Title for the Like button in the Reader Detail toolbar. +This is shown when the user has already liked the post. +Note: Since the display space is limited, a short or concise translation is preferred. */ +"reader.detail.toolbar.liked.button" = "Mit einem „Like“ markiert"; + /* Accessibility hint for the Liked button state. The button shows that the user has liked the post, but tapping on this button will remove their like from the post. */ "reader.detail.toolbar.liked.button.a11y.hint" = "Entfernt das „Like“ für diesen Beitrag."; @@ -10840,10 +10945,18 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Accessibility hint for the comment button on the reader post card cell */ "reader.post.button.comment.accessibility.hint" = "Öffnet die Kommentare für den Beitrag."; -/* Accessibility hint for the like button on the reader post card cell - Accessibility hint for the liked button on the reader post card cell */ +/* Text for the 'Like' button on the reader post card cell. */ +"reader.post.button.like" = "Mit einem „Like“ markieren"; + +/* Accessibility hint for the like button on the reader post card cell */ "reader.post.button.like.accessibility.hint" = "Markiert den Beitrag mit einem „Like“."; +/* Text for the 'Liked' button on the reader post card cell. */ +"reader.post.button.liked" = "Mit einem „Like“ markiert"; + +/* Accessibility hint for the liked button on the reader post card cell */ +"reader.post.button.liked.accessibility.hint" = "Entfernt das „Like“ für den Beitrag."; + /* Accessibility hint for the site header on the reader post card cell */ "reader.post.button.menu.accessibility.hint" = "Öffnet ein Menü mit weiteren Aktionen."; @@ -10907,6 +11020,12 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Title for the tooltip anchor. */ "readerDetail.tooltipAnchorTitle.accessibilityLabel" = "Neu"; +/* The button title for the transfer footer view in Register Domain screen */ +"register.domain.transfer.button.title" = "Domain übertragen"; + +/* The title for the transfer footer view in Register Domain screen */ +"register.domain.transfer.title" = "Du möchtest eine Domain übertragen, die du bereits besitzt?"; + /* Information of what related post are and how they are presented */ "relatedPostsSettings.optionsFooter" = "Als „Ähnliche Beiträge“ werden relevante Inhalte von deiner Website unter deinen Beiträgen angezeigt."; @@ -11006,6 +11125,21 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Accessibility hint for actions when displaying media items. */ "siteMedia.cellAccessibilityHint" = "Wähle Medien aus."; +/* Accessibility hint for media item preview for user's viewing an item in their media library */ +"siteMediaItem.contentViewAccessibilityHint" = "Tippen, um Medien im Vollbildmodus anzuzeigen"; + +/* Accessibility label for media item preview for user's viewing an item in their media library */ +"siteMediaItem.contentViewAccessibilityLabel" = "Medienvorschau anzeigen"; + +/* Title for confirmation navigation bar button item */ +"siteMediaPicker.add" = "Hinzufügen"; + +/* Button selection media in media picker */ +"siteMediaPicker.deselect" = "Abwählen"; + +/* Button selection media in media picker */ +"siteMediaPicker.select" = "Auswählen"; + /* Media screen navigation title */ "siteMediaPicker.title" = "Medien"; @@ -11013,7 +11147,7 @@ Example: given a notice format "Following %@" and empty site name, this will be "siteSettings.privacy.title" = "Datenschutz"; /* Hint for users when hidden privacy setting is set */ -"siteVisibility.hidden.hint" = "Deine Website ist für alle sichtbar. Suchmaschinen werden aufgefordert, sie nicht zu indexieren."; +"siteVisibility.hidden.hint" = "Bis deine Website einsatzbereit ist, bleibt sie mit dem Hinweis „Demnächst verfügbar“ für Besucher verborgen."; /* Text for privacy settings: Hidden */ "siteVisibility.hidden.title" = "Verborgen"; @@ -11174,6 +11308,12 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Dismiss the AlertView */ "stockPhotos.strings.dismiss" = "Verwerfen"; +/* Subtitle for placeholder in Free Photos. The company name 'Pexels' should always be written as it is. */ +"stockPhotos.subtitle" = "Fotos bereitgestellt von Pexels"; + +/* Title for placeholder in Free Photos */ +"stockPhotos.title" = "Suche nach kostenlosen Fotos, die du deiner Mediathek hinzufügen kannst!"; + /* Section title for prominent suggestions */ "suggestions.section.prominent" = "In dieser Konversation"; @@ -11321,6 +11461,9 @@ Example: given a notice format "Following %@" and empty site name, this will be /* View title for Help & Support page. */ "support.title" = "Hilfe"; +/* Title for placeholder in Tenor picker */ +"tenor.welcomeMessage" = "Suche nach kostenlosen GIFs, die du deiner Mediathek hinzufügen kannst!"; + /* Header of delete screen section listing things that will be deleted. */ "these items will be deleted:" = "Diese Auswahl wird gelöscht:"; @@ -11336,9 +11479,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Displayed in the confirmation alert when marking unread notifications as read. */ "unread" = "ungelesen"; -/* Label displayed on video media items. */ -"video" = "Video"; - /* Portion of a message for Jetpack users that have multisite WP installation, thus Restore is not available. This part is a link, colored with a different color. */ "visit our documentation page" = "besuche unsere Dokumentationsseite"; @@ -11415,7 +11555,7 @@ Example: given a notice format "Following %@" and empty site name, this will be "widget.today.disabled.view.title" = "Die Statistiken wurden in die Jetpack-App verschoben. Der Wechsel ist kostenlos und dauert nur eine Minute."; /* Title of likes label in today widget */ -"widget.today.likes.label" = "Gefällt mir"; +"widget.today.likes.label" = "Likes"; /* Fallback title of the no data view in the stats widget */ "widget.today.nodata.view.fallbackTitle" = "Website-Statistiken konnten nicht geladen werden."; diff --git a/WordPress/Resources/en-AU.lproj/Localizable.strings b/WordPress/Resources/en-AU.lproj/Localizable.strings index 671c7b3f9f4e..e7ac13e96635 100644 --- a/WordPress/Resources/en-AU.lproj/Localizable.strings +++ b/WordPress/Resources/en-AU.lproj/Localizable.strings @@ -1,4 +1,4 @@ -/* Translation-Revision-Date: 2023-09-18 15:19:37+0000 */ +/* Translation-Revision-Date: 2024-01-04 02:58:06+0000 */ /* Plural-Forms: nplurals=2; plural=n != 1; */ /* Generator: GlotPress/4.0.0-alpha.11 */ /* Language: en_AU */ @@ -152,9 +152,6 @@ /* Difference label for Period Overview stat, indicating change from previous period. Ex: +99.9K (5%) */ "%@%@ (%@%%)" = "%1$@%2$@ (%3$@%%)"; -/* Accessibility label for a post in the post list. The parameters are the title, and date respectively. For example, \"Let it Go, 1 hour ago.\" */ -"%@, %@." = "%1$@, %2$@."; - /* Accessibility value for a Stats' Posting Activity Month if the user selected a day with posts. The first parameter is day (e.g. November 2019). The second parameter is the number of posts. */ "%@. %d posts." = "%1$@. %2$d posts."; @@ -210,9 +207,6 @@ /* Age between dates over one year. */ "%d years" = "%d years"; -/* Max image size in pixels (e.g. 300x300px) */ -"%dx%dpx" = "%1$dx%2$dpx"; - /* One menu area available in the theme */ "%i menu area in this theme" = "%i menu area in this theme"; @@ -250,12 +244,30 @@ translators: Block name. %s: The localized block name */ /* translators: accessibility text for blocks with invalid content. %d: localized block title */ "%s block. This block has invalid content" = "%s block. This block has invalid content"; +/* translators: %s: name of the synced block */ +"%s detached" = "%s detached"; + /* translators: %s: embed block variant's label e.g: \"Twitter\". */ "%s embed block previews are coming soon" = "%s embed block previews are coming soon"; +/* translators: %s: social link name e.g: \"Instagram\". */ +"%s has URL set" = "%s has URL set"; + +/* translators: %s: social link name e.g: \"Instagram\". */ +"%s has no URL set" = "%s has no URL set"; + +/* translators: %s: embed block variant's label e.g: \"Twitter\". */ +"%s link" = "%s link"; + /* translators: %s: embed block variant's label e.g: \"Twitter\". */ "%s previews not yet available" = "%s previews not yet available"; +/* translators: %s: social link name e.g: \"Instagram\". */ +"%s social icon" = "%s social icon"; + +/* translators: displayed right after the classic block is converted to blocks. %s: The localized classic block name */ +"'%s' block converted to blocks" = "'%s' block converted to blocks"; + /* translators: Missing block alert title. %s: The localized block name */ "'%s' is not fully-supported" = "'%s' is not fully supported"; @@ -406,6 +418,9 @@ translators: Block name. %s: The localized block name */ /* Label for selecting the Accelerated Mobile Pages (AMP) Blog Traffic Setting */ "Accelerated Mobile Pages (AMP)" = "Accelerated Mobile Pages (AMP)"; +/* No comment provided by engineer. */ +"Access this Paywall block on your web browser for advanced settings." = "Access this Paywall block on your web browser for advanced settings."; + /* Title for the account section in site settings screen */ "Account" = "Account"; @@ -460,13 +475,6 @@ translators: Block name. %s: The localized block name */ /* Label for the Activity Type filter button when there are more than 1 activity type selected */ "Activity Type (%1$d)" = "Activity Type (%1$d)"; -/* Accessibility label for add button to add items to the user's media library - Remove asset from media picker list */ -"Add" = "Add"; - -/* Action for Media Picker to indicate selection of media. The argument in the string represents the number of elements (as numeric digits) selected */ -"Add %@" = "Add %@"; - /* No comment provided by engineer. */ "Add Block After" = "Add Block After"; @@ -527,15 +535,33 @@ translators: Block name. %s: The localized block name */ /* Placeholder text. A call to action for the user to type any topic to which they would like to subscribe. */ "Add any topic" = "Add any topic"; +/* No comment provided by engineer. */ +"Add audio" = "Add audio"; + /* No comment provided by engineer. */ "Add blocks" = "Add blocks"; +/* No comment provided by engineer. */ +"Add button text" = "Add button text"; + +/* No comment provided by engineer. */ +"Add description" = "Add description"; + +/* No comment provided by engineer. */ +"Add image" = "Add image"; + +/* No comment provided by engineer. */ +"Add image or video" = "Add image or video"; + /* Accessibility hint text for adding an image to a new user account. */ "Add image, or avatar, to represent this new account." = "Adds image, or avatar, to represent this new account."; /* No comment provided by engineer. */ "Add link text" = "Add link text"; +/* translators: %s: social link name e.g: \"Instagram\". */ +"Add link to %s" = "Add link to %s"; + /* No comment provided by engineer. */ "Add menu item above" = "Add menu item above"; @@ -545,9 +571,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Add menu item to children" = "Add menu item to children"; -/* Accessibility hint for add button to add items to the user's media library */ -"Add new media" = "Add new media"; - /* Screen reader text for Menus button that adds a new menu to a site. */ "Add new menu" = "Add new menu"; @@ -585,6 +608,9 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Add title" = "Add title"; +/* No comment provided by engineer. */ +"Add video" = "Add video"; + /* User-facing string, presented to reflect that site assembly is underway. */ "Adding site features" = "Adding site features"; @@ -610,9 +636,6 @@ translators: Block name. %s: The localized block name */ /* Option to select the Airmail app when logging in with magic links */ "Airmail" = "Airmail"; -/* Description of albums in the photo libraries */ -"Albums" = "Albums"; - /* Image alignment option title. Title of the screen for choosing an image's alignment. */ "Alignment" = "Alignment"; @@ -626,6 +649,9 @@ translators: Block name. %s: The localized block name */ Title of the drafts filter. This filter shows a list of draft posts. */ "All" = "All"; +/* Information about redeeming domain credit on site dashboard. */ +"All WordPress.com annual plans include a custom domain name. Register your free domain now." = "All WordPress.com annual plans include a custom domain name. Register your free domain now."; + /* Footer of the free domain registration section for a paid plan. Information about redeeming domain credit on site dashboard. */ "All WordPress.com plans include a custom domain name. Register your free premium domain now." = "All WordPress.com plans include a custom domain name. Register your free premium domain now."; @@ -691,7 +717,13 @@ translators: Block name. %s: The localized block name */ "Alt Text" = "Alt Text"; /* No comment provided by engineer. */ -"Alternatively, you can detach and edit these blocks separately by tapping “Detach patterns”." = "Alternatively, you can detach and edit these blocks separately by tapping “Detach patterns”."; +"Alternatively, you can convert the content to blocks." = "Alternatively, you can convert the content to blocks."; + +/* No comment provided by engineer. */ +"Alternatively, you can detach and edit this block separately by tapping “Detach”." = "Alternatively, you can detach and edit this block separately by tapping “Detach”."; + +/* translators: Alternative option included in a warning related to having blocks deeply nested. */ +"Alternatively, you can flatten the content by ungrouping the block." = "Alternatively, you can flatten the content by ungrouping the block."; /* Instruction text to explain to help users type their password instead of using magic link login option. */ "Alternatively, you may enter the password for this account." = "Alternatively, you may enter the password for this account."; @@ -840,15 +872,9 @@ translators: Block name. %s: The localized block name */ /* Message prompting the user to confirm that they want to disconnect Jetpack from the site. */ "Are you sure you want to disconnect Jetpack from the site?" = "Are you sure you want to disconnect Jetpack from the site?"; -/* Message prompting the user to confirm that they want to permanently delete a group of media items. */ -"Are you sure you want to permanently delete these items?" = "Are you sure you want to permanently delete these items?"; - /* Message prompting the user to confirm that they want to permanently delete a media item. Should match Calypso. */ "Are you sure you want to permanently delete this item?" = "Are you sure you want to permanently delete this item?"; -/* Message of the confirmation alert when deleting a page from the trash. */ -"Are you sure you want to permanently delete this page?" = "Are you sure you want to permanently delete this page?"; - /* Message of the confirmation alert when deleting a post from the trash. */ "Are you sure you want to permanently delete this post?" = "Are you sure you want to permanently delete this post?"; @@ -880,9 +906,6 @@ translators: Block name. %s: The localized block name */ /* Title of message shown when user taps submit for review. */ "Are you sure you want to submit for review?" = "Are you sure you want to submit for review?"; -/* Message of the trash page confirmation alert. */ -"Are you sure you want to trash this page?" = "Are you sure you want to bin this page?"; - /* Message of the trash confirmation alert. */ "Are you sure you want to trash this post?" = "Are you sure you want to trash this post?"; @@ -923,9 +946,6 @@ translators: Block name. %s: The localized block name */ /* translators: accessibility text. Empty Audio caption. */ "Audio caption. Empty" = "Audio caption. Empty"; -/* Accessibility label for audio items in the media collection view. The parameter is the creation date of the audio. */ -"Audio, %@" = "Audio, %@"; - /* No comment provided by engineer. */ "Authenticating" = "Authenticating"; @@ -1073,10 +1093,16 @@ translators: Block name. %s: The localized block name */ /* Notice that a page without content has been created */ "Blank page created" = "Blank page created"; +/* Noun. Links to a blog's Blaze screen. */ +"Blaze" = "Blaze"; + /* Accessibility label for block quote button on formatting toolbar. Discoverability title for block quote keyboard shortcut. */ "Block Quote" = "Block Quote"; +/* No comment provided by engineer. */ +"Block cannot be rendered because it is deeply nested. Tap here for more details." = "Block cannot be rendered because it is deeply nested. Tap here for more details."; + /* translators: displayed right after the block is copied. */ "Block copied" = "Block copied"; @@ -1089,6 +1115,9 @@ translators: Block name. %s: The localized block name */ /* Popup title about why this post is being opened in block editor */ "Block editor enabled" = "Block editor enabled"; +/* translators: displayed right after the block is grouped */ +"Block grouped" = "Block grouped"; + /* Jetpack Settings: Block malicious login attempts */ "Block malicious login attempts" = "Block malicious login attempts"; @@ -1104,9 +1133,15 @@ translators: Block name. %s: The localized block name */ /* The title of a button that triggers blocking a site from the user's reader. */ "Block this site" = "Block this site"; +/* translators: displayed right after the block is ungrouped. */ +"Block ungrouped" = "Block ungrouped"; + /* Notice title when blocking a site succeeds. */ "Blocked site" = "Blocked site"; +/* Notice title when blocking a user succeeds. */ +"Blocked user" = "Blocked user"; + /* Blocklist Title Settings: Comments Blocklist */ "Blocklist" = "Blocklist"; @@ -1117,6 +1152,12 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Blocks are pieces of content that you can insert, rearrange, and style without needing to know how to code. Blocks are an easy and modern way for you to create beautiful layouts." = "Blocks are pieces of content that you can insert, rearrange, and style without needing to know how to code. Blocks are an easy and modern way for you to create beautiful layouts."; +/* No comment provided by engineer. */ +"Blocks menu" = "Blocks menu"; + +/* translators: Warning related to having blocks deeply nested. %d: The deepest nesting level. */ +"Blocks nested deeper than %d levels may not render properly in the mobile editor." = "Blocks nested deeper than %d levels may not render properly in the mobile editor."; + /* Title of a button that displays the WordPress.com blog */ "Blog" = "Blog"; @@ -1169,6 +1210,9 @@ translators: Block name. %s: The localized block name */ /* Jetpack Settings: Brute Force Attack Protection Section */ "Brute Force Attack Protection" = "Brute Force Attack Protection"; +/* No comment provided by engineer. */ +"Build layouts" = "Build layouts"; + /* Discoverability title for bullet list keyboard shortcut. */ "Bullet List" = "Bullet List"; @@ -1184,12 +1228,12 @@ translators: Block name. %s: The localized block name */ /* Title for a list of different button styles. */ "Button Style" = "Button Style"; +/* No comment provided by engineer. */ +"Button position" = "Button position"; + /* Label for the post author in the post detail. */ "By " = "By "; -/* Accessibility label for the post author in the post list. The parameter is the author name. For example, \"By Elsa.\" */ -"By %@." = "By %@."; - /* Legal disclaimer for logging in. The underscores _..._ denote underline. */ "By continuing, you agree to our _Terms of Service_." = "By continuing, you agree to our _Terms of Service_."; @@ -1209,8 +1253,7 @@ translators: Block name. %s: The localized block name */ /* Label for size of media while it's being calculated. */ "Calculating..." = "Calculating..."; -/* Accessibility label for taking an image or video with the camera on formatting toolbar. - Accessibility label for the camera tile in the collection view */ +/* Accessibility label for taking an image or video with the camera on formatting toolbar. */ "Camera" = "Camera"; /* Title of an alert informing the user the camera permission for the app is disabled and its needed to proceed */ @@ -1261,10 +1304,7 @@ translators: Block name. %s: The localized block name */ Cancels an Action Cancels an alert. Cancels the mark all as read action. - Dismiss the post action sheet Editing GIF alert cancel action button. - Label for a cancel button - Label for the auto-upload cancelation button in the post list. Tapping will prevent the app from auto-uploading the post. Menus cancel button for deleting a menu. Menus cancel button within text bar while editing items. Menus: Cancel button title for canceling an edited menu item. @@ -1281,10 +1321,6 @@ translators: Block name. %s: The localized block name */ Verb. Title for Jetpack Restore cancel button. */ "Cancel" = "Cancel"; -/* Label for the Post List option that cancels automatic uploading of a post. - Media Library option to cancel an in-progress or failed upload. */ -"Cancel Upload" = "Cancel Upload"; - /* Menus alert button title to cancel discarding changes and not createa a new menu. Menus alert button title to cancel discarding changes and not select a new menu Menus alert button title to cancel discarding changes and not select a new menu location */ @@ -1293,6 +1329,9 @@ translators: Block name. %s: The localized block name */ /* Dialog box title for when the user is canceling an upload. */ "Cancel media uploads" = "Cancel media uploads"; +/* No comment provided by engineer. */ +"Cancel search" = "Cancel search"; + /* Share extension dialog dismiss button label - displayed when user is missing a login token. */ "Cancel sharing" = "Cancel sharing"; @@ -1340,9 +1379,6 @@ translators: Block name. %s: The localized block name */ Main title */ "Change Password" = "Change Password"; -/* Title of button that takes user to the system Settings section for the app */ -"Change Settings" = "Change Settings"; - /* Change Username title. */ "Change Username" = "Change Username"; @@ -1436,6 +1472,9 @@ translators: Block name. %s: The localized block name */ Title for the button to progress with the selected site homepage design. */ "Choose" = "Choose"; +/* Title for selecting a new homepage */ +"Choose Homepage" = "Choose Homepage"; + /* Title for settings section which allows user to select their home page and posts page */ "Choose Pages" = "Choose Pages"; @@ -1451,6 +1490,9 @@ translators: Block name. %s: The localized block name */ /* Select domain name. Title */ "Choose a domain" = "Choose a domain"; +/* No comment provided by engineer. */ +"Choose a file" = "Choose a file"; + /* OK Button title shown in alert informing users about the Reader Save for Later feature. */ "Choose a new app icon" = "Choose a new app icon"; @@ -1476,9 +1518,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Choose file" = "Choose file"; -/* Menu option for selecting media from the device's photo library. */ -"Choose from My Device" = "Choose from My Device"; - /* Explanatory text for Homepage Settings homepage type selection. */ "Choose from a homepage that displays your latest posts (classic blog) or a fixed \/ static page." = "Choose from a home page that displays your latest posts (classic blog) or a fixed \/ static page."; @@ -1533,6 +1572,9 @@ translators: Block name. %s: The localized block name */ /* Message of the trash confirmation alert. */ "Clear all old activity logs?" = "Clear all old activity logs?"; +/* No comment provided by engineer. */ +"Clear search" = "Clear search"; + /* Title of a button. */ "Clear search history" = "Clear search history"; @@ -1585,6 +1627,12 @@ translators: Block name. %s: The localized block name */ /* Title displayed for selection of custom app icons that have colorful backgrounds. */ "Colorful backgrounds" = "Colourful backgrounds"; +/* translators: %d: column index. */ +"Column %d" = "Column %d"; + +/* No comment provided by engineer. */ +"Column Settings" = "Column Settings"; + /* No comment provided by engineer. */ "Columns Settings" = "Columns Settings"; @@ -1670,9 +1718,6 @@ translators: Block name. %s: The localized block name */ /* Community & Non-Profit site intent topic */ "Community & Non-Profit" = "Community & Non-Profit"; -/* Accessibility indication that the current Post List style is currently Compact. */ -"Compact" = "Compact"; - /* The action is completed */ "Completed" = "Completed"; @@ -1858,10 +1903,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Copied block" = "Copied block"; -/* Copy the post url and paste anywhere in phone - Label for page copy link. Tapping copy the url of page */ -"Copy Link" = "Copy Link"; - /* Copy link to oomment button title */ "Copy Link to Comment" = "Copy Link to Comment"; @@ -1970,9 +2011,6 @@ translators: Block name. %s: The localized block name */ /* Error title displayed when unable to close user account. */ "Couldn’t close account automatically" = "Couldn’t close account automatically"; -/* Message to show while media data source is finding the number of items available. */ -"Counting media items..." = "Counting media items..."; - /* Period Stats 'Countries' header */ "Countries" = "Countries"; @@ -2058,6 +2096,12 @@ translators: Block name. %s: The localized block name */ /* Button title for download backup action */ "Create downloadable file" = "Create downloadable file"; +/* No comment provided by engineer. */ +"Create embed" = "Create embed"; + +/* No comment provided by engineer. */ +"Create link" = "Create link"; + /* Title of a Quick Start Tour */ "Create your site" = "Create your site"; @@ -2076,6 +2120,9 @@ translators: Block name. %s: The localized block name */ /* Description of the cell displaying status of a backup in progress */ "Creating downloadable backup" = "Creating downloadable backup"; +/* No comment provided by engineer. */ +"Crosspost" = "Crosspost"; + /* No comment provided by engineer. */ "Current" = "Current"; @@ -2094,6 +2141,9 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Current placeholder text is" = "Current placeholder text is"; +/* translators: accessibility text. Inform about current unit value. %s: Current unit value. */ +"Current unit is %s" = "Current unit is %s"; + /* translators: %s: current cell value. */ "Current value is %s" = "Current value is %s"; @@ -2109,6 +2159,9 @@ translators: Block name. %s: The localized block name */ /* Description of the current entry being restored. %1$@ is a placeholder for the specific entry being restored. */ "Currently restoring: %1$@" = "Currently restoring: %1$@"; +/* No comment provided by engineer. */ +"Custom URL" = "Custom URL"; + /* Placeholder for Invite People message field. */ "Custom message…" = "Custom message…"; @@ -2181,6 +2234,9 @@ translators: Block name. %s: The localized block name */ /* Only December needs to be translated */ "December 17, 2017" = "December 17, 2017"; +/* No comment provided by engineer. */ +"Deeply nested block" = "Deeply nested block"; + /* Description of the default paragraph formatting style in the editor. Placeholder text displayed in the share extension's summary view. It lets the user know the default category will be used on their post. */ "Default" = "Default"; @@ -2205,7 +2261,6 @@ translators: Block name. %s: The localized block name */ /* Delete Delete button title for the warning shown to the user when he refuses to re-login when the authToken is missing. Title for button that permanently deletes a media item (photo / video) - Title for button that permanently deletes one or more media items (photos / videos) Title of a delete button Title of the trash confirmation alert. */ "Delete" = "Delete"; @@ -2213,15 +2268,11 @@ translators: Block name. %s: The localized block name */ /* Menus confirmation button for deleting a menu. */ "Delete Menu" = "Delete Menu"; -/* Delete option in the confirmation alert when deleting a page from the trash. - Delete option in the confirmation alert when deleting a post from the trash. - Label for a button permanently deletes a page. - Label for the delete post option. Tapping permanently deletes a post. +/* Delete option in the confirmation alert when deleting a post from the trash. Title for button on the comment details page that deletes the comment when tapped. */ "Delete Permanently" = "Delete Permanently"; -/* Title of the confirmation alert when deleting a page from the trash. - Title of the confirmation alert when deleting a post from the trash. */ +/* Title of the confirmation alert when deleting a post from the trash. */ "Delete Permanently?" = "Delete Permanently?"; /* Button label for deleting the current site @@ -2250,6 +2301,9 @@ translators: Block name. %s: The localized block name */ /* Verb. Denies a 2fa authentication challenge. */ "Deny" = "Deny"; +/* No comment provided by engineer. */ +"Describe the purpose of the image. Leave empty if decorative." = "Describe the purpose of the image. Leave empty if decorative."; + /* Label for the description for a media asset (image / video) Section header for tag name in Tag Details View. Title of section that contains plugins' description */ @@ -2258,6 +2312,9 @@ translators: Block name. %s: The localized block name */ /* Shortened version of the main title to be used in back navigation. */ "Design" = "Design"; +/* Navigates to design system gallery only available in development builds */ +"Design System" = "Design System"; + /* Title for the desktop web preview */ "Desktop" = "Desktop"; @@ -2334,7 +2391,6 @@ translators: Block name. %s: The localized block name */ /* Accessibility label for button to dismiss a bottom sheet Accessibility label for the transparent space above the login dialog which acts as a button to dismiss the dialog. Accessibility label for the transparent space above the signup dialog which acts as a button to dismiss the dialog. - Action to show on alert when view asset fails. Dismiss a view. Verb */ "Dismiss" = "Dismiss"; @@ -2352,12 +2408,6 @@ translators: Block name. %s: The localized block name */ User's Display Name */ "Display Name" = "Display Name"; -/* Accessibility label for other media items in the media collection view. The parameter is the creation date of the document. */ -"Document, %@" = "Document, %@"; - -/* Accessibility label for other media items in the media collection view. The parameter is the filename file. */ -"Document: %@" = "Document: %@"; - /* Message shown when all tours have been completed */ "Doesn't it feel good to cross things off a list?" = "Doesn't it feel good to cross things off a list?"; @@ -2518,8 +2568,7 @@ translators: Block name. %s: The localized block name */ /* Description of a Quick Start Tour */ "Draft and publish a post." = "Draft and publish a post."; -/* Title of the drafts filter. This filter shows a list of draft posts. - Title of the drafts header in search list. */ +/* Title of the drafts filter. This filter shows a list of draft posts. */ "Drafts" = "Drafts"; /* No comment provided by engineer. */ @@ -2531,26 +2580,21 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Drag to adjust focal point" = "Drag to adjust focal point"; -/* Label for page duplicate option. Tapping creates a copy of the page. - Label for post duplicate option. Tapping creates a copy of the post. */ -"Duplicate" = "Duplicate"; - /* No comment provided by engineer. */ "Duplicate block" = "Duplicate block"; +/* No comment provided by engineer. */ +"Dynamic" = "Dynamic"; + /* Placeholder text for the search field int the Site Intent screen. */ "E.g. Fashion, Poetry, Politics" = "Eg. Fashion, Poetry, Politics"; /* No comment provided by engineer. */ "Each block has its own settings. To find them, tap on a block. Its settings will appear on the toolbar at the bottom of the screen." = "Each block has its own settings. To find them, tap on a block. Its settings will appear on the toolbar at the bottom of the screen."; -/* Accessibility label for edit button to enable multi selection mode in the user's media library - Edit the post. - Editing GIF alert default action button. +/* Editing GIF alert default action button. Edits a Comment Edits the comment - Label for a button that opens the Edit Page view controller - Label for the edit post button. Tapping displays the editor. User action to edit media details. Verb, edit a comment */ "Edit" = "Edit"; @@ -2558,9 +2602,6 @@ translators: Block name. %s: The localized block name */ /* Title for the edit more button section */ "Edit \"More\" button" = "Edit \"More\" button"; -/* Button that displays the media editor to the user */ -"Edit %@" = "Edit %@"; - /* Blocklist Keyword Edition Title */ "Edit Blocklist Word" = "Edit Blocklist Word"; @@ -2587,6 +2628,9 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Edit file" = "Edit file"; +/* No comment provided by engineer. */ +"Edit focal point" = "Edit focal point"; + /* No comment provided by engineer. */ "Edit media" = "Edit media"; @@ -2605,6 +2649,12 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Edit video" = "Edit video"; +/* translators: %s: name of the host app (e.g. WordPress) */ +"Editing synced patterns is not yet supported on %s for Android" = "Editing synced patterns is not yet supported on %s for Android"; + +/* translators: %s: name of the host app (e.g. WordPress) */ +"Editing synced patterns is not yet supported on %s for iOS" = "Editing synced patterns is not yet supported on %s for iOS"; + /* Editing GIF alert message. */ "Editing this GIF will remove its animation." = "Editing this GIF will remove its animation."; @@ -2666,6 +2716,12 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Embed block previews are coming soon" = "Embed block previews are coming soon"; +/* translators: accessibility text. %s: Embed caption. */ +"Embed caption. %s" = "Embed caption. %s"; + +/* translators: accessibility text. Empty Embed caption. */ +"Embed caption. Empty" = "Embed caption. Empty"; + /* No comment provided by engineer. */ "Embed media" = "Embed media"; @@ -2738,9 +2794,6 @@ translators: Block name. %s: The localized block name */ /* Secondary message shown when there are no domains that match the user entered text. */ "Enter different words above and we'll look for an address that matches it." = "Enter different words above and we'll look for an address that matches it."; -/* Accessibility hint for edit button to enable multi selection mode in the user's media library */ -"Enter edit mode to enable multi select to delete" = "Enter edit mode to enable multi select to delete"; - /* (placeholder) Help enter WordPress password Placeholder of a field to type a password to protect the post. */ "Enter password" = "Enter password"; @@ -2896,9 +2949,6 @@ translators: Block name. %s: The localized block name */ /* Short title telling the user they will receive a blogging reminder every day of the week. */ "Every day at %@" = "Every day at %@"; -/* Label for the post author filter. This filter shows posts for all users on the blog. */ -"Everyone" = "Everyone"; - /* Example story title description */ "Example story title" = "Example story title"; @@ -2908,9 +2958,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Excerpt length (words)" = "Excerpt length (words)"; -/* Accessibility label for a post's excerpt in the post list. The parameter is the post excerpt. For example, \"Excerpt. This is the first paragraph.\" */ -"Excerpt. %@." = "Excerpt. %@."; - /* Should be the same as the text displayed if the user clicks the (i) in Calypso. */ "Excerpts are optional hand-crafted summaries of your content." = "Excerpts are optional hand-crafted summaries of your content."; @@ -2920,8 +2967,7 @@ translators: Block name. %s: The localized block name */ /* Accessibility Label for the exit full screen button on the full screen comment reply mode */ "Exit Full Screen" = "Exit Full Screen"; -/* Accessibility indication that the current Post List style is currently Expanded. - Screen reader text to represent the expanded state of a UI control */ +/* Screen reader text to represent the expanded state of a UI control */ "Expanded" = "Expanded"; /* Accessibility hint */ @@ -2971,9 +3017,6 @@ translators: Block name. %s: The localized block name */ The action failed */ "Failed" = "Failed"; -/* Error title when picked media cannot be imported into stories. */ -"Failed Media Export" = "Failed Media Export"; - /* Message for mark all as read success notice */ "Failed marking Notifications as read" = "Failed marking Notifications as read"; @@ -3175,6 +3218,12 @@ translators: Block name. %s: The localized block name */ /* An example tag used in the login prologue screens. */ "Football" = "Football"; +/* translators: Recommendation included in a warning related to having blocks deeply nested. */ +"For this reason, we recommend editing the block using the web editor." = "For this reason, we recommend editing the block using the web editor."; + +/* translators: Recommendation included in a warning related to having blocks deeply nested. */ +"For this reason, we recommend editing the block using your web browser." = "For this reason, we recommend editing the block using your web browser."; + /* Register Domain - Domain contact information section header description */ "For your convenience, we have pre-filled your WordPress.com contact information. Please review to be sure it’s the correct information you want to use for this domain." = "For your convenience, we have pre-filled your WordPress.com contact information. Please review to be sure it’s the correct information you want to use for this domain."; @@ -3208,6 +3257,9 @@ translators: Block name. %s: The localized block name */ /* Button title displayed in popup indicating date of change on another device */ "From another device" = "From another device"; +/* No comment provided by engineer. */ +"From clipboard" = "From clipboard"; + /* Button title displayed in popup indicating date of change on device */ "From this device" = "From this device"; @@ -3465,6 +3517,9 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Hide keyboard" = "Hide keyboard"; +/* No comment provided by engineer. */ +"Hide search heading" = "Hide search heading"; + /* Displays the History screen from the editor's alert sheet Title of a navigation button that opens the scan history view Title of the post history screen */ @@ -3486,8 +3541,7 @@ translators: Block name. %s: The localized block name */ "Home" = "Home"; /* Title for setting which shows the current page assigned as a site's homepage - Title for the homepage section in site settings screen - Title of the Homepage Badge */ + Title for the homepage section in site settings screen */ "Homepage" = "Home page"; /* Label for Homepage Settings site settings section @@ -3506,6 +3560,12 @@ translators: Block name. %s: The localized block name */ /* How to create story description */ "How to create a story post" = "How to create a story post"; +/* No comment provided by engineer. */ +"How to edit your page" = "How to edit your page"; + +/* No comment provided by engineer. */ +"How to edit your post" = "How to edit your post"; + /* Title for the fix section in Threat Details */ "How will we fix it?" = "How will we fix it?"; @@ -3578,9 +3638,6 @@ translators: Block name. %s: The localized block name */ /* Hint for image title on image settings. */ "Image title" = "Image title"; -/* Accessibility label for image thumbnails in the media collection view. The parameter is the creation date of the image. */ -"Image, %@" = "Image, %@"; - /* Undated post time label */ "Immediately" = "Immediately"; @@ -3608,6 +3665,9 @@ translators: Block name. %s: The localized block name */ /* The plugin is not active on the site and has enabled automatic updates */ "Inactive, Autoupdates on" = "Inactive, Autoupdates on"; +/* Title of the switch to turn on or off the blogging prompts feature. */ +"Include a Blogging Prompt" = "Include a Blogging Prompt"; + /* Describes a standard *.wordpress.com site domain */ "Included with Site" = "Included with Site"; @@ -3637,9 +3697,18 @@ translators: Block name. %s: The localized block name */ Button title used in media picker to insert media (photos / videos) into a post. Placeholder will be the number of items that will be inserted. */ "Insert %@" = "Insert %@"; +/* No comment provided by engineer. */ +"Insert Audio Block" = "Insert Audio Block"; + +/* No comment provided by engineer. */ +"Insert Gallery Block" = "Insert Gallery Block"; + /* Accessibility label for insert horizontal ruler button on formatting toolbar. */ "Insert Horizontal Ruler" = "Insert Horizontal Ruler"; +/* No comment provided by engineer. */ +"Insert Image Block" = "Insert Image Block"; + /* Accessibility label for insert link button on formatting toolbar. Discoverability title for insert link keyboard shortcut. Label action for inserting a link on the editor */ @@ -3648,6 +3717,12 @@ translators: Block name. %s: The localized block name */ /* Discoverability title for insert media keyboard shortcut. */ "Insert Media" = "Insert Media"; +/* No comment provided by engineer. */ +"Insert Video Block" = "Insert Video Block"; + +/* No comment provided by engineer. */ +"Insert crosspost" = "Insert crosspost"; + /* Accessibility label for insert media button on formatting toolbar. */ "Insert media" = "Insert media"; @@ -3657,6 +3732,9 @@ translators: Block name. %s: The localized block name */ /* Default accessibility label for the media picker insert button. */ "Insert selected" = "Insert selected"; +/* No comment provided by engineer. */ +"Inside" = "Inside"; + /* Title of Insights stats filter. */ "Insights" = "Insights"; @@ -3696,6 +3774,9 @@ translators: Block name. %s: The localized block name */ /* Interior Design site intent topic */ "Interior Design" = "Interior Design"; +/* Title displayed on the feature introduction view. */ +"Introducing Blogging Prompts" = "Introducing Blogging Prompts"; + /* Stories intro header title */ "Introducing Story Posts" = "Introducing Story Posts"; @@ -4016,6 +4097,9 @@ translators: Block name. %s: The localized block name */ /* Link name field placeholder */ "Link Name" = "Link Name"; +/* No comment provided by engineer. */ +"Link Rel" = "Link Rel"; + /* Noun. Title for screen in editor that allows to configure link options */ "Link Settings" = "Link Settings"; @@ -4039,33 +4123,27 @@ translators: Block name. %s: The localized block name */ Settings: Comments Approval settings */ "Links in comments" = "Links in comments"; -/* The accessibility label for the list style button in the Post List. */ -"List style" = "List style"; - /* Title of the screen that load selected the revisions. */ "Load" = "Load"; /* A short label. A call to action to load more posts. */ "Load more posts" = "Load more posts"; +/* No comment provided by engineer. */ +"Loading" = "Loading"; + /* Text displayed while loading the activity feed for a site */ "Loading Activities..." = "Loading Activities..."; /* Text displayed while loading the activity feed for a site */ "Loading Backups..." = "Loading Backups..."; -/* Phrase to show when the user has searched for GIFs and they are being loaded. */ -"Loading GIFs..." = "Loading GIFs..."; - /* Menus label text displayed while menus are loading */ "Loading Menus..." = "Loading Menus..."; /* Text displayed while loading site People. */ "Loading People..." = "Loading People..."; -/* Phrase to show when the user has searched for images and they are being loaded. */ -"Loading Photos..." = "Loading Photos..."; - /* Text displayed while loading plans details */ "Loading Plan..." = "Loading Plan..."; @@ -4126,8 +4204,7 @@ translators: Block name. %s: The localized block name */ /* Local Services site intent topic */ "Local Services" = "Local Services"; -/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. - Title of the Local Changes Badge */ +/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. */ "Local changes" = "Local changes"; /* Title for alert when a generic error happened when trying to find the location of the device */ @@ -4230,6 +4307,9 @@ translators: Block name. %s: The localized block name */ /* Return to blog screen action when theme activation succeeds */ "Manage site" = "Manage site"; +/* No comment provided by engineer. */ +"Manual" = "Manual"; + /* Section name for manual offsets in time zone selector */ "Manual Offsets" = "Manual Offsets"; @@ -4288,7 +4368,6 @@ translators: Block name. %s: The localized block name */ "Max Video Upload Size" = "Max Video Upload Size"; /* Accessibility label for the Me button in My Site. - Label for the post author filter. This filter shows posts only authored by the current user. Me page title Noun. Title. Links to the Me screen. The accessibility value of the me tab. @@ -4296,9 +4375,7 @@ translators: Block name. %s: The localized block name */ "Me" = "Me"; /* Noun. Title. Links to the blog's Media library. - Tab bar title for the Media tab in Media Picker The menu item to select during a guided tour. - Title for Media Library section of the app. Title for the media section in site settings screen Title label for the media settings section in the app settings */ "Media" = "Media"; @@ -4310,13 +4387,6 @@ translators: Block name. %s: The localized block name */ /* Label for size of media cache in the app. */ "Media Cache Size" = "Media Cache Size"; -/* Title for alert when access to media capture is not granted */ -"Media Capture" = "Media Capture"; - -/* Title for alert when a generic error happened when loading media - Title for alert when access to the media library is not granted by the user */ -"Media Library" = "Media Library"; - /* Title for action sheet with media options. */ "Media Options" = "Media Options"; @@ -4336,8 +4406,8 @@ translators: Block name. %s: The localized block name */ /* Error message to show to users when trying to upload a media object with file size is larger than the max file size allowed in the site */ "Media filesize (%@) is too large to upload. Maximum allowed is %@" = "Media filesize (%1$@) is too large to upload. Maximum allowed is %2$@"; -/* Alert title when there is issues loading an asset to preview. */ -"Media preview failed." = "Media preview failed."; +/* translators: %s: block title e.g: \"Paragraph\". */ +"Media options" = "Media options"; /* Alert displayed to the user when multiple media items have uploaded successfully. */ "Media uploaded (%ld files)" = "Media uploaded (%ld files)"; @@ -4348,6 +4418,9 @@ translators: Block name. %s: The localized block name */ /* Medium image size. Should be the same as in core WP. */ "Medium" = "Medium"; +/* No comment provided by engineer. */ +"Mention" = "Mention"; + /* The default text used for filling the name of a menu when creating it. Title for the site menu view on the My Site screen */ "Menu" = "Menu"; @@ -4373,9 +4446,6 @@ translators: Block name. %s: The localized block name */ Label for the share message field on the post settings. */ "Message" = "Message"; -/* Title of section containing image / video metadata such as size and file type */ -"Metadata" = "Metadata"; - /* Option to select the Microsft Outlook app when logging in with magic links */ "Microsoft Outlook" = "Microsoft Outlook"; @@ -4395,13 +4465,11 @@ translators: Block name. %s: The localized block name */ "Months and Years" = "Months and Years"; /* Accessibility label for more button in dashboard quick start card. - Accessibility label for the More button in Page List. Accessibility label for the More button in Post List (compact view). Accessibility label for the More button on formatting toolbar. Accessibility label for the More button on Reader Cell Accessibility label for the More button on Reader's post details - Action button to display more available options - Label for the more post button. Tapping displays an action sheet with post options. */ + Action button to display more available options */ "More" = "More"; /* Action button to display more available options @@ -4411,6 +4479,9 @@ translators: Block name. %s: The localized block name */ /* Label for the posting activity legend. */ "More Posts" = "More Posts"; +/* No comment provided by engineer. */ +"More support options" = "More support options"; + /* Insights 'Most Popular Time' header */ "Most Popular Time" = "Most Popular Time"; @@ -4447,21 +4518,17 @@ translators: Block name. %s: The localized block name */ /* translators: accessibility text. %1: current block position (number). %2: next block position (number) */ "Move block up from row %1$s to row %2$s" = "Move block up from row %1$s to row %2$s"; +/* No comment provided by engineer. */ +"Move blocks" = "Move blocks"; + /* Option to move Insight down in the view. */ "Move down" = "Move down"; /* Screen reader text for button that will move the menu item */ "Move menu item" = "Move menu item"; -/* Label for a button that moves a page to the draft folder - Label for an option that moves a post to the draft folder */ -"Move to Draft" = "Move to Draft"; - -/* Label for a button that moves a page to the trash folder - Label for a option that moves a post to the trash folder - Title for button on the comment details page that moves the comment to trash when tapped. +/* Title for button on the comment details page that moves the comment to trash when tapped. Trash option in the trash confirmation alert. - Trash option in the trash page confirmation alert. Trashes the comment */ "Move to Trash" = "Move to Trash"; @@ -4493,7 +4560,8 @@ translators: Block name. %s: The localized block name */ Title of My Site tab */ "My Site" = "My Site"; -/* Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ +/* Title for site picker screen. + Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ "My Sites" = "My Sites"; /* Siri Suggestion to open My Sites */ @@ -4528,6 +4596,10 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Navigates to layout selection screen" = "Navigates to layout selection screen"; +/* translators: %s: Select control button label e.g. Small +translators: %s: Select control button label e.g. \"Button width\" */ +"Navigates to select %s" = "Navigates to select %s"; + /* No comment provided by engineer. */ "Navigates to the previous content sheet" = "Navigates to the previous content sheet"; @@ -4695,6 +4767,9 @@ translators: Block name. %s: The localized block name */ /* Accessibility label for no currently selected range. */ "No date range selected" = "No date range selected"; +/* No comment provided by engineer. */ +"No description" = "No description"; + /* Title for the view when there aren't any fixed threats to display */ "No fixed threats" = "No fixed threats"; @@ -4736,9 +4811,7 @@ translators: Block name. %s: The localized block name */ /* Title for the view when there aren't any Activities to display in the Activity Log for a given filter. */ "No matching events found." = "No matching events found."; -/* Message displayed when no results are returned from a media library search. Should match Calypso. - Phrase to show when the user search for images but there are no result to show. - Phrase to show when the user searches for GIFs but there are no result to show. */ +/* Message displayed when no results are returned from a media library search. Should match Calypso. */ "No media matching your search" = "No media matching your search"; /* Menus text shown when no menus were available for loading the Menus editor. */ @@ -4756,8 +4829,7 @@ translators: Block name. %s: The localized block name */ /* Displayed in the Notifications Tab as a title, when there are no notifications */ "No notifications yet" = "No notifications yet"; -/* Displayed when the user is searching the pages list and there are no matching pages - Text displayed when there's no matching with the text search */ +/* Text displayed when there's no matching with the text search */ "No pages matching your search" = "No pages matching your search"; /* Text displayed when search for plugins returns no results */ @@ -4778,12 +4850,12 @@ translators: Block name. %s: The localized block name */ /* Message shown whent the reader finds no posts for the chosen tag */ "No posts have been made recently with this tag." = "No posts have been made recently with this tag."; -/* Displayed when the user is searching the posts list and there are no matching posts */ -"No posts matching your search" = "No posts matching your search"; - /* Accessibility value for a Stats' Posting Activity Month if there are no posts. */ "No posts." = "No posts."; +/* No comment provided by engineer. */ +"No preview available" = "No preview available"; + /* String to display in place of the site address, in case it was not retrieved from the backend. */ "No primary site address found" = "No primary site address found"; @@ -4818,6 +4890,9 @@ translators: Block name. %s: The localized block name */ /* Message for a notice informing the user their scan completed and no threats were found */ "No threats found" = "No threats found"; +/* No comment provided by engineer. */ +"No title" = "No title"; + /* Disabled No alignment for an image (default). Should be the same as in core WP. No comment will be autoapproved @@ -4875,9 +4950,6 @@ translators: Block name. %s: The localized block name */ /* A message title */ "Nothing liked yet" = "Nothing liked yet"; -/* Default message for empty media picker */ -"Nothing to show" = "Nothing to show"; - /* Notifications Details Accessibility Identifier */ "Notification Details Table" = "Notification Details Table"; @@ -4937,7 +5009,6 @@ translators: Block name. %s: The localized block name */ Alert dismissal title Button title. Acknowledges a prompt. Button title. An acknowledgement of the message displayed in a prompt. - Confirmation of action Default action Dismisses the alert Marks all notifications as read. @@ -4999,9 +5070,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Only show excerpt" = "Only show excerpt"; -/* Message telling the user that they've only enabled limited photo library permissions for the app. */ -"Only the selected photos you've given access to are available." = "Only the selected photos you've given access to are available."; - /* An informal exclaimation that means `something went wrong`. Title for the view when there's an error loading a comment. Title for the view when there's an error loading Activity Log @@ -5016,6 +5084,9 @@ translators: Block name. %s: The localized block name */ Title for the warning shown to the user when the app realizes there should be an auth token but there isn't one. */ "Oops!" = "Oops!"; +/* No comment provided by engineer. */ +"Opacity" = "Opacity"; + /* No comment provided by engineer. */ "Open Block Actions Menu" = "Open Block Actions Menu"; @@ -5033,9 +5104,6 @@ translators: Block name. %s: The localized block name */ Title of a button that opens the apps settings in the system Settings.app */ "Open Settings" = "Open Settings"; -/* Editor button to swich the media picker from quick mode to full picker */ -"Open full media picker" = "Open full media picker"; - /* No comment provided by engineer. */ "Open in Safari" = "Open in Safari"; @@ -5075,6 +5143,9 @@ translators: Block name. %s: The localized block name */ /* Divider on initial auth view separating auth options. */ "Or" = "Or"; +/* Instruction text for other forms of two-factor auth methods. */ +"Or choose another form of authentication." = "Or choose another form of authentication."; + /* Label for button to log in using site address. Underscores _..._ denote underline. */ "Or log in by _entering your site address_." = "Or log in by _entering your site address_."; @@ -5116,6 +5187,9 @@ translators: Block name. %s: The localized block name */ Other Sites Notification Settings Title */ "Other Sites" = "Other Sites"; +/* No comment provided by engineer. */ +"Outside" = "Outside"; + /* Register Domain - Phone number section header title */ "PHONE" = "Phone"; @@ -5130,15 +5204,6 @@ translators: Block name. %s: The localized block name */ Title shown when selecting a post type of Page from the Share Extension. */ "Page" = "Page"; -/* Prompts the user that a restored page was moved to the drafts list. */ -"Page Restored to Drafts" = "Page Restored to Drafts"; - -/* Prompts the user that a restored page was moved to the published list. */ -"Page Restored to Published" = "Page Restored to Published"; - -/* Prompts the user that a restored page was moved to the scheduled list. */ -"Page Restored to Scheduled" = "Page Restored to Scheduled"; - /* Name of the button to open the page settings The title of the Page Settings screen. */ "Page Settings" = "Page Settings"; @@ -5155,9 +5220,6 @@ translators: Block name. %s: The localized block name */ /* Title of notification displayed when a page has failed to upload. */ "Page failed to upload" = "Page failed to upload"; -/* A short message explaining that a page was moved to the trash bin. */ -"Page moved to trash." = "Page moved to trash."; - /* Title of notification displayed when a page has been successfully saved as a draft. */ "Page pending review" = "Page pending review"; @@ -5229,8 +5291,7 @@ translators: Block name. %s: The localized block name */ Title of pending Comments filter. */ "Pending" = "Pending"; -/* Name for the status of a post pending review. - Title of the Pending Review Badge */ +/* Name for the status of a post pending review. */ "Pending review" = "Pending review"; /* Noun. Title of the people management feature. @@ -5259,12 +5320,6 @@ translators: Block name. %s: The localized block name */ /* Photography site intent topic */ "Photography" = "Photography"; -/* Tab bar title for the Photos tab in Media Picker */ -"Photos" = "Photos"; - -/* Subtitle for placeholder in Free Photos. The company name 'Pexels' should always be written as it is. */ -"Photos provided by Pexels" = "Photos provided by Pexels"; - /* Title for selecting a new username in the site creation flow. */ "Pick username" = "Pick username"; @@ -5278,6 +5333,12 @@ translators: Block name. %s: The localized block name */ /* User action to play a video on the editor. */ "Play video" = "Play video"; +/* No comment provided by engineer. */ +"Playback Bar Color" = "Playback Bar Colour"; + +/* No comment provided by engineer. */ +"Playback Settings" = "Playback Settings"; + /* Suggestion to add content before trying to publish post or page */ "Please add some content before trying to publish." = "Please add some content before trying to publish."; @@ -5351,7 +5412,7 @@ translators: Block name. %s: The localized block name */ "Please enter the password for your WordPress.com account to log in with your Apple ID." = "Please enter the password for your WordPress.com account to log in with your Apple ID."; /* Instruction text on the two-factor screen. */ -"Please enter the verification code from your authenticator app, or tap the link below to receive a code via SMS." = "Please enter the verification code from your authenticator app, or tap the link below to receive a code via SMS."; +"Please enter the verification code from your authenticator app." = "Please enter the verification code from your authenticator app."; /* Popup message to ask for user credentials (fields shown below). */ "Please enter your credentials" = "Please enter your credentials"; @@ -5446,15 +5507,6 @@ translators: Block name. %s: The localized block name */ The post formats available for the post. Should be the same as in core WP. */ "Post Format" = "Post Format"; -/* Prompts the user that a restored post was moved to the drafts list. */ -"Post Restored to Drafts" = "Post Restored to Drafts"; - -/* Prompts the user that a restored post was moved to the published list. */ -"Post Restored to Published" = "Post Restored to Published"; - -/* Prompts the user that a restored post was moved to the scheduled list. */ -"Post Restored to Scheduled" = "Post Restored to Scheduled"; - /* Name of the button to open the post settings The title of the Post Settings screen. */ "Post Settings" = "Post Settings"; @@ -5474,9 +5526,6 @@ translators: Block name. %s: The localized block name */ /* Title of notification displayed when a post has failed to upload. */ "Post failed to upload" = "Post failed to upload"; -/* A short message explaining that a post was moved to the trash bin. */ -"Post moved to trash." = "Post moved to trash."; - /* Title of notification displayed when a post has been successfully saved as a draft. */ "Post pending review" = "Post pending review"; @@ -5535,9 +5584,6 @@ translators: Block name. %s: The localized block name */ Period Stats 'Posts and Pages' header */ "Posts and Pages" = "Posts and Pages"; -/* Title of the Posts Page Badge */ -"Posts page" = "Posts page"; - /* Message informing the user that their static homepage for posts was set successfully */ "Posts page successfully updated" = "Posts page successfully updated"; @@ -5550,9 +5596,6 @@ translators: Block name. %s: The localized block name */ /* A message explaining the Posts I Like feature in the reader */ "Posts that you like will appear here." = "Posts that you like will appear here."; -/* Subtitle for placeholder in Tenor picker. `The company name 'Tenor' should always be written as it is. */ -"Powered by Tenor" = "Powered by Tenor"; - /* Browse premium themes selection title */ "Premium" = "Premium"; @@ -5571,18 +5614,12 @@ translators: Block name. %s: The localized block name */ Title for screen to preview a static content. */ "Preview" = "Preview"; -/* Action for Media Picker to preview the selected media items. The argument in the string represents the number of elements (as numeric digits) selected */ -"Preview %@" = "Preview %@"; - /* Title for web preview device switching button */ "Preview Device" = "Preview Device"; /* Title on display preview error */ "Preview Unavailable" = "Preview Unavailable"; -/* Accessibility label for media item preview for user's viewing an item in their media library */ -"Preview media" = "Preview media"; - /* No comment provided by engineer. */ "Preview page" = "Preview page"; @@ -5622,12 +5659,14 @@ translators: Block name. %s: The localized block name */ Privacy Settings Title */ "Privacy Settings" = "Privacy Settings"; +/* No comment provided by engineer. */ +"Privacy and Rating" = "Privacy and Rating"; + /* Link to the CCPA privacy notice for residents of California. */ "Privacy notice for California users" = "Privacy notice for California users"; /* Name for the status of a post that is marked private. - Privacy setting for posts set to 'Private'. Should be the same as in core WP. - Title of the Private Badge */ + Privacy setting for posts set to 'Private'. Should be the same as in core WP. */ "Private" = "Private"; /* No comment provided by engineer. */ @@ -5677,12 +5716,10 @@ translators: Block name. %s: The localized block name */ Label for the publish date button. */ "Publish Date" = "Publish Date"; -/* A short phrase indicating a post is due to be immedately published. - Label for a button that moves a page to the published folder, publishing with the current date/time. */ +/* A short phrase indicating a post is due to be immedately published. */ "Publish Immediately" = "Publish Immediately"; /* Label for a button that publishes the post - Label for an option that moves a publishes a post immediately Title of button allowing the user to immediately publish the post they are editing. */ "Publish Now" = "Publish Now"; @@ -5700,8 +5737,7 @@ translators: Block name. %s: The localized block name */ /* Name for the status of a published post. Period Stats 'Published' header - Title of the published filter. This filter shows a list of posts that the user has published. - Title of the published header in search list. */ + Title of the published filter. This filter shows a list of posts that the user has published. */ "Published" = "Published"; /* Precedes the name of the blog just posted on */ @@ -5801,6 +5837,9 @@ translators: Block name. %s: The localized block name */ /* Message shwon to confirm a publicize connection has been successfully reconnected. */ "Reconnected" = "Reconnected"; +/* Action button to redo last change */ +"Redo" = "Redo"; + /* Label for link title in Referrers stat. */ "Referrer" = "Referrer"; @@ -5840,8 +5879,7 @@ translators: Block name. %s: The localized block name */ /* Title of the completion screen of the Blogging Reminders Settings screen when the reminders are removed. */ "Reminders removed" = "Reminders removed"; -/* Add asset to media picker list - Alert button to confirm a plugin to be removed +/* Alert button to confirm a plugin to be removed Button label when removing a blog Label for the Remove Feature Image icon. Tapping will show a confirmation screen for removing the feature image from the post. Remove Action @@ -5883,6 +5921,9 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Remove block" = "Remove block"; +/* No comment provided by engineer. */ +"Remove blocks" = "Remove blocks"; + /* Option to remove Insight from view. */ "Remove from insights" = "Remove from insights"; @@ -5991,9 +6032,6 @@ translators: Block name. %s: The localized block name */ /* Title of secondary button on alert prompting verify their accounts while attempting to publish */ "Resend" = "Resend"; -/* Title of the reset button */ -"Reset" = "Reset"; - /* Accessibility label for the reset activity type button */ "Reset Activity Type filter" = "Reset Activity Type filter"; @@ -6048,12 +6086,9 @@ translators: Block name. %s: The localized block name */ Button title, displayed when media has failed to upload. Allows the user to try the upload again. Button title. Retries uploading a post. If a user taps the button with this label, the action that evinced this error view will be retried. - Label for a button that attempts to re-upload a page that previously failed to upload. - Label for the retry post upload button. Tapping attempts to upload the post again. Opens the media library . Retries the upload of a user's post. Retry updating User's Role - Retry uploading the post. Retry. Action Retry. Verb – retry a failed media upload. The Jetpack view button title used when an error occurred @@ -6065,9 +6100,6 @@ translators: Block name. %s: The localized block name */ /* Button title that triggers a scan */ "Retry Scan" = "Retry Scan"; -/* User action to retry media upload. */ -"Retry Upload" = "Retry Upload"; - /* User action to retry all failed media uploads. */ "Retry all" = "Retry all"; @@ -6165,9 +6197,6 @@ translators: Block name. %s: The localized block name */ /* Accessibility label for the 'Save Post' button when a post has been saved. */ "Saved Post" = "Saved Post"; -/* Text displayed in HUD when a media item's metadata (title, etc) is saved successfully. */ -"Saved!" = "Saved!"; - /* Accessibility hint for the 'Save Post' button. */ "Saves this post for later." = "Saves this post for later."; @@ -6178,7 +6207,6 @@ translators: Block name. %s: The localized block name */ "Saving post…" = "Saving post…"; /* Menus save button title while it is saving a Menu. - Text displayed in HUD while a media item's metadata (title, etc) is being saved. Text displayed in HUD while a post is being saved as a draft. */ "Saving..." = "Saving..."; @@ -6248,6 +6276,9 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Search block label. Current text is" = "Search block label. Current text is"; +/* No comment provided by engineer. */ +"Search blocks" = "Search blocks"; + /* No comment provided by engineer. */ "Search button. Current button text is" = "Search button. Current button text is"; @@ -6257,20 +6288,17 @@ translators: Block name. %s: The localized block name */ /* title of the button that searches the first domain. */ "Search for a domain" = "Search for a domain"; -/* No comment provided by engineer. */ -"Search or type URL" = "Search or type URL"; - -/* Text displayed when the search controller will be presented */ -"Search pages" = "Search pages"; +/* Select domain name. Subtitle */ +"Search for a short and memorable keyword to help people find and visit your website." = "Search for a short and memorable keyword to help people find and visit your website."; -/* Text displayed when the search controller will be presented */ -"Search posts" = "Search posts"; +/* No comment provided by engineer. */ +"Search input field." = "Search input field."; -/* Title for placeholder in Tenor picker */ -"Search to find GIFs to add to your Media Library!" = "Search to find GIFs to add to your Media Library!"; +/* No comment provided by engineer. */ +"Search or type URL" = "Search or type URL"; -/* Title for placeholder in Free Photos */ -"Search to find free photos to add to your Media Library!" = "Search to find free photos to add to your Media Library!"; +/* No comment provided by engineer. */ +"Search settings" = "Search settings"; /* Menus search bar placeholder text. */ "Search..." = "Search..."; @@ -6342,9 +6370,6 @@ translators: Block name. %s: The localized block name */ /* Register Domain - Domain contact information field placeholder for Country */ "Select Country" = "Select Country"; -/* Title of button that allows the user to select more photos to access within the app */ -"Select More" = "Select More"; - /* Blog Picker's Title */ "Select Site" = "Select Site"; @@ -6354,6 +6379,9 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Select a color" = "Select a colour"; +/* No comment provided by engineer. */ +"Select a color above" = "Select a colour above"; + /* Reader select interests next button disabled title text */ "Select a few to continue" = "Select a few to continue"; @@ -6363,9 +6391,6 @@ translators: Block name. %s: The localized block name */ /* Register domain - Title for the Choose domain button of Suggested domains screen */ "Select domain" = "Select domain"; -/* Accessibility hint for actions when displaying media items. */ -"Select media." = "Select media."; - /* Accessibility label for selecting paragraph style button on formatting toolbar. */ "Select paragraph style" = "Select paragraph style"; @@ -6414,6 +6439,13 @@ translators: Block name. %s: The localized block name */ /* Accessibility label for summary of currently selected range. %1$@ is the start date, %2$@ is the end date. */ "Selected range: %1$@ to %2$@" = "Selected range: %1$@ to %2$@"; +/* translators: %s: Select font size option value e.g: \"Selected: Large\". +translators: %s: Select control option value e.g: \"Auto, 25%\". */ +"Selected: %s" = "Selected: %s"; + +/* No comment provided by engineer. */ +"Selected: Default" = "Selected: Default"; + /* Menus alert message for alerting the user to unsaved changes while trying to select a different menu location. */ "Selecting a different menu location will discard changes you've made to the current menu. Are you sure you want to continue?" = "Selecting a different menu location will discard changes you've made to the current menu. Are you sure you want to continue?"; @@ -6462,19 +6494,12 @@ translators: Block name. %s: The localized block name */ /* Label for connected service in Publicize stat. */ "Service" = "Service"; -/* Label for a button that opens the Set Parent options view controller - Navigation title displayed on the navigation bar */ +/* Navigation title displayed on the navigation bar */ "Set Parent" = "Set Parent"; /* No comment provided by engineer. */ "Set as Featured Image" = "Set as Featured Image"; -/* Label for a button that sets the selected page as the site's Homepage */ -"Set as Homepage" = "Set as Home page"; - -/* Label for a button that sets the selected page as the site's Posts page */ -"Set as Posts Page" = "Set as Posts Page"; - /* Notice confirming that an image has been set as the post's featured image. */ "Set as featured image" = "Set as featured image"; @@ -6518,7 +6543,6 @@ translators: Block name. %s: The localized block name */ /* Accessibility label for share buttons in nav bars Button label to share a post Button label to share a web page - Share the post. Shares the comment URL Spoken accessibility label Title for a button that allows the user to share their answer to the prompt. @@ -6751,6 +6775,9 @@ translators: Block name. %s: The localized block name */ /* Menu title to skip today's prompt. */ "Skip for today" = "Skip for today"; +/* translators: Slash inserter autocomplete results */ +"Slash inserter results" = "Slash inserter results"; + /* Label for the slug field. Should be the same as WP core. */ "Slug" = "Slug"; @@ -6901,8 +6928,7 @@ translators: Block name. %s: The localized block name */ /* Name of setting configured when a site uses a static page as its homepage */ "Static Homepage" = "Static Home page"; -/* Label for post stats option. Tapping displays statistics for a post. - Noun. Abbreviation of Statistics. Name of the Stats feature +/* Noun. Abbreviation of Statistics. Name of the Stats feature Noun. Abbv. of Statistics. Links to a blog's Stats screen. Stats 3D Touch Shortcut Stats window title @@ -6933,9 +6959,6 @@ translators: Block name. %s: The localized block name */ /* Label text that defines a post marked as sticky */ "Sticky" = "Sticky"; -/* Accessibility label for a sticky post in the post list. */ -"Sticky." = "Sticky."; - /* User action to stop upload. */ "Stop upload" = "Stop upload"; @@ -6992,7 +7015,7 @@ translators: Block name. %s: The localized block name */ Theme Support action title */ "Support" = "Support"; -/* Button used to switch site */ +/* Title for back button that leads to the site picker screen. */ "Switch Site" = "Switch Site"; /* Switches the Editor to HTML Mode */ @@ -7037,6 +7060,9 @@ translators: Block name. %s: The localized block name */ /* Accessibility Identifier for the Default Font Aztec Style. */ "Switches to the default Font Size" = "Switches to the default Font Size"; +/* No comment provided by engineer. */ +"Synced patterns" = "Synced patterns"; + /* Title for the app appearance setting (light / dark mode) that uses the system default value */ "System default" = "System default"; @@ -7077,9 +7103,6 @@ translators: Block name. %s: The localized block name */ /* Label explaining why users might want to add tags. */ "Tags help tell readers what a post is about. Separate different tags with commas." = "Tags help tell readers what a post is about. Separate different tags with commas."; -/* Menu option for taking an image or video with the device's camera. */ -"Take Photo or Video" = "Take Photo or Video"; - /* No comment provided by engineer. */ "Take a Photo" = "Take a Photo"; @@ -7098,6 +7121,9 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Tap here to show help" = "Tap here to show help"; +/* No comment provided by engineer. */ +"Tap here to show more details." = "Tap here to show more details."; + /* Accessibility hint for a button that opens a view that allows to add new stats cards. */ "Tap to add new stats cards." = "Tap to add new stats cards."; @@ -7147,12 +7173,6 @@ translators: Block name. %s: The localized block name */ /* Accessibility hint */ "Tap to select the previous period" = "Tap to select the previous period"; -/* Accessibility hint for button used to switch site */ -"Tap to switch to another site, or add a new site" = "Tap to switch to another site, or add a new site"; - -/* Accessibility hint for media item preview for user's viewing an item in their media library */ -"Tap to view media in full screen" = "Tap to view media in full screen"; - /* Accessibility hint for a button that opens a new view with more details. */ "Tap to view more details." = "Tap to view more details."; @@ -7192,13 +7212,18 @@ translators: Block name. %s: The localized block name */ /* Title of a button style */ "Text Only" = "Text Only"; +/* No comment provided by engineer. */ +"Text color" = "Text colour"; + /* No comment provided by engineer. */ "Text formatting controls are located within the toolbar positioned above the keyboard while editing a text block" = "Text formatting controls are located within the toolbar positioned above the keyboard while editing a text block"; -/* Button title - The button's title text to send a 2FA code via SMS text message. */ +/* Button title */ "Text me a code instead" = "Text me a code instead"; +/* The button's title text to send a 2FA code via SMS text message. */ +"Text me a code via SMS" = "Text me a code via SMS"; + /* Message of alert when theme activation succeeds */ "Thanks for choosing %@ by %@" = "Thanks for choosing %1$@ by %2$@"; @@ -7226,9 +7251,6 @@ translators: Block name. %s: The localized block name */ /* Error message shown to a user who is trying to share to Facebook but does not have any available Facebook Pages. */ "The Facebook connection cannot find any Pages. Publicize cannot connect to Facebook Profiles, only published Pages." = "The Facebook connection cannot find any Pages. Publicize cannot connect to Facebook Profiles, only published Pages."; -/* Message shown when a GIF failed to load while trying to add it to the Media library. */ -"The GIF could not be added to the Media Library." = "The GIF could not be added to the Media Library."; - /* Description shown when a user logs in with Google but no matching WordPress.com account is found */ "The Google account \"%@\" doesn't match any account on WordPress.com" = "The Google account \"%@\" doesn't match any account on WordPress.com"; @@ -7356,7 +7378,7 @@ translators: Block name. %s: The localized block name */ /* Error message shown when user attempts to remove the site owner. */ "The user you are trying to remove is the owner of this site. Please contact support for assistance." = "The user you are trying to remove is the owner of this site. Please contact support for assistance."; -/* Error message informing a user about an invalid password. */ +/* No comment provided by engineer. */ "The username or password stored in the app may be out of date. Please re-enter your password in the settings and try again." = "The username or password stored in the app may be out of date. Please re-enter your password in the settings and try again."; /* Message shown when a video failed to load while trying to add it to the Media library. */ @@ -7424,9 +7446,6 @@ translators: Block name. %s: The localized block name */ /* A short error message letting the user know about a problem displaying a post. */ "There was a problem displaying this post." = "There was a problem displaying this post."; -/* Error message displayed when the Media Library is unable to load a full sized preview of an item. */ -"There was a problem loading the media item." = "There was a problem loading the media item."; - /* The loading view subtitle displayed when an error occurred */ "There was a problem loading your data, refresh your page to try again." = "There was a problem loading your data, refresh your page to try again."; @@ -7439,9 +7458,6 @@ translators: Block name. %s: The localized block name */ /* Explaining to the user there was an error trying to obtain the current location of the user. */ "There was a problem when trying to access your location. Please try again later." = "There was a problem when trying to access your location. Please try again later."; -/* Explaining to the user there was an generic error accesing media. */ -"There was a problem when trying to access your media. Please try again later." = "There was a problem when trying to access your media. Please try again later."; - /* Message for stories unknown error. */ "There was a problem with the Stories editor. If the problem persists you can contact us via the Me > Help & Support screen." = "There was a problem with the Stories editor. If the problem persists you can contact us via the Me > Help & Support screen."; @@ -7512,9 +7528,6 @@ translators: Block name. %s: The localized block name */ /* A description informing the user in order to proceed with this feature we will need camera permissions, and how to enable it. */ "This app needs permission to access the Camera to scan login codes, tap on the Open Settings button to enable it." = "This app needs permission to access the Camera to scan login codes, tap on the Open Settings button to enable it."; -/* Explaining to the user why the app needs access to the device media library. */ -"This app needs permission to access your device media library in order to add photos and\/or video to your posts. Please change the privacy settings if you wish to allow this." = "This app needs permission to access your device media library in order to add photos and\/or video to your posts. Please change the privacy settings if you wish to allow this."; - /* No comment provided by engineer. */ "This color combination may be hard for people to read. Try using a brighter background color and\/or a darker text color." = "This colour combination may be hard for people to read. Try using a brighter background colour and\/or a darker text colour."; @@ -7601,6 +7614,9 @@ translators: Block name. %s: The localized block name */ /* Message displayed when a threat is ignored successfully. */ "Threat ignored." = "Threat ignored."; +/* No comment provided by engineer. */ +"Three" = "Three"; + /* Thumbnail image size. Should be the same as in core WP. */ "Thumbnail" = "Thumbnail"; @@ -7621,6 +7637,9 @@ translators: Block name. %s: The localized block name */ /* Description of a Quick Start Tour */ "Time to finish setting up your site! Our checklist walks you through the next steps." = "Time to finish setting up your site! Our checklist walks you through the next steps."; +/* Error when the uses takes more than 1 minute to submit a security key. */ +"Time's up, but don't worry, your security is our priority. Please try again!" = "Time's up, but don't worry, your security is our priority. Please try again!"; + /* WordPress.com Marketing Footer Text */ "Tips for getting the most out of WordPress.com." = "Tips for getting the most out of WordPress.com."; @@ -7744,18 +7763,20 @@ translators: Block name. %s: The localized block name */ /* Title for the traffic section in site settings screen */ "Traffic" = "Traffic"; -/* Accessibility label for trash button to delete items from the user's media library - Accessibility label for trash buttons in nav bars +/* Describes a domain that was transferred from elsewhere to wordpress.com */ +"Transferred Domain" = "Transferred Domain"; + +/* translators: %s: block title e.g: \"Paragraph\". */ +"Transform %s to" = "Transform %s to"; + +/* No comment provided by engineer. */ +"Transform block…" = "Transform block…"; + +/* Accessibility label for trash buttons in nav bars Trashes a comment Trashes the comment */ "Trash" = "Trash"; -/* Accessibility hint for trash button to delete items from the user's media library */ -"Trash selected media" = "Trash selected media"; - -/* Title of the trash page confirmation alert. */ -"Trash this page?" = "Trash this page?"; - /* Title of the trash confirmation alert. */ "Trash this post?" = "Trash this post?"; @@ -7795,6 +7816,9 @@ translators: Block name. %s: The localized block name */ Re-load the history again. It appears if the loading call fails. */ "Try again" = "Try again"; +/* No comment provided by engineer. */ +"Try another search term" = "Try another search term"; + /* Button title on the blogging prompt's feature introduction view to answer a prompt. */ "Try it now" = "Try it now"; @@ -7831,6 +7855,9 @@ translators: Block name. %s: The localized block name */ /* Notice title when turning site notifications on succeeds. */ "Turned on site notifications" = "Turned on site notifications"; +/* Notification Settings switch for the app. */ +"Turning the switch off will disable all notifications from this app, regardless of type." = "Turning the switch off will disable all notifications from this app, regardless of type."; + /* Title of button that displays the app's Twitter profile */ "Twitter" = "Twitter"; @@ -7843,6 +7870,9 @@ translators: Block name. %s: The localized block name */ /* Type menu item in share extension. */ "Type" = "Type"; +/* No comment provided by engineer. */ +"Type a URL" = "Type a URL"; + /* Placeholder text for domain search during site creation. */ "Type a keyword for more ideas" = "Type a keyword for more ideas"; @@ -7864,9 +7894,6 @@ translators: Block name. %s: The localized block name */ /* Shown when a user logs in with Google but it subsequently fails to work as login to WordPress.com */ "Unable To Connect" = "Unable To Connect"; -/* An error message. */ -"Unable to Connect" = "Unable to Connect"; - /* Title for stories unknown error. */ "Unable to Create Stories Editor" = "Unable to Create Stories Editor"; @@ -7882,9 +7909,6 @@ translators: Block name. %s: The localized block name */ /* An error message shown when there is an issue creating new invite links. */ "Unable to create new invite links." = "Unable to create new invite links."; -/* Text displayed in HUD if there was an error attempting to delete a group of media items. */ -"Unable to delete all media items." = "Unable to delete all media items."; - /* Text displayed in HUD if there was an error attempting to delete a media item. */ "Unable to delete media item." = "Unable to delete media item."; @@ -7894,6 +7918,9 @@ translators: Block name. %s: The localized block name */ /* Message displayed when opening the link to the downloadable backup fails. */ "Unable to download file" = "Unable to download file"; +/* No comment provided by engineer. */ +"Unable to embed media" = "Unable to embed media"; + /* The app failed to subscribe to the comments for the post */ "Unable to follow conversation" = "Unable to follow conversation"; @@ -7945,12 +7972,6 @@ translators: Block name. %s: The localized block name */ /* Message displayed when sharing a link to the downloadable backup fails. */ "Unable to share link" = "Unable to share link"; -/* Message that appears when a user tries to trash a page while their device is offline. */ -"Unable to trash pages while offline. Please try again later." = "Unable to trash pages while offline. Please try again later."; - -/* Message that appears when a user tries to trash a post while their device is offline. */ -"Unable to trash posts while offline. Please try again later." = "Unable to trash posts while offline. Please try again later."; - /* Notice title when turning site notifications off fails. */ "Unable to turn off site notifications" = "Unable to turn off site notifications"; @@ -8023,8 +8044,6 @@ translators: Block name. %s: The localized block name */ Button title. Reverts the previous notification operation Revert an operation Revert enabling notification after successfully subcribing to the comments for the post. - The title of an 'undo' button. Tapping the button moves a trashed page out of the trash folder. - The title of an 'undo' button. Tapping the button moves a trashed post out of the trash folder. Undo action */ "Undo" = "Undo"; @@ -8054,6 +8073,9 @@ translators: Block name. %s: The localized block name */ /* VoiceOver accessibility hint, informing the user the button can be used to unfollow a blog. */ "Unfollows the blog." = "Unfollows the blog."; +/* No comment provided by engineer. */ +"Ungroup block" = "Ungroup block"; + /* Unhides a site from the site picker list */ "Unhide" = "Unhide"; @@ -8064,9 +8086,6 @@ translators: Block name. %s: The localized block name */ /* Title for Unknown HTML Editor */ "Unknown HTML" = "Unknown HTML"; -/* Label to use when creation date from media asset is not know. */ -"Unknown creation date" = "Unknown creation date"; - /* No comment provided by engineer. */ "Unknown error" = "Unknown error"; @@ -8076,6 +8095,9 @@ translators: Block name. %s: The localized block name */ /* Search Terms label for 'unknown search terms'. */ "Unknown search terms" = "Unknown search terms"; +/* translators: %s: the hex color value */ +"Unlabeled color. %s" = "Unlabeled colour. %s"; + /* VoiceOver accessibility hint, informing the user the button can be used to stop liking a comment */ "Unlike the Comment." = "Unlike the Comment."; @@ -8214,6 +8236,9 @@ translators: Block name. %s: The localized block name */ /* Label to show while uploading media to server */ "Uploading..." = "Uploading..."; +/* No comment provided by engineer. */ +"Uploading…" = "Uploading…"; + /* Title for alert when trying to save post with failed media items */ "Uploads failed" = "Uploads failed"; @@ -8226,9 +8251,15 @@ translators: Block name. %s: The localized block name */ /* Title of a row displayed on the debug screen used to configure the sandbox store use in the App. */ "Use Sandbox Store" = "Use Sandbox Store"; +/* The button's title text to use a security key. */ +"Use a security key" = "Use a security key"; + /* Option to enable the block editor for new posts */ "Use block editor" = "Use block editor"; +/* No comment provided by engineer. */ +"Use icon button" = "Use icon button"; + /* The button title text for logging in with WP.com password instead of magic link. */ "Use password to sign in" = "Use password to sign in"; @@ -8298,15 +8329,10 @@ translators: Block name. %s: The localized block name */ /* Title of an alert informing users that the video they are trying to select is not allowed. */ "Video not uploaded" = "Video not uploaded"; -/* Accessibility label for video thumbnails in the media collection view. The parameter is the creation date of the video. */ -"Video, %@" = "Video, %@"; - /* Period Stats 'Videos' header */ "Videos" = "Videos"; /* Button title. Displays a summary / sharing screen for a specific post. - Label for a button that opens the page when tapped. - Label for the view post button. Tapping displays the post as it appears on the web. Opens the post epilogue screen to allow sharing / viewing of a post. Theme View action title Verb. The screen title shown when viewing a post inside the app. */ @@ -8421,6 +8447,10 @@ translators: Block name. %s: The localized block name */ /* Message shown on screen while waiting for Google to finish its signup process. */ "Waiting for Google to complete…" = "Waiting for Google to complete…"; +/* Text while the webauthn signature is being verified + Text while waiting for a security key challenge */ +"Waiting for security key" = "Waiting for security key"; + /* View title during the Google auth process. */ "Waiting..." = "Waiting..."; @@ -8432,6 +8462,9 @@ translators: Block name. %s: The localized block name */ Title for Jetpack Restore Warning screen */ "Warning" = "Warning"; +/* No comment provided by engineer. */ +"Warning message" = "Warning Message"; + /* Caption displayed in promotional screens shown during the login flow. */ "Watch your audience grow with in-depth analytics." = "Watch your audience grow with in-depth analytics."; @@ -8741,6 +8774,12 @@ translators: Block name. %s: The localized block name */ /* Navigates to page with details about What is WordPress.com. */ "What is WordPress.com?" = "What is WordPress.com?"; +/* No comment provided by engineer. */ +"What is a block?" = "What is a block?"; + +/* No comment provided by engineer. */ +"What is alt text?" = "What is alt text?"; + /* Title for the problem section in the Threat Details */ "What was the problem?" = "What was the problem?"; @@ -8786,9 +8825,18 @@ translators: Block name. %s: The localized block name */ /* An error message shown when a wpcom user provides the wrong password. */ "Whoops, something went wrong and we couldn't log you in. Please try again!" = "Whoops, something went wrong and we couldn't log you in. Please try again!"; +/* Generic error on the 2FA screen */ +"Whoops, something went wrong. Please try again!" = "Whoops, something went wrong. Please try again!"; + +/* Error when the uses chooses an invalid security key on the 2FA screen. */ +"Whoops, that security key does not seem valid. Please try again with another one" = "Whoops, that security key does not seem valid. Please try again with another one"; + /* Error message shown when an incorrect two factor code is provided. */ "Whoops, that's not a valid two-factor verification code. Double-check your code and try again!" = "Whoops, that's not a valid two-factor verification code. Double-check your code and try again!"; +/* No comment provided by engineer. */ +"Width Settings" = "Width Settings"; + /* Help text when editing email address */ "Will not be publicly displayed." = "Will not be publicly displayed."; @@ -8810,9 +8858,6 @@ translators: Block name. %s: The localized block name */ /* Siri Suggestion to open Support */ "WordPress Help" = "WordPress Help"; -/* Name for the WordPress Media Library */ -"WordPress Media" = "WordPress Media"; - /* Accessibility label for selecting an image or video from the user's WordPress media library on formatting toolbar. */ "WordPress Media Library" = "WordPress Media Library"; @@ -8910,6 +8955,12 @@ translators: Block name. %s: The localized block name */ /* Title of button that asks the users if they'd like to focus on checking their sites stats */ "Writing blog posts" = "Writing blog posts"; +/* No comment provided by engineer. */ +"X-Axis Position" = "X-Axis Position"; + +/* No comment provided by engineer. */ +"Y-Axis Position" = "Y-Axis Position"; + /* Option to select the Yahoo Mail app when logging in with magic links */ "Yahoo Mail" = "Yahoo Mail"; @@ -8959,6 +9010,12 @@ translators: Block name. %s: The localized block name */ /* Information shown below the optional password field after new account creation. */ "You can always log in with a link like the one you just used, but you can also set up a password if you prefer." = "You can always log in with a link like the one you just used, but you can also set up a password if you prefer."; +/* Note displayed in the Feature Introduction view. */ +"You can control Blogging Prompts and Reminders at any time in My Site > Settings > Blogging" = "You can control Blogging Prompts and Reminders at any time in My Site > Settings > Blogging"; + +/* Accessibility hint for Note displayed in the Feature Introduction view. */ +"You can control Blogging Prompts and Reminders at any time in My Site, Settings, Blogging" = "You can control Blogging Prompts and Reminders at any time in My Site, Settings, Blogging"; + /* No comment provided by engineer. */ "You can edit this block using the web version of the editor." = "You can edit this block using the web version of the editor."; @@ -9115,9 +9172,6 @@ translators: Block name. %s: The localized block name */ /* Share extension error dialog text. */ "Your account does not have permission to upload media to this site. The Site Administrator can change these permissions." = "Your account does not have permission to upload media to this site. The Site Administrator can change these permissions."; -/* Explaining to the user why the app needs access to the device media library. */ -"Your app is not authorized to access media library due to active restrictions such as parental controls. Please check your parental control settings in this device." = "Your app is not authorised to access media library due to active restrictions such as parental controls. Please check your parental control settings in this device."; - /* Title for the Jetpack Backup Complete message. */ "Your backup is now available for download" = "Your backup is now available for download"; @@ -9136,9 +9190,6 @@ translators: Block name. %s: The localized block name */ /* Title of the site address section in the Domains Dashboard. */ "Your free WordPress.com address is" = "Your free WordPress.com address is"; -/* Error message when picked media cannot be imported into stories. */ -"Your media could not be exported. If the problem persists you can contact us via the Me > Help & Support screen." = "Your media could not be exported. If the problem persists you can contact us via the Me > Help & Support screen."; - /* Details about recently acquired domain on domain credit redemption success screen */ "Your new domain %@ is being set up. It may take up to 30 minutes for your domain to start working." = "Your new domain %@ is being set up. It may take up to 30 minutes for your domain to start working."; @@ -9256,8 +9307,53 @@ translators: Block name. %s: The localized block name */ /* Age between dates equaling one hour. */ "an hour" = "an hour"; -/* Label displayed on audio media items. */ -"audio" = "audio"; +/* This is the string we display when prompting the user to review the Jetpack app */ +"appRatings.jetpack.prompt" = "What do you think about Jetpack?"; + +/* This is the string we display when prompting the user to review the WordPress app */ +"appRatings.wordpress.prompt" = "What do you think about WordPress?"; + +/* Option to enable the optimization of images when uploading. */ +"appSettings.media.imageOptimizationRow" = "Optimise Images"; + +/* Indicates an image will use high quality when uploaded. */ +"appSettings.media.imageQuality.high" = "High"; + +/* Indicates an image will use low quality when uploaded. */ +"appSettings.media.imageQuality.low" = "Low"; + +/* Indicates an image will use maximum quality when uploaded. */ +"appSettings.media.imageQuality.maximum" = "Maximum"; + +/* Indicates an image will use medium quality when uploaded. */ +"appSettings.media.imageQuality.medium" = "Medium"; + +/* The quality of image used when uploading */ +"appSettings.media.imageQuality.title" = "Quality"; + +/* Title for the image quality settings option. */ +"appSettings.media.imageQualityRow" = "Image Quality"; + +/* Message of an alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.message" = "Image optimisation shrinks images for faster uploading.\n\nThis option is enabled by default, but you can change it in the app settings at any time."; + +/* Title of an alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.title" = "Keep optimising images?"; + +/* Title of button for turning off image optimization, displayed in the alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.turnOff" = "No, turn off"; + +/* Title of button for leaving on image optimization, displayed in the alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.turnOn" = "Yes, leave on"; + +/* translators: displays audio file extension. e.g. MP3 audio file */ +"audio file" = "audio file"; + +/* Alert message when something goes wrong with the selected image. */ +"avatarMenu.failedToSetAvatarAlertMessage" = "Unable to load the image. Please choose a different one or try again later."; + +/* Title for menu that is shown when you tap your gravatar */ +"avatarMenu.title" = "Update Gravatar"; /* The title of a button to close the classic editor deprecation notice alert dialog. */ "aztecPost.deprecationNotice.dismiss" = "Dismiss"; @@ -9265,18 +9361,193 @@ translators: Block name. %s: The localized block name */ /* User action to dismiss media options. */ "aztecPost.mediaAttachmentActionSheet.dismiss" = "Dismiss"; +/* Button title for the button that shows the Blaze flow when tapped. */ +"blaze.campaigns.create.button.title" = "Create"; + +/* Text displayed when there are no Blaze campaigns to display. */ +"blaze.campaigns.empty.subtitle" = "You have not created any campaigns yet. Click create to get started."; + +/* Title displayed when there are no Blaze campaigns to display. */ +"blaze.campaigns.empty.title" = "You have no campaigns"; + +/* Text displayed when there is a failure loading Blaze campaigns. */ +"blaze.campaigns.errorMessage" = "There was an error loading campaigns."; + +/* Title for the view when there's an error loading Blaze campiagns. */ +"blaze.campaigns.errorTitle" = "Oops"; + +/* Displayed while Blaze campaigns are being loaded. */ +"blaze.campaigns.loading.title" = "Loading campaigns..."; + +/* Title for the screen that allows users to manage their Blaze campaigns. */ +"blaze.campaigns.title" = "Blaze Campaigns"; + +/* Description for the Blaze dashboard card. */ +"blaze.dashboard.card.description" = "Display your work across millions of sites."; + +/* Title for a menu action in the context menu on the Blaze card. */ +"blaze.dashboard.card.menu.hide" = "Hide this"; + +/* Title for a menu action in the context menu on the Blaze card. */ +"blaze.dashboard.card.menu.learnMore" = "Learn more"; + +/* Title for the Blaze dashboard card. */ +"blaze.dashboard.card.title" = "Promote your content with Blaze"; + +/* Button title for a Blaze overlay prompting users to select a post to blaze. */ +"blaze.overlay.buttonTitle" = "Blaze a post now"; + +/* Description for the Blaze overlay. */ +"blaze.overlay.descriptionOne" = "Promote any post or page in only a few minutes for just a few dollars a day."; + +/* Description for the Blaze overlay. */ +"blaze.overlay.descriptionThree" = "Track your campaign's performance and cancel at anytime."; + +/* Description for the Blaze overlay. */ +"blaze.overlay.descriptionTwo" = "Your content will appear on millions of WordPress and Tumblr sites."; + +/* Title for the Blaze overlay. */ +"blaze.overlay.title" = "Drive more traffic to your site with Blaze"; + +/* Button title for the Blaze overlay prompting users to blaze the selected page. */ +"blaze.overlay.withPage.buttonTitle" = "Blaze this page"; + +/* Button title for the Blaze overlay prompting users to blaze the selected post. */ +"blaze.overlay.withPost.buttonTitle" = "Blaze this post"; + +/* Short status description */ +"blazeCampaign.status.active" = "Active"; + +/* Short status description */ +"blazeCampaign.status.approved" = "Approved"; + +/* Short status description */ +"blazeCampaign.status.canceled" = "Cancelled"; + +/* Short status description */ +"blazeCampaign.status.completed" = "Completed"; + +/* Short status description */ +"blazeCampaign.status.inmoderation" = "In Moderation"; + +/* Short status description */ +"blazeCampaign.status.processing" = "Processing"; + +/* Short status description */ +"blazeCampaign.status.rejected" = "Rejected"; + +/* Short status description */ +"blazeCampaign.status.scheduled" = "Scheduled"; + +/* Title for budget stats view */ +"blazeCampaigns.budget" = "Budget"; + +/* Title for impressions stats view */ +"blazeCampaigns.clicks" = "Clicks"; + +/* Title for impressions stats view */ +"blazeCampaigns.impressions" = "Impressions"; + +/* Title for the context menu action that hides the dashboard card. */ +"blogDashboard.contextMenu.hideThis" = "Hide this"; + /* Action shown in a bottom notice to dismiss it. */ "blogDashboard.dismiss" = "Dismiss"; +/* Context menu button title */ +"blogHeader.actionCopyURL" = "Copy URL"; + +/* Context menu button title */ +"blogHeader.actionVisitSite" = "Visit site"; + +/* Title for a button that, when tapped, shows more info about participating in Bloganuary. */ +"bloganuary.dashboard.card.button.learnMore" = "Learn more"; + +/* Short description for the Bloganuary event, shown right below the title. */ +"bloganuary.dashboard.card.description" = "For the month of January, blogging prompts will come from Bloganuary — our community challenge to build a blogging habit for the new year."; + +/* Title for the Bloganuary dashboard card while Bloganuary is running. */ +"bloganuary.dashboard.card.runningTitle" = "Bloganuary is here!"; + +/* Title for the Bloganuary dashboard card. */ +"bloganuary.dashboard.card.title" = "Bloganuary is coming!"; + +/* Title of a button that calls the user to enable the Blogging Prompts feature. */ +"bloganuary.learnMore.modal.button.promptsDisabled" = "Turn on blogging prompts"; + +/* Title of a button that will dismiss the Bloganuary modal when tapped. +Note that the word 'go' here should have a closer meaning to 'start' rather than 'move forward'. */ +"bloganuary.learnMore.modal.button.promptsEnabled" = "Let’s go!"; + +/* The second line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.description.second" = "Publish your response."; + +/* The third line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.description.third" = "Read other bloggers’ responses to get inspiration and make new connections."; + +/* The first line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.descriptions.first" = "Receive a new prompt to inspire you each day."; + +/* An additional piece of information shown in case the user has the Blogging Prompts feature disabled. */ +"bloganuary.learnMore.modal.footer.addition" = "To join Bloganuary you need to enable Blogging Prompts."; + +/* An informative excerpt shown in a subtler tone. */ +"bloganuary.learnMore.modal.footer.text" = "Bloganuary will use Daily Blogging Prompts to send you topics for the month of January."; + +/* The headline text of the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.headline" = "Join our month-long writing challenge"; + /* Verb. Dismisses the blogging prompt notification. */ "bloggingPrompt.pushNotification.customActionDescription.dismiss" = "Dismiss"; /* Used when displaying author of a plugin. */ "by %@" = "by %@"; +/* Option for users to rate a chat bot answer as helpful. */ +"chat.rateHelpful" = "Rate as helpful"; + /* Displayed in the confirmation alert when marking comment notifications as read. */ "comment" = "comment"; +/* Sentence fragment. +The full phrase is 'Comments on' followed by the title of the post on a separate line. */ +"comment.header.subText.commentThread" = "Comments on"; + +/* Provides a hint that the current screen displays a comment on a post. +The title of the post will be displayed below this text. +Example: Comment on + My First Post */ +"comment.header.subText.post" = "Comment on"; + +/* Provides a hint that the current screen displays a reply to a comment. +%1$@ is a placeholder for the comment author's name that's been replied to. +Example: Reply to Pamela Nguyen */ +"comment.header.subText.reply" = "Reply to %1$@"; + +/* Error message informing a user about an invalid password. */ +"common.reEnterPasswordMessage" = "The username or password stored in the app may be out of date. Please re-enter your password in the settings and try again."; + +/* An error message. */ +"common.unableToConnect" = "Unable to Connect"; + +/* Footnote for the privacy compliance popover. */ +"compliance.analytics.popover.footnote" = "These cookies allow us to optimise performance by collecting information on how users interact with our websites."; + +/* Save Button Title for the privacy compliance popover. */ +"compliance.analytics.popover.save.button" = "Save"; + +/* Settings Button Title for the privacy compliance popover. */ +"compliance.analytics.popover.settings.button" = "Go to Settings"; + +/* Subtitle for the privacy compliance popover. */ +"compliance.analytics.popover.subtitle" = "We process your personal data to optimise our website and marketing activities based on your consent and our legitimate interest."; + +/* Title for the privacy compliance popover. */ +"compliance.analytics.popover.title" = "Manage privacy"; + +/* Toggle Title for the privacy compliance popover. */ +"compliance.analytics.popover.toggle" = "Analytics"; + /* The menu item to select during a guided tour. */ "connections" = "connections"; @@ -9298,6 +9569,243 @@ translators: Block name. %s: The localized block name */ /* Customize Insights button title */ "customizeInsightsCell.tryItButton.title" = "Try it now"; +/* Title for an empty state view when no cards are displayed */ +"dasboard.emptyView.subtitle" = "Add cards that fit your needs to see information about your site."; + +/* Title for an empty state view when no cards are displayed */ +"dasboard.emptyView.title" = "No cards to display"; + +/* Personialize home tab button title */ +"dasboard.personalizeHomeButtonTitle" = "Personalise your home tab"; + +/* Title for a menu action in the context menu on the Jetpack Social dashboard card. */ +"dashboard.card.social.menu.hide" = "Hide this"; + +/* Title for the Jetpack Social dashboard card when the user has no social connections. */ +"dashboard.card.social.noconnections.title" = "Share across your social networks"; + +/* Title for the Jetpack Social dashboard card when the user has no social shares left. */ +"dashboard.card.social.noshares.title" = "You’re out of shares!"; + +/* Title for comments button on dashboard. */ +"dashboard.menu.comments" = "Comments"; + +/* Title for media button on dashboard. */ +"dashboard.menu.media" = "Media"; + +/* Title for more button on dashboard. */ +"dashboard.menu.more" = "More"; + +/* Title for pages button on dashboard. */ +"dashboard.menu.pages" = "Pages"; + +/* Title for posts button on dashboard. */ +"dashboard.menu.posts" = "Posts"; + +/* Title for stats button on dashboard. */ +"dashboard.menu.stats" = "Stats"; + +/* Title for the Activity Log dashboard card context menu item that navigates the user to the full Activity Logs screen. */ +"dashboardCard.ActivityLog.contextMenu.allActivity" = "All activity"; + +/* Title for the Activity Log dashboard card. */ +"dashboardCard.ActivityLog.title" = "Recent activity"; + +/* Title for an action that opens the full pages list. */ +"dashboardCard.Pages.contextMenu.allPages" = "All pages"; + +/* Title for the Pages dashboard card. */ +"dashboardCard.Pages.title" = "Pages"; + +/* Title for impressions stats view */ +"dashboardCard.blazeCampaigns.clicks" = "Clicks"; + +/* Title of a button that starts the campaign creation flow. */ +"dashboardCard.blazeCampaigns.createCampaignButton" = "Create campaign"; + +/* Title for impressions stats view */ +"dashboardCard.blazeCampaigns.impressions" = "Impressions"; + +/* Title for the Learn more button in the More menu. */ +"dashboardCard.blazeCampaigns.learnMore" = "Learn more"; + +/* Title for the card displaying blaze campaigns. */ +"dashboardCard.blazeCampaigns.title" = "Blaze campaign"; + +/* Title for the View All Campaigns button in the More menu */ +"dashboardCard.blazeCampaigns.viewAllCampaigns" = "View all campaigns"; + +/* Title of a button that starts the page creation flow. */ +"dashboardCard.pages.add.button.title" = "Add pages to your site"; + +/* Title of label marking a draft page */ +"dashboardCard.pages.cell.status.draft" = "Draft"; + +/* Title of label marking a published page */ +"dashboardCard.pages.cell.status.publish" = "Published"; + +/* Title of label marking a scheduled page */ +"dashboardCard.pages.cell.status.schedule" = "Scheduled"; + +/* Title of a button that starts the page creation flow. */ +"dashboardCard.pages.create.button.title" = "Create another page"; + +/* Title of a label that encourages the user to create a new page. */ +"dashboardCard.pages.create.description" = "Start with bespoke, mobile friendly layouts."; + +/* Title for the View stats button in the More menu */ +"dashboardCard.stats.viewStats" = "View stats"; + +/* Feature flags menu item */ +"debugMenu.featureFlags" = "Feature Flags"; + +/* General section title */ +"debugMenu.generalSectionTitle" = "General"; + +/* Remote config params debug menu footer explaining the meaning of a cell with a checkmark. */ +"debugMenu.remoteConfig.footer" = "Overridden parameters are denoted by a checkmark."; + +/* Hint for overriding remote config params */ +"debugMenu.remoteConfig.hint" = "Override the chosen param by defining a new value here."; + +/* Placeholder for overriding remote config params */ +"debugMenu.remoteConfig.placeholder" = "No remote or default value"; + +/* Remote Config debug menu title */ +"debugMenu.remoteConfig.title" = "Remote Config"; + +/* Remove current quick start tour menu item */ +"debugMenu.removeQuickStart" = "Remove Current Tour"; + +/* Title for a menu action in the context menu on the Jetpack install card. */ +"domain.dashboard.card.menu.hide" = "Hide this"; + +/* Search domain - Title for the Suggested domains screen */ +"domain.management.addDomain.search.title" = "Search for a domain"; + +/* Domain management buy domain card button title */ +"domain.management.buy.card.button.title" = "Get Domain"; + +/* Domain management buy domain card subtitle */ +"domain.management.buy.card.subtitle" = "Add a site later."; + +/* Domain management buy domain card title */ +"domain.management.buy.card.title" = "Just buy a domain"; + +/* The expired label of the domain card in All Domains screen. */ +"domain.management.card.expired.label" = "Expired"; + +/* The renews label of the domain card in All Domains screen. */ +"domain.management.card.renews.label" = "Renews"; + +/* The empty state button title in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.button.title" = "Find a domain"; + +/* The empty state description in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.description" = "Tap below to find your perfect domain."; + +/* The empty state title in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.title" = "You don't have any domains"; + +/* The empty state description in All Domains screen when an error occurs */ +"domain.management.error.empty.state.description" = "We encountered an error while loading your domains. Please contact support if the issue persists."; + +/* The empty state title in All Domains screen when an error occurs */ +"domain.management.error.empty.state.title" = "Something went wrong"; + +/* The empty state button title in All Domains screen when an error occurs */ +"domain.management.error.state.button.title" = "Try again"; + +/* The empty state description in All Domains screen when the user is offline */ +"domain.management.offline.empty.state.description" = "Please check your network connection and try again."; + +/* The empty state title in All Domains screen when the user is offline */ +"domain.management.offline.empty.state.title" = "No Internet Connection"; + +/* Domain management choose site card button title */ +"domain.management.purchase.footer" = "*A free domain for one year is included with all paid annual plans"; + +/* Domain management purchase domain screen title */ +"domain.management.purchase.subtitle" = "Don't worry, you can easily add a site later."; + +/* Domain management purchase domain screen title. */ +"domain.management.purchase.title" = "Choose how to use your domain"; + +/* The search bar title in All Domains screen. */ +"domain.management.search-bar.title" = "Search domains"; + +/* The empty state description in All Domains screen when the are no domains matching the search criteria */ +"domain.management.search.empty.state.description" = "We couldn't find any domains that match your search for '%@'"; + +/* The empty state title in All Domains screen when the are no domains matching the search criteria */ +"domain.management.search.empty.state.title" = "No Matching Domains Found"; + +/* Domain management choose site card button title */ +"domain.management.site.card.button.title" = "Choose Site"; + +/* Domain management choose site card subtitle */ +"domain.management.site.card.footer" = "Free domain for the first year*"; + +/* Domain management choose site card subtitle */ +"domain.management.site.card.subtitle" = "Use with a site you already started."; + +/* Domain management choose site card title */ +"domain.management.site.card.title" = "Existing WordPress.com site"; + +/* Domain Management Screen Title */ +"domain.management.title" = "All Domains"; + +/* Domain Purchase Completion footer */ +"domain.purchase.preview.footer" = "It may take up to 30 minutes for your custom domain to start working."; + +/* Domain Purchase Completion description (only for FREE domains). */ +"domain.purchase.preview.free.description" = "Next, we'll help you get it ready to be browsed."; + +/* Domain Purchase Completion description (only for PAID domains). */ +"domain.purchase.preview.paid.description" = "We’ve emailed your receipt. Next, we'll help you get it ready for everyone."; + +/* Reflects that site is live when domain purchase feature flag is ON. */ +"domain.purchase.preview.title" = "Kudos, your site is live!"; + +/* The 'Best Alternative' label under the domain name in 'Choose a domain' screen */ +"domain.suggestions.row.best-alternative" = "Best Alternative"; + +/* The text to display for paid domains on sale in 'Site Creation > Choose a domain' screen */ +"domain.suggestions.row.first-year" = "for the first year"; + +/* The text to display for free domains in 'Site Creation > Choose a domain' screen */ +"domain.suggestions.row.free" = "Free"; + +/* The text to display for paid domains that are free for the first year with the paid plan in 'Site Creation > Choose a domain' screen */ +"domain.suggestions.row.free-with-plan" = "Free for the first year with annual paid plans"; + +/* The 'Recommended' label under the domain name in 'Choose a domain' screen */ +"domain.suggestions.row.recommended" = "Recommended"; + +/* The 'Sale' label under the domain name in 'Choose a domain' screen */ +"domain.suggestions.row.sale" = "Sale"; + +/* The text to display for paid domains in 'Site Creation > Choose a domain' screen */ +"domain.suggestions.row.yearly" = "per year"; + +/* Title for the checkout screen. */ +"domains.checkout.title" = "Checkout"; + +/* Action shown in a bottom notice to dismiss it. */ +"domains.failure.dismiss" = "Dismiss"; + +/* Content show when the domain selection action fails. */ +"domains.failure.title" = "Sorry, the domain you are trying to add cannot be bought on the Jetpack app at this time."; + +/* Title for the screen where the user can choose how to use the domain they're end up purchasing. */ +"domains.purchase.choice.title" = "Purchase Domain"; + +/* Back button title that navigates back to the search domains screen. */ +"domains.search.backButton.title" = "Search"; + +/* Title of screen where user chooses a site to connect to their selected domain */ +"domains.sitePicker.title" = "Choose Site"; + /* No comment provided by engineer. */ "double-tap to change unit" = "double-tap to change unit"; @@ -9315,9 +9823,51 @@ translators: Block name. %s: The localized block name */ Site Address placeholder */ "example.com" = "example.com"; +/* Title for confirmation navigation bar button item */ +"externalMediaPicker.add" = "Add"; + +/* Bottom toolbar title in the selection mode */ +"externalMediaPicker.toolbarSelectItemsPrompt" = "Select Images"; + +/* Bottom toolbar title in the selection mode */ +"externalMediaPicker.toolbarViewSelected" = "View Selected (%@)"; + +/* Title of screen the displays the details of an advertisement campaign. */ +"feature.blaze.campaignDetails.title" = "Campaign Details"; + +/* Name of a feature that allows the user to promote their posts. */ +"feature.blaze.title" = "Blaze"; + /* Displayed in the confirmation alert when marking follow notifications as read. */ "follow" = "follow"; +/* Description for the Free to Paid plans dashboard card. */ +"freeToPaidPlans.dashboard.card.description" = "Get a free domain for the first year, remove ads on your site, and increase your storage."; + +/* Title for a menu action in the context menu on the Free to Paid plans dashboard card. */ +"freeToPaidPlans.dashboard.card.menu.hide" = "Hide this"; + +/* Title for the Free to Paid plans dashboard card. */ +"freeToPaidPlans.dashboard.card.shortTitle" = "Free domain with an annual plan"; + +/* Done button title on the domain purchase result screen. Closes the screen. */ +"freeToPaidPlans.resultView.done" = "Done"; + +/* Notice on the domain purchase result screen. Tells user how long it might take for their domain to be ready. */ +"freeToPaidPlans.resultView.notice" = "It may take up to 30 minutes for your domain to start working properly"; + +/* Sub-title for the domain purchase result screen. Tells user their domain is being set up. */ +"freeToPaidPlans.resultView.subtitle" = "Your new domain %@ is being set up."; + +/* Title for the domain purchase result screen. Tells user their domain was obtained. */ +"freeToPaidPlans.resultView.title" = "All ready to go!"; + +/* A generic error message for a footer view in a list with pagination */ +"general.pagingFooterView.errorMessage" = "An error occurred"; + +/* A footer retry button */ +"general.pagingFooterView.retry" = "Retry"; + /* Title for button that will open up the blogging reminders screen. */ "growAudienceCell.bloggingReminders.actionButton" = "Set up blogging reminders"; @@ -9381,9 +9931,6 @@ translators: Block name. %s: The localized block name */ /* (placeholder) Help the user enter a URL into the field */ "http:\/\/my-site-address (URL)" = "http:\/\/my-site-address (URL)"; -/* Label displayed on image media items. */ -"image" = "image"; - /* Sentence to justify why the app is asking permission from the user to use their camera. */ "infoplist.NSCameraUsageDescription" = "To take photos or videos to use in your posts."; @@ -9444,6 +9991,9 @@ translators: Block name. %s: The localized block name */ /* Title of a badge indicating when a feature in singular form will be removed. First argument is the feature name. Second argument is the number of days/weeks it will be removed in. Ex: Reader is moving in 2 weeks */ "jetpack.branding.badge_banner.moving_in.singular" = "%1$@ is moving in %2$@"; +/* Title of a badge or banner indicating that this feature will be moved in a few days. */ +"jetpack.branding.badge_banner.moving_in_days.plural" = "Moving to the Jetpack app in a few days."; + /* Title of a badge indicating that a feature in plural form will be removed soon. First argument is the feature name. Ex: Notifications are moving soon */ "jetpack.branding.badge_banner.moving_soon.plural" = "%@ are moving soon"; @@ -9468,18 +10018,42 @@ translators: Block name. %s: The localized block name */ /* Title of a button that navigates the user to the Jetpack app if installed, or to the app store. */ "jetpack.fullscreen.overlay.early.switch.title" = "Switch to the new Jetpack app"; +/* Title of a button that navigates the user to the Jetpack app if installed, or to the app store. */ +"jetpack.fullscreen.overlay.late.switch.title" = "Switch to the Jetpack app"; + /* Title of a button that displays a blog post in a web view. */ "jetpack.fullscreen.overlay.learnMore" = "Learn more at jetpack.com"; +/* Description of the Notifications feature. */ +"jetpack.fullscreen.overlay.newUsers.notifications.subtitle" = "Get notifications for new comments, likes, views, and more."; + +/* Description of the Reader feature. */ +"jetpack.fullscreen.overlay.newUsers.reader.subtitle" = "Find and follow your favourite sites and communities, and share you content."; + +/* Description of the Statistics feature. */ +"jetpack.fullscreen.overlay.newUsers.stats.subtitle" = "Watch your traffic grow with helpful insights and comprehensive stats."; + /* Name of the Statistics feature. */ "jetpack.fullscreen.overlay.newUsers.stats.title" = "Stats & Insights"; +/* Title of a screen that prompts the user to switch the Jetpack app. */ +"jetpack.fullscreen.overlay.newUsers.subtitle" = "Jetpack lets you do more with your WordPress site. Switching is free and only takes a minute."; + +/* Title of a screen that prompts the user to switch the Jetpack app. */ +"jetpack.fullscreen.overlay.newUsers.title" = "Give WordPress a boost with Jetpack"; + /* Title of a button that dismisses an overlay and displays the Notifications screen. */ "jetpack.fullscreen.overlay.notifications.continue.title" = "Continue to Notifications"; /* Title of a button that dismisses an overlay that prompts the user to switch the Jetpack app. */ "jetpack.fullscreen.overlay.phaseFour.general.continue.title" = "Do this later"; +/* Title of a screen that prompts the user to switch the Jetpack app. */ +"jetpack.fullscreen.overlay.phaseFour.subtitle" = "Stats, Reader, Notifications and other Jetpack powered features have been removed from the WordPress app."; + +/* Title of a screen that prompts the user to switch the Jetpack app. */ +"jetpack.fullscreen.overlay.phaseFour.title" = "Jetpack features have moved."; + /* Subtitle of a screen displayed when the user accesses the Notifications screen from the WordPress app. The screen showcases the Jetpack app. */ "jetpack.fullscreen.overlay.phaseOne.notifications.subtitle" = "Switch to the Jetpack app to keep receiving real-time notifications on your device."; @@ -9489,30 +10063,117 @@ translators: Block name. %s: The localized block name */ /* Subtitle of a screen displayed when the user accesses the Reader screen from the WordPress app. The screen showcases the Jetpack app. */ "jetpack.fullscreen.overlay.phaseOne.reader.subtitle" = "Switch to the Jetpack app to find, follow, and like all your favourite sites and posts with Reader."; +/* Title of a screen displayed when the user accesses the Reader screen from the WordPress app. The screen showcases the Jetpack app. */ +"jetpack.fullscreen.overlay.phaseOne.reader.title" = "Follow any site with the Jetpack app"; + +/* Subtitle of a screen displayed when the user trys creating a new site from the WordPress app. The screen showcases the Jetpack app. */ +"jetpack.fullscreen.overlay.phaseOne.siteCreation.subtitle" = "Jetpack provides stats, notifications and more to help you build and grow the WordPress site of your dreams."; + +/* Subtitle of a screen displayed when the user accesses the Stats screen from the WordPress app. The screen showcases the Jetpack app. */ +"jetpack.fullscreen.overlay.phaseOne.stats.subtitle" = "Switch to the Jetpack app to watch your site’s traffic grow with stats and insights."; + +/* Title of a screen displayed when the user accesses the Stats screen from the WordPress app. The screen showcases the Jetpack app. */ +"jetpack.fullscreen.overlay.phaseOne.stats.title" = "Get your stats using the new Jetpack app"; + +/* A footnote in a screen displayed when the user accesses a Jetpack powered feature from the WordPress app. The screen showcases the Jetpack app. */ +"jetpack.fullscreen.overlay.phaseThree.footnote" = "Switching is free and only takes a minute."; + +/* Title of a button that dismisses an overlay that showcases the Jetpack app. */ +"jetpack.fullscreen.overlay.phaseThree.general.continue.title" = "Continue without Jetpack"; + +/* Title of a screen that showcases the Jetpack app. */ +"jetpack.fullscreen.overlay.phaseThree.general.title" = "Jetpack features are moving soon."; + +/* Subtitle of a screen displayed when the user trys creating a new site from the WordPress app. The screen showcases the Jetpack app. */ +"jetpack.fullscreen.overlay.phaseTwo.siteCreation.subtitle" = "Jetpack provides stats, notifications and more to help you build and grow the WordPress site of your dreams.\n\nThe WordPress app no longer supports creating a new site."; + +/* Subtitle of a screen displayed when the user accesses a Jetpack-powered feature from the WordPress app. */ +"jetpack.fullscreen.overlay.phaseTwoAndThree.fallbackSubtitle" = "Stats, Reader, Notifications and other Jetpack powered features will be removed from the WordPress app soon."; + +/* Title of a screen displayed when the user accesses the Notifications screen from the WordPress app. The screen showcases the Jetpack app. */ +"jetpack.fullscreen.overlay.phaseTwoAndThree.notifications.title" = "Notifications are moving to Jetpack"; + +/* Title of a screen displayed when the user accesses the Reader screen from the WordPress app. The screen showcases the Jetpack app. */ +"jetpack.fullscreen.overlay.phaseTwoAndThree.reader.title" = "Reader is moving to the Jetpack app"; + +/* Title of a screen displayed when the user accesses the Stats screen from the WordPress app. The screen showcases the Jetpack app. */ +"jetpack.fullscreen.overlay.phaseTwoAndThree.stats.title" = "Stats are moving to the Jetpack app"; + +/* Subtitle of a screen displayed when the user accesses a Jetpack-powered feature from the WordPress app. The '%@' characters are a placeholder for the date the features will be removed. */ +"jetpack.fullscreen.overlay.phaseTwoAndThree.subtitle" = "Stats, Reader, Notifications and other Jetpack powered features will be removed from the WordPress app on %@."; + /* Title of a button that dismisses an overlay and displays the Reader screen. */ "jetpack.fullscreen.overlay.reader.continue.title" = "Continue to Reader"; +/* Title of a screen that prompts the user to switch the Jetpack app. */ +"jetpack.fullscreen.overlay.selfHosted.subtitle" = "The Jetpack mobile app is designed to work in companion with the Jetpack plugin. Switch now to get access to Stats, Reader, Notifications and more."; + +/* Title of a screen that prompts the user to switch the Jetpack app. */ +"jetpack.fullscreen.overlay.selfHosted.title" = "Your site has the Jetpack plugin"; + /* Title of a button that navigates the user to the Jetpack app if installed, or to the app store. */ "jetpack.fullscreen.overlay.siteCreation.continue.title" = "Continue without Jetpack"; /* Title of a button that navigates the user to the Jetpack app if installed, or to the app store. */ "jetpack.fullscreen.overlay.siteCreation.switch.title" = "Try the new Jetpack app"; +/* Title of a screen displayed when the user trys creating a new site from the WordPress app. The screen showcases the Jetpack app. */ +"jetpack.fullscreen.overlay.siteCreation.title" = "Create a new WordPress site with the Jetpack app"; + /* Title of a button that dismisses an overlay and displays the Stats screen. */ "jetpack.fullscreen.overlay.stats.continue.title" = "Continue to Stats"; +/* The description text shown after the user has successfully installed the Jetpack plugin. */ +"jetpack.install-flow.success.description" = "Ready to use this site with the app."; + +/* Title of the primary button shown after the Jetpack plugin has been installed. Tapping on the button dismisses the installation screen. */ +"jetpack.install-flow.success.primaryButtonText" = "Done"; + +/* Title of a button for connecting user account to Jetpack. */ +"jetpack.install.connectUser.button.title" = "Connect your user account"; + +/* Message asking the user if they want to set up Jetpack from notifications */ +"jetpack.install.connectUser.notifications.description" = "To get helpful notifications on your phone from your WordPress site, you'll need to connect to your user account."; + +/* Message asking the user if they want to set up Jetpack from stats by connecting their user account */ +"jetpack.install.connectUser.stats.description" = "To use stats on your site, you'll need to connect the Jetpack plugin to your user account."; + +/* Description inside a menu card communicating that features are moving to the Jetpack app. */ +"jetpack.menuCard.description" = "Stats, Reader, Notifications and other features will move to the Jetpack mobile app soon."; + /* Menu item title to hide the card. */ "jetpack.menuCard.hide" = "Hide this"; /* Title of a button that displays a blog post in a web view. */ "jetpack.menuCard.learnMore" = "Learn more"; +/* Description inside a menu card prompting users to switch to the Jetpack app. */ +"jetpack.menuCard.newUsers.title" = "Unlock your site’s full potential. Get Stats, Reader, Notifications and more with Jetpack."; + /* Title of a button prompting users to switch to the Jetpack app. */ "jetpack.menuCard.phaseFour.title" = "Switch to Jetpack"; /* Menu item title to hide the card for now and show it later. */ "jetpack.menuCard.remindLater" = "Remind me later"; +/* Jetpack Plugin Modal footnote */ +"jetpack.plugin.modal.footnote" = "By setting up Jetpack you agree to our %@"; + +/* Jetpack Plugin Modal primary button title */ +"jetpack.plugin.modal.primary.button.title" = "Install the full plugin"; + +/* Jetpack Plugin Modal secondary button title */ +"jetpack.plugin.modal.secondary.button.title" = "Contact Support"; + +/* The 'full Jetpack plugin' string in the subtitle */ +"jetpack.plugin.modal.subtitle.jetpack.plugin" = "full Jetpack plugin"; + +/* Jetpack Plugin Modal footnote terms and conditions */ +"jetpack.plugin.modal.terms" = "Terms and Conditions"; + +/* Jetpack Plugin Modal title */ +"jetpack.plugin.modal.title" = "Please install the full Jetpack plugin"; + /* Add an author prompt for the jetpack prologue */ "jetpack.prologue.prompt.addAuthor" = "Add an author"; @@ -9549,6 +10210,18 @@ translators: Block name. %s: The localized block name */ /* Write a blog prompt for the jetpack prologue */ "jetpack.prologue.prompt.writeBlog" = "Write a blog"; +/* Title for a call-to-action button on the Jetpack install card. */ +"jetpackinstallcard.button.learn" = "Learn more"; + +/* Title for a menu action in the context menu on the Jetpack install card. */ +"jetpackinstallcard.menu.hide" = "Hide this"; + +/* Text displayed in the Jetpack install card on the Home screen and Menu screen when a user has an individual Jetpack plugin installed but not the full plugin. %1$@ is a placeholder for the plugin the user has installed. %1$@ is bold. */ +"jetpackinstallcard.notice.individual" = "This site is using the %1$@ plugin, which doesn't support all features of the app yet. Please install the full Jetpack plugin."; + +/* Text displayed in the Jetpack install card on the Home screen and Menu screen when a user has multiple installed individual Jetpack plugins but not the full plugin. */ +"jetpackinstallcard.notice.multiple" = "This site is using individual Jetpack plugins, which don’t support all features of the app yet. Please install the full Jetpack plugin."; + /* Later today */ "later today" = "later today"; @@ -9558,42 +10231,207 @@ translators: Block name. %s: The localized block name */ /* Indicating that referrer was marked as spam */ "marked as spam" = "marked as spam"; -/* Verb. User action to dismiss error alert when failing to load media item. */ -"mediaItemTable.errorAlert.dismissButton" = "Dismiss"; +/* Products header text in Me Screen. */ +"me.products.header" = "Products"; -/* Verb. Button title. Tapping dismisses a prompt. */ +/* Title of error prompt shown when a sync fails. */ +"media.syncFailed" = "Unable to sync media"; + +/* An error message the app shows if media import fails */ +"mediaExporter.error.unknown" = "The item could not be added to the Media library"; + +/* An error message the app shows if media import fails */ +"mediaExporter.error.unsupportedContentType" = "Unsupported content type"; + +/* Message of an alert informing users that the video they are trying to select is not allowed. */ +"mediaExporter.videoLimitExceededError" = "Uploading videos longer than 5 minutes requires a paid plan."; + +/* Accessibility hint for add button to add items to the user's media library */ +"mediaLibrary.addButtonAccessibilityHint" = "Add new media"; + +/* Accessibility label for add button to add items to the user's media library */ +"mediaLibrary.addButtonAccessibilityLabel" = "Add"; + +/* Button name in the more menu */ +"mediaLibrary.aspectRatioGrid" = "Aspect Ratio Grid"; + +/* Context menu button */ +"mediaLibrary.buttonDelete" = "Delete"; + +/* Media screen navigation bar button Select title */ +"mediaLibrary.buttonSelect" = "Select"; + +/* Context menu button */ +"mediaLibrary.buttonShare" = "Share"; + +/* Verb. Button title. Tapping cancels an action. */ +"mediaLibrary.deleteConfirmationCancel" = "Cancel"; + +/* Title for button that permanently deletes one or more media items (photos / videos) */ +"mediaLibrary.deleteConfirmationConfirm" = "Delete"; + +/* Message prompting the user to confirm that they want to permanently delete a group of media items. */ +"mediaLibrary.deleteConfirmationMessageMany" = "Are you sure you want to permanently delete these items?"; + +/* Message prompting the user to confirm that they want to permanently delete a media item. Should match Calypso. */ +"mediaLibrary.deleteConfirmationMessageOne" = "Are you sure you want to permanently delete this item?"; + +/* Text displayed in HUD if there was an error attempting to delete a group of media items. */ +"mediaLibrary.deletionFailureMessage" = "Unable to delete all media items."; + +/* Text displayed in HUD while a media item is being deleted. */ +"mediaLibrary.deletionProgressViewTitle" = "Deleting..."; + +/* Text displayed in HUD after successfully deleting a media item */ +"mediaLibrary.deletionSuccessMessage" = "Deleted!"; + +/* The name of the media filter */ +"mediaLibrary.filterAll" = "All"; + +/* The name of the media filter */ +"mediaLibrary.filterAudio" = "Audio"; + +/* The name of the media filter */ +"mediaLibrary.filterDocuments" = "Documents"; + +/* The name of the media filter */ +"mediaLibrary.filterImages" = "Images"; + +/* The name of the media filter */ +"mediaLibrary.filterVideos" = "Videos"; + +/* User action to delete un-uploaded media. */ +"mediaLibrary.retryOptionsAlert.delete" = "Delete"; + +/* Verb. Button title. Tapping dismisses a prompt. */ "mediaLibrary.retryOptionsAlert.dismissButton" = "Dismiss"; +/* User action to retry media upload. */ +"mediaLibrary.retryOptionsAlert.retry" = "Retry Upload"; + +/* Message displayed when no results are returned from a media library search. Should match Calypso. */ +"mediaLibrary.searchResultsEmptyTitle" = "No media matching your search"; + +/* Text displayed in HUD if there was an error attempting to share a group of media items. */ +"mediaLibrary.sharingFailureMessage" = "Unable to share the selected items."; + +/* Button name in the more menu */ +"mediaLibrary.squareGrid" = "Square Grid"; + +/* Media screen navigation title */ +"mediaLibrary.title" = "Media"; + +/* Bottom toolbar title in the selection mode */ +"mediaLibrary.toolbarSelectImagesMany" = "%d Images Selected"; + +/* Bottom toolbar title in the selection mode */ +"mediaLibrary.toolbarSelectImagesOne" = "1 Image Selected"; + +/* Bottom toolbar title in the selection mode */ +"mediaLibrary.toolbarSelectItemsMany" = "%d Items Selected"; + +/* Bottom toolbar title in the selection mode */ +"mediaLibrary.toolbarSelectItemsOne" = "1 Item Selected"; + +/* Bottom toolbar title in the selection mode */ +"mediaLibrary.toolbarSelectItemsPrompt" = "Select Items"; + /* The title of the button to dismiss the alert shown when the picked media cannot be imported into stories. */ "mediaPicker.failedMediaExportAlert.dismissButton" = "Dismiss"; +/* Error message when picked media cannot be imported into stories. */ +"mediaPicker.failedMediaExportAlert.message" = "Your media could not be exported. If the problem persists you can contact us via the Me > Help & Support screen."; + +/* Error title when picked media cannot be imported into stories. */ +"mediaPicker.failedMediaExportAlert.title" = "Failed Media Export"; + +/* Message for alert when access to camera is not granted */ +"mediaPicker.noCameraAccessMessage" = "This app needs permission to access the Camera to capture new media, please change the privacy settings if you wish to allow this."; + +/* Title for alert when access to camera is not granted */ +"mediaPicker.noCameraAccessTitle" = "Media Capture"; + +/* Button that opens the Settings app */ +"mediaPicker.openSettings" = "Open Settings"; + +/* The name of the action in the context menu for selecting photos from Tenor (free GIF library) */ +"mediaPicker.pickFromFreeGIFLibrary" = "Free GIF Library"; + +/* The name of the action in the context menu (user's WordPress Media Library */ +"mediaPicker.pickFromMediaLibrary" = "Choose from Media"; + +/* The name of the action in the context menu for selecting photos from other apps (Files app) */ +"mediaPicker.pickFromOtherApps" = "Other Files"; + +/* The name of the action in the context menu */ +"mediaPicker.pickFromPhotosLibrary" = "Choose from Device"; + +/* The name of the action in the context menu for selecting photos from free stock photos */ +"mediaPicker.pickFromStockPhotos" = "Free Photo Library"; + +/* The name of the action in the context menu */ +"mediaPicker.takePhoto" = "Take Photo"; + +/* The name of the action in the context menu */ +"mediaPicker.takePhotoOrVideo" = "Take Photo or Video"; + +/* The name of the action in the context menu */ +"mediaPicker.takeVideo" = "Take Video"; + +/* Navigation title for media preview. Example: 1 of 3 */ +"mediaPreview.NofM" = "%1$@ of %2$@"; + +/* Max image size in pixels (e.g. 300x300px) */ +"mediaSizeSlider.valueFormat" = "%1$d × %2$d px"; + +/* The description in the Delete WordPress screen */ +"migration.deleteWordpress.description" = "It looks like you still have the WordPress app installed."; + /* The primary button title in the Delete WordPress screen */ "migration.deleteWordpress.primaryButton" = "Got it"; /* The secondary button title in the Delete WordPress screen */ "migration.deleteWordpress.secondaryButton" = "Need help?"; -/* Primary button title in the migration done screen. */ -"migration.done.actions.primary.title" = "Finish"; +/* The title in the Delete WordPress screen */ +"migration.deleteWordpress.title" = "You no longer need the WordPress app on your device"; + +/* Footer for the migration done screen. */ +"migration.done.footer" = "We recommend uninstalling the WordPress app on your device to avoid data conflicts."; + +/* Highlighted text in the footer of the migration done screen. */ +"migration.done.footer.highlighted" = "uninstalling the WordPress app"; /* Primary description in the migration done screen. */ "migration.done.primaryDescription" = "We’ve transferred all your data and settings. Everything is right where you left it."; +/* Secondary description (second paragraph) in the migration done screen. */ +"migration.done.secondaryDescription" = "It's time to continue your WordPress journey on the Jetpack app!"; + /* Title of the migration done screen. */ "migration.done.title" = "Thanks for switching to Jetpack!"; +/* The description in the Load WordPress screen */ +"migration.loadWordpress.description" = "It looks like you have the WordPress app installed."; + /* The primary button title in the Load WordPress screen */ "migration.loadWordpress.primaryButton" = "Open WordPress"; /* The secondary button title in the Load WordPress screen */ "migration.loadWordpress.secondaryButton" = "No thanks"; +/* The secondary description in the Load WordPress screen */ +"migration.loadWordpress.secondaryDescription" = "Would you like to transfer your data from the WordPress app and sign in automatically?"; + /* The title in the Load WordPress screen */ "migration.loadWordpress.title" = "Welcome to Jetpack!"; /* Secondary button title in the migration notifications screen. */ "migration.notifications.actions.secondary.title" = "Decide later"; +/* Footer for the migration notifications screen. */ +"migration.notifications.footer" = "When the alert appears tap Allow to continue receiving all your WordPress notifications."; + /* Highlighted text in the footer of the migration notifications screen. */ "migration.notifications.footer.highlighted" = "Allow"; @@ -9618,21 +10456,258 @@ translators: Block name. %s: The localized block name */ /* The title in the migration welcome screen */ "migration.welcome.title" = "Welcome to Jetpack!"; +/* Primary button title in the migration done screen. */ +"migrationDone.actions.primaryTitle" = "Let's go"; + +/* Description for the static screen displayed prompting users to switch the Jetpack app. */ +"movedToJetpack.description" = "The Jetpack app has all the WordPress app’s functionality, and now exclusive access to Stats, Reader, Notifications and more."; + +/* Hint for the static screen displayed prompting users to switch the Jetpack app. */ +"movedToJetpack.hint" = "Switching is free and only takes a minute."; + +/* Title for a button that prompts users to switch to the Jetpack app. */ +"movedToJetpack.jetpackButtonTitle" = "Switch to the Jetpack app"; + +/* Title for a button that displays a blog post in a web view. */ +"movedToJetpack.learnMoreButtonTitle" = "Learn more at jetpack.com"; + +/* Title for the static screen displayed in the Stats screen prompting users to switch to the Jetpack app. */ +"movedToJetpack.notifications.title" = "Use WordPress with Notifications in the Jetpack app."; + +/* Title for the static screen displayed in the Reader screen prompting users to switch to the Jetpack app. */ +"movedToJetpack.reader.title" = "Use WordPress with Reader in the Jetpack app."; + +/* Title for the static screen displayed in the Stats screen prompting users to switch to the Jetpack app. */ +"movedToJetpack.stats.title" = "Use WordPress with Stats in the Jetpack app."; + +/* Section title for the content table section in the blog details screen */ +"my-site.menu.content.section.title" = "Content"; + +/* Section title for the maintenance table section in the blog details screen */ +"my-site.menu.maintenance.section.title" = "Maintenance"; + +/* Title for the social row in the blog details screen */ +"my-site.menu.social.row.title" = "Social"; + +/* Section title for the traffic table section in the blog details screen */ +"my-site.menu.traffic.section.title" = "Traffic"; + +/* Title for the card displaying draft posts. */ +"my-sites.drafts.card.title" = "Work on a draft post"; + +/* Title for the View all drafts button in the More menu */ +"my-sites.drafts.card.viewAllDrafts" = "View all drafts"; + +/* Title for the View all scheduled drafts button in the More menu */ +"my-sites.scheduled.card.viewAllScheduledPosts" = "View all scheduled posts"; + +/* Title for the card displaying today's stats. */ +"my-sites.stats.card.title" = "Today's Stats"; + +/* Title for the domain focus card on My Site */ +"mySite.domain.focus.cardCell.title" = "News"; + +/* Button title of the domain focus card on My Site */ +"mySite.domain.focus.cardView.button.title" = "Transfer your domains"; + +/* Description of the domain focus card on My Site */ +"mySite.domain.focus.cardView.description" = "As you may know, Google Domains has been sold to Squarespace. Transfer your domains to WordPress.com now, and we'll pay all transfer fees plus an extra year of your domain registration."; + +/* Title of the domain focus card on My Site */ +"mySite.domain.focus.cardView.title" = "Reclaim your Google Domains"; + +/* Action sheet button title. Launches the flow to a add self-hosted site. */ +"mySite.noSites.actionSheet.addSelfHostedSite" = "Add self-hosted site"; + +/* Action sheet button title. Launches the flow to create a WordPress.com site. */ +"mySite.noSites.actionSheet.createWPComSite" = "Create WordPress.com site"; + +/* Button title. Displays the account and setting screen. */ +"mySite.noSites.button.accountAndSettings" = "Account and settings"; + +/* Button title. Displays a screen to add a new site when tapped. */ +"mySite.noSites.button.addNewSite" = "Add new site"; + +/* Message description for when a user has no sites. */ +"mySite.noSites.description" = "Create a new site for your business, magazine, or personal blog; or connect an existing WordPress installation."; + +/* Message title for when a user has no sites. */ +"mySite.noSites.title" = "You don't have any sites"; + +/* Menu title for the add site option */ +"mySite.siteActions.addSite" = "Add site"; + +/* Button that reveals more site actions */ +"mySite.siteActions.button" = "Site Actions"; + +/* Accessibility hint for button used to show more site actions */ +"mySite.siteActions.hint" = "Tap to show more site actions"; + +/* Menu title for the personalize home option */ +"mySite.siteActions.personalizeHome" = "Personalise home"; + +/* Menu title for the change site icon option */ +"mySite.siteActions.siteIcon" = "Change site icon"; + +/* Menu title for the change site title option */ +"mySite.siteActions.siteTitle" = "Change site title"; + +/* Menu title for the switch site option */ +"mySite.siteActions.switchSite" = "Switch site"; + +/* Menu title for the visit site option */ +"mySite.siteActions.visitSite" = "Visit site"; + /* Dismiss button title. */ "noResultsViewController.dismissButton" = "Dismiss"; -/* Word separating the current index from the total amount. I.e.: 7 of 9 */ -"of" = "of"; +/* This is one of the buttons we display inside of the prompt to review the app */ +"notifications.appRatings.prompt.no.buttonTitle" = "Could improve"; + +/* This is one of the buttons we display inside of the prompt to review the app */ +"notifications.appRatings.prompt.yes.buttonTitle" = "I like it"; + +/* This is one of the buttons we display when prompting the user for a review */ +"notifications.appRatings.sendFeedback.no.buttonTitle" = "No thanks"; + +/* This is one of the buttons we display when prompting the user for a review */ +"notifications.appRatings.sendFeedback.yes.buttonTitle" = "Send feedback"; + +/* Badge for page cells */ +"pageList.badgeHomepage" = "Homepage"; + +/* Badge for page cells */ +"pageList.badgeLocalChanges" = "Local changes"; + +/* Badge for page cells */ +"pageList.badgePendingReview" = "Pending review"; + +/* Badge for page cells */ +"pageList.badgePosts" = "Posts page"; + +/* Badge for page cells */ +"pageList.badgePrivate" = "Private"; + +/* Subtitle of the theme template homepage cell */ +"pages.template.subtitle" = "Your homepage is using a Theme template and will open in the web editor."; -/* Label displayed on media items that are not video, image, or audio. */ -"other" = "other"; +/* Title of the theme template homepage cell */ +"pages.template.title" = "Homepage"; + +/* Message informing the user that their static homepage page was set successfully */ +"pages.updatePage.successTitle" = "Page successfully updated"; + +/* Delete option in the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.actionTitle" = "Delete Permanently"; + +/* Message of the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.alertMessage" = "Are you sure you want to permanently delete this page?"; + +/* Title of the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.alertTitle" = "Delete Permanently?"; + +/* Menu option for filtering posts by everyone */ +"pagesList.pagesByEveryone" = "Pages by everyone"; + +/* Menu option for filtering posts by me */ +"pagesList.pagesByMe" = "Pages by me"; + +/* Trash option in the trash page confirmation alert. */ +"pagesList.trash.actionTitle" = "Move to Trash"; + +/* Message of the trash page confirmation alert. */ +"pagesList.trash.alertMessage" = "Are you sure you want to trash this page?"; + +/* Title of the trash page confirmation alert. */ +"pagesList.trash.alertTitle" = "Trash this page?"; + +/* Cancels an Action */ +"pagesList.trash.cancel" = "Cancel"; /* No comment provided by engineer. */ "password" = "password"; +/* Section footer displayed below the list of toggles */ +"personalizeHome.cardsSectionFooter" = "Cards may show different content depending on what's happening on your site. We're working on more cards and controls."; + +/* Section header */ +"personalizeHome.cardsSectionHeader" = "Show or hide cards"; + +/* Card title for the pesonalization menu */ +"personalizeHome.dashboardCard.activityLog" = "Recent activity"; + +/* Card title for the pesonalization menu */ +"personalizeHome.dashboardCard.blaze" = "Blaze"; + +/* Card title for the pesonalization menu */ +"personalizeHome.dashboardCard.draftPosts" = "Draft posts"; + +/* Card title for the pesonalization menu */ +"personalizeHome.dashboardCard.pages" = "Pages"; + +/* Card title for the pesonalization menu */ +"personalizeHome.dashboardCard.prompts" = "Blogging prompts"; + +/* Card title for the pesonalization menu */ +"personalizeHome.dashboardCard.scheduledPosts" = "Scheduled posts"; + +/* Card title for the pesonalization menu */ +"personalizeHome.dashboardCard.todaysStats" = "Today's stats"; + +/* Section header for shortcuts */ +"personalizeHome.shortcutsSectionHeader" = "Show or hide shortcuts"; + +/* Page title */ +"personalizeHome.title" = "Personalise Home Tab"; + /* Register Domain - Domain contact information field Phone */ "phone number" = "phone number"; +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.createdTimeAgo" = "Created %@"; + +/* Status mesasge for post cells */ +"post.deletingPostPermanentlyStatusMessage" = "Deleting post..."; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.editedTimeAgo" = "Edited %@"; + +/* Status mesasge for post cells */ +"post.movingToTrashStatusMessage" = "Moving post to trash..."; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.publishedTimeAgo" = "Published %@"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.scheduledForDate" = "Scheduled %@"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.trashedTimeAgo" = "Trashed %@"; + +/* Accessibility label for the post author in the post list. The parameter is the author name. For example, \"By Elsa.\" */ +"postList.a11y.authorChunkFormat" = "By %@."; + +/* Accessibility label for a post's excerpt in the post list. The parameter is the post excerpt. For example, \"Excerpt. This is the first paragraph.\" */ +"postList.a11y.exerptChunkFormat" = "Excerpt. %@."; + +/* Accessibility label for a sticky post in the post list. */ +"postList.a11y.sticky" = "Sticky."; + +/* Accessibility label for a post in the post list. The first placeholder is the post title. The second placeholder is the date. */ +"postList.a11y.titleAndDateChunkFormat" = "%1$@, %2$@."; + +/* Title for the 'Trash' post list row swipe action */ +"postList.swipeActionDelete" = "Trash"; + +/* Title for the 'Delete' post list row swipe action */ +"postList.swipeActionDeletePermanently" = "Delete"; + +/* Title for the 'Share' post list row swipe action */ +"postList.swipeActionShare" = "Share"; + +/* Title for the 'View' post list row swipe action */ +"postList.swipeActionView" = "View"; + /* User action to dismiss featured media options. */ "postSettings.featuredImageUploadActionSheet.dismiss" = "Dismiss"; @@ -9645,15 +10720,273 @@ translators: Block name. %s: The localized block name */ /* Title for action sheet with featured media options. */ "postSettings.featuredImageUploadActionSheet.title" = "Featured Image Options"; +/* Section title for the disabled Twitter service in the Post Settings screen */ +"postSettings.section.disabledTwitter.header" = "Twitter Auto-Sharing Is No Longer Available"; + +/* Button in Post Settings */ +"postSettings.setFeaturedImageButton" = "Set Featured Image"; + +/* Error message on post/page settings screen */ +"postSettings.updateFailedMessage" = "Failed to update the post settings"; + +/* Promote the post with Blaze. */ +"posts.blaze.actionTitle" = "Promote with Blaze"; + +/* Label for the Post List option that cancels automatic uploading of a post. */ +"posts.cancelUpload.actionTitle" = "Cancel upload"; + +/* Label for post comments option. Tapping displays comments for a post. */ +"posts.comments.actionTitle" = "Comments"; + +/* Label for the delete post option. Tapping permanently deletes a post. */ +"posts.delete.actionTitle" = "Delete permanently"; + +/* Label for an option that moves a post to the draft folder */ +"posts.draft.actionTitle" = "Move to draft"; + +/* Label for post duplicate option. Tapping creates a copy of the post. */ +"posts.duplicate.actionTitle" = "Duplicate"; + +/* Opens a submenu for page attributes. */ +"posts.pageAttributes.actionTitle" = "Page attributes"; + +/* Label for the preview post button. Tapping displays the post as it appears on the web. */ +"posts.preview.actionTitle" = "Preview"; + +/* Label for an option that moves a publishes a post immediately */ +"posts.publish.actionTitle" = "Publish now"; + +/* Retry uploading the post. */ +"posts.retry.actionTitle" = "Retry"; + +/* Set the selected page as the homepage. */ +"posts.setHomepage.actionTitle" = "Set as homepage"; + +/* Set the parent page for the selected page. */ +"posts.setParent.actionTitle" = "Set parent"; + +/* Set the selected page as a posts page. */ +"posts.setPostsPage.actionTitle" = "Set as posts page"; + +/* Set the selected page as a regular page. */ +"posts.setRegularPage.actionTitle" = "Set as regular page"; + +/* Label for post settings option. Tapping displays settings for a post. */ +"posts.settings.actionTitle" = "Settings"; + +/* Share the post. */ +"posts.share.actionTitle" = "Share"; + +/* Label for post stats option. Tapping displays statistics for a post. */ +"posts.stats.actionTitle" = "Stats"; + +/* Label for a option that moves a post to the trash folder */ +"posts.trash.actionTitle" = "Move to trash"; + +/* Label for the view post button. Tapping displays the post as it appears on the web. */ +"posts.view.actionTitle" = "View"; + +/* A short message explaining that a page was deleted permanently. */ +"postsList.deletePage.message" = "Page deleted permanently"; + +/* A short message explaining that a post was deleted permanently. */ +"postsList.deletePost.message" = "Post deleted permanently"; + +/* A short message explaining that a page was moved to the trash bin. */ +"postsList.movePageToTrash.message" = "Page moved to trash"; + +/* A short message explaining that a post was moved to the trash bin. */ +"postsList.movePostToTrash.message" = "Post moved to trash"; + +/* Menu option for filtering posts by everyone */ +"postsList.postsByEveryone" = "Posts by everyone"; + +/* Menu option for filtering posts by me */ +"postsList.postsByMe" = "Posts by me"; + +/* Title for the button to subscribe to Jetpack Social on the remaining shares view */ +"postsettings.social.remainingshares.subscribe" = "Subscribe now to share more"; + +/* The second half of the remaining social shares a user has. This is only displayed when there is no social limit warning. */ +"postsettings.social.remainingshares.text.part" = " in the next 30 days"; + +/* Beginning text of the remaining social shares a user has left. %1$d is their current remaining shares. This text is combined with ' in the next 30 days' if there is no warning displayed. */ +"postsettings.social.shares.text.format" = "%1$d social shares remaining"; + +/* The primary label for the auto-sharing row on the pre-publishing sheet. +Indicates the number of social accounts that will be sharing the blog post. +%1$d is a placeholder for the number of social network accounts that will be auto-shared. +Example: Sharing to 3 accounts */ +"prepublishing.social.label.multipleConnections" = "Sharing to %1$d accounts"; + +/* The primary label for the auto-sharing row on the pre-publishing sheet. +Indicates the blog post will not be shared to any social accounts. */ +"prepublishing.social.label.notSharing" = "Not sharing to social"; + +/* The primary label for the auto-sharing row on the pre-publishing sheet. +Indicates the number of social accounts that will be sharing the blog post. +This string is displayed when some of the social accounts are turned off for auto-sharing. +%1$d is a placeholder for the number of social media accounts that will be sharing the blog post. +%2$d is a placeholder for the total number of social media accounts connected to the user's blog. +Example: Sharing to 2 of 3 accounts */ +"prepublishing.social.label.partialConnections" = "Sharing to %1$d of %2$d accounts"; + +/* The primary label for the auto-sharing row on the pre-publishing sheet. +Indicates the blog post will be shared to a social media account. +%1$@ is a placeholder for the account name. +Example: Sharing to @wordpress */ +"prepublishing.social.label.singleConnection" = "Sharing to %1$@"; + +/* A subtext that's shown below the primary label in the auto-sharing row on the pre-publishing sheet. +Informs the remaining limit for post auto-sharing. +%1$d is a placeholder for the remaining shares. +Example: 27 social shares remaining */ +"prepublishing.social.remainingShares.format" = "%1$d social shares remaining"; + +/* a VoiceOver description for the warning icon to hint that the remaining shares are low. */ +"prepublishing.social.warningIcon.accessibilityHint" = "Warning"; + +/* The navigation title for a screen that edits the sharing message for the post. */ +"prepublishing.socialAccounts.editMessage.navigationTitle" = "Customise message"; + +/* The label for a call-to-action button in the social accounts' footer section. */ +"prepublishing.socialAccounts.footer.button.text" = "Subscribe to share more"; + +/* Text shown below the list of social accounts to indicate how many social shares available for the site. +Note that the '30 days' part is intended to be a static value. +%1$d is a placeholder for the amount of remaining shares. +Example: 27 social shares remaining in the next 30 days */ +"prepublishing.socialAccounts.footer.remainingShares.text" = "%1$d social shares remaining in the next 30 days"; + +/* a VoiceOver description for the warning icon to hint that the remaining shares are low. */ +"prepublishing.socialAccounts.footer.warningIcon.accessibilityHint" = "Warning"; + +/* The label displayed for a table row that displays the sharing message for the post. +Tapping on this row allows the user to edit the sharing message. */ +"prepublishing.socialAccounts.message.label" = "Message"; + +/* The navigation title for the pre-publishing social accounts screen. */ +"prepublishing.socialAccounts.navigationTitle" = "Social"; + +/* Title for a tappable string that opens the reader with a prompts tag */ +"prompts.card.viewprompts.title" = "View all responses"; + +/* Subtitle of the notification when prompts are hidden from the dashboard card */ +"prompts.notification.removed.subtitle" = "Visit Site Settings to turn back on"; + /* Title of the notification when prompts are hidden from the dashboard card */ "prompts.notification.removed.title" = "Blogging Prompts hidden"; /* Button label that dismisses the qr log in flow and returns the user back to the previous screen */ "qrLoginVerifyAuthorization.completedInstructions.dismiss" = "Dismiss"; +/* Subtitle instructing the user to tap the dismiss button to leave the log in flow. %@ is a placeholder for the dismiss button name. */ +"qrLoginVerifyAuthorization.completedInstructions.subtitle" = "Tap '%@' and head back to your web browser to continue."; + /* Title for the success view when the user has successfully logged in */ "qrLoginVerifyAuthorization.completedInstructions.title" = "You're logged in!"; +/* The quick tour actions item to select during a guided tour. */ +"quickStart.moreMenu" = "More"; + +/* Accessibility hint to inform that the author section can be tapped to see posts from the site. */ +"reader.detail.header.authorInfo.a11y.hint" = "Views posts from the site"; + +/* Title for the Comment button on the Reader Detail toolbar. +Note: Since the display space is limited, a short or concise translation is preferred. */ +"reader.detail.toolbar.comment.button" = "Comment"; + +/* Title for the Like button in the Reader Detail toolbar. +This is shown when the user has not liked the post yet. +Note: Since the display space is limited, a short or concise translation is preferred. */ +"reader.detail.toolbar.like.button" = "Like"; + +/* Accessibility hint for the Like button state. The button shows that the user has not liked the post, +but tapping on this button will add a Like to the post. */ +"reader.detail.toolbar.like.button.a11y.hint" = "Likes this post."; + +/* Title for the Like button in the Reader Detail toolbar. +This is shown when the user has already liked the post. +Note: Since the display space is limited, a short or concise translation is preferred. */ +"reader.detail.toolbar.liked.button" = "Liked"; + +/* Accessibility hint for the Liked button state. The button shows that the user has liked the post, +but tapping on this button will remove their like from the post. */ +"reader.detail.toolbar.liked.button.a11y.hint" = "Unlikes this post."; + +/* Accessibility hint for the 'Save Post' button. */ +"reader.detail.toolbar.save.button.a11y.hint" = "Saves this post for later."; + +/* Accessibility label for the 'Save Post' button. */ +"reader.detail.toolbar.save.button.a11y.label" = "Save post"; + +/* Accessibility hint for the 'Save Post' button when a post is already saved. */ +"reader.detail.toolbar.saved.button.a11y.hint" = "Unsaves this post."; + +/* Accessibility label for the 'Save Post' button when a post has been saved. */ +"reader.detail.toolbar.saved.button.a11y.label" = "Saved Post"; + +/* Reader search button accessibility label. */ +"reader.navigation.search.button.label" = "Search"; + +/* Reader settings button accessibility label. */ +"reader.navigation.settings.button.label" = "Reader Settings"; + +/* A default value used to fill in the site name when the followed site somehow has missing site name or URL. +Example: given a notice format "Following %@" and empty site name, this will be "Following this site". */ +"reader.notice.follow.site.unknown" = "this site"; + +/* Notice title when blocking a user fails. */ +"reader.notice.user.blocked" = "reader.notice.user.block.failed"; + +/* Text for the 'Comment' button on the reader post card cell. */ +"reader.post.button.comment" = "Comment"; + +/* Accessibility hint for the comment button on the reader post card cell */ +"reader.post.button.comment.accessibility.hint" = "Opens the comments for the post."; + +/* Text for the 'Like' button on the reader post card cell. */ +"reader.post.button.like" = "Like"; + +/* Accessibility hint for the like button on the reader post card cell */ +"reader.post.button.like.accessibility.hint" = "Likes the post."; + +/* Text for the 'Liked' button on the reader post card cell. */ +"reader.post.button.liked" = "Liked"; + +/* Accessibility hint for the liked button on the reader post card cell */ +"reader.post.button.liked.accessibility.hint" = "Unlikes the post."; + +/* Accessibility hint for the site header on the reader post card cell */ +"reader.post.button.menu.accessibility.hint" = "Opens a menu with more actions."; + +/* Accessibility label for the more menu button on the reader post card cell */ +"reader.post.button.menu.accessibility.label" = "More"; + +/* Text for the 'Reblog' button on the reader post card cell. */ +"reader.post.button.reblog" = "Reblog"; + +/* Accessibility hint for the reblog button on the reader post card cell */ +"reader.post.button.reblog.accessibility.hint" = "Reblogs the post."; + +/* Accessibility hint for the site header on the reader post card cell */ +"reader.post.header.accessibility.hint" = "Opens the site details for the post."; + +/* The title of a button that triggers blocking a user from the user's reader. */ +"reader.post.menu.block.user" = "Block this user"; + +/* The title of a button that removes a saved post. */ +"reader.post.menu.remove.post" = "Remove Saved Post"; + +/* The title of a button that triggers the reporting of a post's author. */ +"reader.post.menu.report.user" = "Report this user"; + +/* The title of a button that saves a post. */ +"reader.post.menu.save.post" = "Save"; + +/* The formatted number of posts and followers for a site. '%1$@' is a placeholder for the site post count. '%2$@' is a placeholder for the site follower count. Example: `5,000 posts • 10M followers` */ +"reader.site.header.counts" = "%1$@ posts • %2$@ followers"; + /* Spoken accessibility label */ "readerDetail.backButton.accessibilityLabel" = "Back"; @@ -9663,6 +10996,9 @@ translators: Block name. %s: The localized block name */ /* Button title for the follow conversations tooltip. */ "readerDetail.followConversationTooltipButton.accessibilityLabel" = "Got it"; +/* Message for the follow conversations tooltip. */ +"readerDetail.followConversationTooltipMessage.accessibilityLabel" = "Get notified when new comments are added to this post."; + /* Title of follow conversations tooltip. */ "readerDetail.followConversationTooltipTitle.accessibilityLabel" = "Follow the conversation"; @@ -9684,6 +11020,54 @@ translators: Block name. %s: The localized block name */ /* Title for the tooltip anchor. */ "readerDetail.tooltipAnchorTitle.accessibilityLabel" = "New"; +/* The button title for the transfer footer view in Register Domain screen */ +"register.domain.transfer.button.title" = "Transfer domain"; + +/* The title for the transfer footer view in Register Domain screen */ +"register.domain.transfer.title" = "Looking to transfer a domain you already own?"; + +/* Information of what related post are and how they are presented */ +"relatedPostsSettings.optionsFooter" = "Related Posts displays relevant content from your site below your posts."; + +/* Text for related post cell preview */ +"relatedPostsSettings.preview1.details" = "in \"Mobile\""; + +/* Text for related post cell preview */ +"relatedPostsSettings.preview1.title" = "Big iPhone\/iPad Update Now Available"; + +/* Text for related post cell preview */ +"relatedPostsSettings.preview2.details" = "in \"Apps\""; + +/* Text for related post cell preview */ +"relatedPostsSettings.preview2.title" = "The WordPress for Android App Gets a Big Facelift"; + +/* Text for related post cell preview */ +"relatedPostsSettings.preview3.details" = "in \"Upgrade\""; + +/* Text for related post cell preview */ +"relatedPostsSettings.preview3.title" = "Upgrade Focus: VideoPress For Weddings"; + +/* Section title for related posts section preview */ +"relatedPostsSettings.previewsHeaders" = "Preview"; + +/* Label for Related Post header preview */ +"relatedPostsSettings.relatedPostsHeader" = "Related Posts"; + +/* Message to show when setting save failed */ +"relatedPostsSettings.settingsUpdateFailed" = "Settings update failed"; + +/* Label for configuration switch to show/hide the header for the related posts section */ +"relatedPostsSettings.showHeader" = "Show Header"; + +/* Label for configuration switch to enable/disable related posts */ +"relatedPostsSettings.showRelatedPosts" = "Show Related Posts"; + +/* Label for configuration switch to show/hide images thumbnail for the related posts */ +"relatedPostsSettings.showThumbnail" = "Show Images"; + +/* Title for screen that allows configuration of your blog/site related posts settings. */ +"relatedPostsSettings.title" = "Related Posts"; + /* User action to dismiss media options. */ "shareExtension.editor.attachmentActions.dismiss" = "Dismiss"; @@ -9699,24 +11083,168 @@ translators: Block name. %s: The localized block name */ /* Share extension error dialog cancel button label. */ "shareModularViewController.retryAlert.dismiss" = "Dismiss"; +/* Share extension error dialog text. */ +"shareModularViewController.retryAlert.message" = "Whoops, something went wrong while sharing. You can try again, maybe it was a glitch."; + /* Share extension error dialog title. */ "shareModularViewController.retryAlert.title" = "Sharing Error"; +/* Template site address for the search bar. */ +"site.cration.domain.site.address" = "https:\/\/yoursitename.com"; + +/* The error message to show in the 'Site Creation > Assembly Step' when the domain checkout fails for unknown reasons. */ +"site.creation.assembly.step.domain.checkout.error.subtitle" = "Your website has been created successfully, but we encountered an issue while preparing your custom domain for checkout. Please try again or contact support for assistance."; + +/* Site name description that sits in the template website view. */ +"site.creation.domain.tooltip.description" = "Like the example above, a domain allows people to find and visit your site from their web browser."; + +/* Site name that is placed in the tooltip view. */ +"site.creation.domain.tooltip.site.name" = "YourSiteName.com"; + +/* Back button title shown in Site Creation flow to come back from Plan selection to Domain selection */ +"siteCreation.domain.backButton.title" = "Domains"; + +/* Button to progress to the next step after selecting domain in Site Creation */ +"siteCreation.domains.buttons.selectDomain" = "Select domain"; + +/* Accessibility label for audio items in the media collection view. The parameter is the creation date of the audio. */ +"siteMedia.accessibilityLabelAudio" = "Audio, %@"; + +/* Accessibility label for other media items in the media collection view. The parameter is the filename file. */ +"siteMedia.accessibilityLabelDocument" = "Document, %@"; + +/* Accessibility label for image thumbnails in the media collection view. The parameter is the creation date of the image. */ +"siteMedia.accessibilityLabelImage" = "Image, %@"; + +/* Accessibility label for video thumbnails in the media collection view. The parameter is the creation date of the video. */ +"siteMedia.accessibilityLabelVideo" = "Video, %@"; + +/* Accessibility label to use when creation date from media asset is not know. */ +"siteMedia.accessibilityUnknownCreationDate" = "Unknown creation date"; + +/* Accessibility hint for actions when displaying media items. */ +"siteMedia.cellAccessibilityHint" = "Select media."; + +/* Accessibility hint for media item preview for user's viewing an item in their media library */ +"siteMediaItem.contentViewAccessibilityHint" = "Tap to view media in full screen"; + +/* Accessibility label for media item preview for user's viewing an item in their media library */ +"siteMediaItem.contentViewAccessibilityLabel" = "Preview media"; + +/* Title for confirmation navigation bar button item */ +"siteMediaPicker.add" = "Add"; + +/* Button selection media in media picker */ +"siteMediaPicker.deselect" = "Deselect"; + +/* Button selection media in media picker */ +"siteMediaPicker.select" = "Select"; + +/* Media screen navigation title */ +"siteMediaPicker.title" = "Media"; + +/* Title for screen to select the privacy options for a blog */ +"siteSettings.privacy.title" = "Privacy"; + +/* Hint for users when hidden privacy setting is set */ +"siteVisibility.hidden.hint" = "Your site is hidden from visitors behind a \"Coming Soon\" notice until it is ready for viewing."; + +/* Text for privacy settings: Hidden */ +"siteVisibility.hidden.title" = "Hidden"; + +/* Hint for users when private privacy setting is set */ +"siteVisibility.private.hint" = "Your site is only visible to you and users you approve."; + +/* Text for privacy settings: Private */ +"siteVisibility.private.title" = "Private"; + +/* Hint for users when public privacy setting is set */ +"siteVisibility.public.hint" = "Your site is visible to everyone, and it may be indexed by search engines."; + +/* Text for privacy settings: Public */ +"siteVisibility.public.title" = "Public"; + +/* Text for unknown privacy setting */ +"siteVisibility.unknown.hint" = "Unknown"; + +/* Text for unknown privacy setting */ +"siteVisibility.unknown.title" = "Unknown"; + /* Label for the blogging reminders setting */ "sitesettings.reminders.title" = "Reminders"; +/* Body text for the Jetpack Social no connection view */ +"social.noconnection.body" = "Increase your traffic by auto-sharing your posts with your friends on social media."; + +/* Title for the connect button to add social sharing for the Jetpack Social no connection view */ +"social.noconnection.connectAccounts" = "Connect accounts"; + +/* Accessibility label for the social media icons in the Jetpack Social no connection view */ +"social.noconnection.icons.accessibility.label" = "Social media icons"; + +/* Title for the not now button to hide the Jetpack Social no connection view */ +"social.noconnection.notnow" = "Not now"; + +/* Plural body text for the Jetpack Social no shares dashboard card. %1$d is the number of social accounts the user has. */ +"social.noshares.body.plural" = "Your posts won’t be shared to your %1$d social accounts."; + +/* Singular body text for the Jetpack Social no shares dashboard card. */ +"social.noshares.body.singular" = "Your posts won’t be shared to your social account."; + +/* Accessibility label for the social media icons in the Jetpack Social no shares dashboard card */ +"social.noshares.icons.accessibility.label" = "Social media icons"; + +/* Title for the button to subscribe to Jetpack Social on the no shares dashboard card */ +"social.noshares.subscribe" = "Subscribe to share more"; + +/* Section title for the disabled Twitter service in the Social screen */ +"social.section.disabledTwitter.header" = "Twitter Auto-Sharing Is No Longer Available"; + +/* Text for a hyperlink that allows the user to learn more about the Twitter deprecation. */ +"social.twitterDeprecation.link" = "Find out more"; + +/* A smallprint that hints the reason behind why Twitter is deprecated. */ +"social.twitterDeprecation.text" = "Twitter auto-sharing is no longer available due to Twitter's changes in terms and pricing."; + /* Accessibility label used for distinguishing Views and Visitors in the Stats → Insights Views Visitors Line chart. */ "stats.insights.accessibility.label.viewsVisitorsLastDays" = "Last 7-days"; /* Accessibility label used for distinguishing Views and Visitors in the Stats → Insights Views Visitors Line chart. */ "stats.insights.accessibility.label.viewsVisitorsPreviousDays" = "Previous 7-days"; +/* Label shown on some metrics in the Stats Insights section, such as Comments count. The placeholders will be populated with a change and a percentage – e.g. '+17 (40%) higher than the previous 7-days'. The *s mark the numerical values, which will be highlighted differently from the rest of the text. */ +"stats.insights.label.totalLikes.higher" = "*%1$@%2$@ (%3$@%%)* higher than the previous 7-days"; + +/* Label shown on some metrics in the Stats Insights section, such as Comments count. The placeholders will be populated with a change and a percentage – e.g. '-17 (40%) lower than the previous 7-days'. The *s mark the numerical values, which will be highlighted differently from the rest of the text. */ +"stats.insights.label.totalLikes.lower" = "*%1$@%2$@ (%3$@%%)* lower than the previous 7-days"; + +/* Label shown in Stats Insights when a metric is showing the same level as the previous 7 days */ +"stats.insights.label.totalLikes.same" = "The same as the previous 7-days"; + +/* Stats insights views higher than previous 7 days */ +"stats.insights.label.views.sevenDays.higher" = "Your views in the last 7-days are %@ higher than the previous 7-days.\n"; + +/* Stats insights views lower than previous 7 days */ +"stats.insights.label.views.sevenDays.lower" = "Your views in the last 7-days are %@ lower than the previous 7-days.\n"; + +/* Stats insights label shown when the user's view count is the same as the previous 7 days. */ +"stats.insights.label.views.sevenDays.same" = "Your views in the last 7-days are the same as the previous 7-days.\n"; + /* Last 7-days legend label */ "stats.insights.label.viewsVisitorsLastDays" = "Last 7-days"; /* Previous 7-days legend label */ "stats.insights.label.viewsVisitorsPreviousDays" = "Previous 7-days"; +/* Stats insights visitors higher than previous 7 days */ +"stats.insights.label.visitors.sevenDays.higher" = "Your visitors in the last 7-days are %@ higher than the previous 7-days.\n"; + +/* Stats insights visitors lower than previous 7 days */ +"stats.insights.label.visitors.sevenDays.lower" = "Your visitors in the last 7-days are %@ lower than the previous 7-days.\n"; + +/* Stats insights label shown when the user's visitor count is the same as the previous 7 days. */ +"stats.insights.label.visitors.sevenDays.same" = "Your visitors in the last 7-days are the same as the previous 7-days.\n"; + /* Title for Comments count in Latest Post Summary stats card. */ "stats.insights.latestPostSummary.comments" = "Comments"; @@ -9771,15 +11299,171 @@ translators: Block name. %s: The localized block name */ /* Hint displayed on the 'Most Popular Time' stats card when a user's site hasn't yet received enough traffic. */ "stats.insights.mostPopularTime.noData" = "Not enough activity. Check back later when your site's had more visitors!"; +/* A hint shown to the user in stats informing the user how many likes one of their posts has received. The %1$@ placeholder will be replaced with the title of a post, the %2$@ with the number of likes. */ +"stats.insights.totalLikes.guideText.plural" = "Your latest post %1$@ has received %2$@ likes."; + +/* A hint shown to the user in stats informing the user that one of their posts has received a like. The %1$@ placeholder will be replaced with the title of a post, and the %2$@ will be replaced by the numeral one. */ +"stats.insights.totalLikes.guideText.singular" = "Your latest post %1$@ has received %2$@ like."; + /* Dismiss the AlertView */ "stockPhotos.strings.dismiss" = "Dismiss"; +/* Subtitle for placeholder in Free Photos. The company name 'Pexels' should always be written as it is. */ +"stockPhotos.subtitle" = "Photos provided by Pexels"; + +/* Title for placeholder in Free Photos */ +"stockPhotos.title" = "Search to find free photos to add to your Media Library!"; + /* Section title for prominent suggestions */ "suggestions.section.prominent" = "In this conversation"; /* Section title for regular suggestions */ "suggestions.section.regular" = "Site members"; +/* Dismiss the current view */ +"support.button.close.title" = "Done"; + +/* Accessibility hint, informing user the button can be used to visit the Jetpack migration FAQ website. */ +"support.button.jetpackMigation.accessibilityHint" = "Tap to visit the Jetpack app FAQ in an external browser"; + +/* Option in Support view to visit the Jetpack migration FAQ website. */ +"support.button.jetpackMigration.title" = "Visit our FAQ"; + +/* Button for confirming logging out from WordPress.com account */ +"support.button.logOut.title" = "Log Out"; + +/* Accessibility hint, informing user the button can be used to visit the support forums website. */ +"support.button.visitForum.accessibilityHint" = "Tap to visit the community forum website in an external browser"; + +/* Option in Support view to visit the WordPress.org support forums. */ +"support.button.visitForum.title" = "Visit WordPress.org"; + +/* Indicator that the chat bot is processing user's input. */ +"support.chatBot.botThinkingIndicator" = "Thinking..."; + +/* Dismiss the current view */ +"support.chatBot.close.title" = "Close"; + +/* Button for users to contact the support team directly. */ +"support.chatBot.contactSupport" = "Contact support"; + +/* Initial message shown to the user when the chat starts. */ +"support.chatBot.firstMessage" = "Hi there, I'm the Jetpack AI Assistant.\\n\\nWhat can we help you with?\\n\\nIf I can't answer your question, I'll help you open a support ticket with our team!"; + +/* Placeholder text for the chat input field. */ +"support.chatBot.inputPlaceholder" = "Send a message..."; + +/* An example question shown to a user seeking support */ +"support.chatBot.questionFive" = "I forgot my login information"; + +/* An example question shown to a user seeking support */ +"support.chatBot.questionFour" = "Why can't I log in?"; + +/* An example question shown to a user seeking support */ +"support.chatBot.questionOne" = "What is my site address?"; + +/* An example question shown to a user seeking support */ +"support.chatBot.questionSix" = "How can I use my custom domain in the app?"; + +/* An example question shown to a user seeking support */ +"support.chatBot.questionThree" = "I can't upload photos\/videos"; + +/* An example question shown to a user seeking support */ +"support.chatBot.questionTwo" = "Help, my site is down!"; + +/* Option for users to report a chat bot answer as inaccurate. */ +"support.chatBot.reportInaccuracy" = "Report as inaccurate"; + +/* Button title referring to the sources of information. */ +"support.chatBot.sources" = "Sources"; + +/* Prompt for users suggesting to select a default question from the list to start a support chat. */ +"support.chatBot.suggestionsPrompt" = "Not sure what to ask?"; + +/* Notice informing user that there was an error submitting their support ticket. */ +"support.chatBot.ticketCreationFailure" = "Error submitting support ticket"; + +/* Notice informing user that their support ticket is being created. */ +"support.chatBot.ticketCreationLoading" = "Creating support ticket..."; + +/* Notice informing user that their support ticket has been created. */ +"support.chatBot.ticketCreationSuccess" = "Ticket created"; + +/* Title of the view that shows support chat bot. */ +"support.chatBot.title" = "Contact Support"; + +/* A title for a text that displays a transcript of an answer in a support chat */ +"support.chatBot.zendesk.answer" = "Answer"; + +/* A title for a text that displays a transcript of user's question in a support chat */ +"support.chatBot.zendesk.question" = "Question"; + +/* A title for a text that displays a transcript from a conversation between Jetpack Mobile Bot (chat bot) and a user */ +"support.chatBot.zendesk.transcript" = "Jetpack Mobile Bot transcript"; + +/* Suggestion in Support view to visit the Forums. */ +"support.row.communityForum.title" = "Ask a question in the community forum and get help from our group of volunteers."; + +/* Accessibility hint describing what happens if the Contact Email button is tapped. */ +"support.row.contactEmail.accessibilityHint" = "Shows a dialog for changing the Contact Email."; + +/* Display value for Support email field if there is no user email address. */ +"support.row.contactEmail.emailNoteSet.detail" = "Not Set"; + +/* Support email label. */ +"support.row.contactEmail.title" = "Email"; + +/* Option in Support view to contact the support team. */ +"support.row.contactUs.title" = "Contact support"; + +/* Option in Support view to enable/disable adding debug information to support ticket. */ +"support.row.debug.title" = "Debug"; + +/* Support email label. */ +"support.row.email.title" = "Email"; + +/* Option in Support view to view the Forums. */ +"support.row.forums.title" = "WordPress Forums"; + +/* Option in Support view to launch the Help Center. */ +"support.row.helpCenter.title" = "WordPress Help Centre"; + +/* An informational card description in Support view explaining what tapping the link on card does */ +"support.row.jetpackMigration.description" = "Our FAQ provides answers to common questions you may have."; + +/* An informational card title in Support view */ +"support.row.jetpackMigration.title" = "Thank you for switching to the Jetpack app!"; + +/* Option in Support view to see activity logs. */ +"support.row.logs.title" = "Logs"; + +/* Option in Support view to access previous help tickets. */ +"support.row.tickets.title" = "Tickets"; + +/* Label in Support view displaying the app version. */ +"support.row.version.title" = "Version"; + +/* Support screen footer text explaining the benefits of enabling the Debug feature. */ +"support.sectionFooter.advanced.title" = "Enable Debugging to include additional information in your logs that can help troubleshoot issues with the app."; + +/* WordPress.com sign-out section header title */ +"support.sectionHeader.account.title" = "WordPress.com Account"; + +/* Section header in Support view for advanced information. */ +"support.sectionHeader.advanced.title" = "Advanced"; + +/* Section header in Support view for the Forums. */ +"support.sectionHeader.forum.title" = "Community Forums"; + +/* Section header in Support view for priority support. */ +"support.sectionHeader.prioritySupport.title" = "Priority Support"; + +/* View title for Help & Support page. */ +"support.title" = "Help"; + +/* Title for placeholder in Tenor picker */ +"tenor.welcomeMessage" = "Search to find GIFs to add to your Media Library!"; + /* Header of delete screen section listing things that will be deleted. */ "these items will be deleted:" = "these items will be deleted:"; @@ -9795,18 +11479,33 @@ translators: Block name. %s: The localized block name */ /* Displayed in the confirmation alert when marking unread notifications as read. */ "unread" = "unread"; -/* Label displayed on video media items. */ -"video" = "video"; - /* Portion of a message for Jetpack users that have multisite WP installation, thus Restore is not available. This part is a link, colored with a different color. */ "visit our documentation page" = "visit our documentation page"; /* Verb. Dismiss the web view screen. */ "webKit.button.dismiss" = "Dismiss"; +/* Preview title of all-time posts and most views widget */ +"widget.allTimePostViews.previewTitle" = "All-Time Posts & Most Views"; + +/* Preview title of all-time views widget */ +"widget.allTimeViews.previewTitle" = "All-Time Views"; + +/* Preview title of all-time views and visitors widget */ +"widget.allTimeViewsVisitors.previewTitle" = "All-Time Views & Visitors"; + /* Title of best views ever label in all time widget */ "widget.alltime.bestviews.label" = "Best views ever"; +/* Title of the label which displays the number of the most daily views the site has ever had. Keep the translation as short as possible. */ +"widget.alltime.bestviewsshort.label" = "Most Views"; + +/* Title of the no data view in all time widget */ +"widget.alltime.nodata.view.title" = "Unable to load all time stats."; + +/* Title of the no site view in all time widget */ +"widget.alltime.nosite.view.title" = "Create or add a site to see all time stats."; + /* Title of posts label in all time widget */ "widget.alltime.posts.label" = "Posts"; @@ -9828,6 +11527,18 @@ translators: Block name. %s: The localized block name */ /* Title of the unconfigured view in today widget */ "widget.jetpack.today.unconfigured.view.title" = "Log in to Jetpack to see today's stats."; +/* Title of the one-liner information consist of views field and all time date range in lock screen all time views widget */ +"widget.lockscreen.alltimeview.label" = "All-Time Views"; + +/* Title of the one-liner information consist of views field and today date range in lock screen today views widget */ +"widget.lockscreen.todayview.label" = "Views Today"; + +/* Title of the no data view in this week widget */ +"widget.thisweek.nodata.view.title" = "Unable to load this week's stats."; + +/* Title of the no site view in this week widget */ +"widget.thisweek.nosite.view.title" = "Create or add a site to see this week's stats."; + /* Description of all time widget in the preview */ "widget.thisweek.preview.description" = "Stay up to date with this week activity on your WordPress site."; @@ -9840,12 +11551,21 @@ translators: Block name. %s: The localized block name */ /* Title of comments label in today widget */ "widget.today.comments.label" = "Comments"; +/* Title of the disabled view in today widget */ +"widget.today.disabled.view.title" = "Stats have moved to the Jetpack app. Switching is free and only takes a minute."; + /* Title of likes label in today widget */ "widget.today.likes.label" = "Likes"; +/* Fallback title of the no data view in the stats widget */ +"widget.today.nodata.view.fallbackTitle" = "Unable to load site stats."; + /* Title of the no data view in today widget */ "widget.today.nodata.view.title" = "Unable to load today's stats."; +/* Title of the no site view in today widget */ +"widget.today.nosite.view.title" = "Create or add a site to see today's stats."; + /* Description of today widget in the preview */ "widget.today.preview.description" = "Stay up to date with today's activity on your WordPress site."; @@ -9864,6 +11584,15 @@ translators: Block name. %s: The localized block name */ /* Title of visitors label in today widget */ "widget.today.visitors.label" = "Visitors"; +/* Preview title of today's likes and commnets widget */ +"widget.todayLikesComments.previewTitle" = "Today's Likes & Comments"; + +/* Preview title of today's views widget */ +"widget.todayViews.previewTitle" = "Today's Views"; + +/* Preview title of today's views and visitors widget */ +"widget.todayViewsVisitors.previewTitle" = "Today's Views & Visitors"; + /* Second part of delete screen title stating [the site] will be unavailable in the future. */ "will be unavailable in the future." = "will be unavailable in the future."; @@ -9888,6 +11617,30 @@ translators: Block name. %s: The localized block name */ /* This is a comma separated list of keywords used for spotlight indexing of the 'My Sites' tab. */ "wordpress, sites, site, blogs, blog" = "wordpress, sites, site, blogs, blog"; +/* Jetpack Plugin Modal on WordPress primary button title */ +"wordpress.jetpack.plugin.modal.primary.button.title" = "Switch to the Jetpack app"; + +/* Jetpack Plugin Modal on WordPress secondary button title */ +"wordpress.jetpack.plugin.modal.secondary.button.title" = "Continue without Jetpack"; + +/* Jetpack Plugin Modal (multiple plugins) on WordPress subtitle with formatted texts. %1$@ is for the site name. */ +"wordpress.jetpack.plugin.modal.subtitle.plural" = "%1$@ is using individual Jetpack plugins, which isn't supported by the WordPress App."; + +/* Jetpack Plugin Modal on WordPress (single plugin) subtitle with formatted texts. %1$@ is for the site name and %2$@ is for the specific plugin name. */ +"wordpress.jetpack.plugin.modal.subtitle.singular" = "%1$@ is using the %2$@ plugin, which isn't supported by the WordPress App."; + +/* Second paragraph of the Jetpack Plugin Modal on WordPress asking the user to switch to Jetpack. */ +"wordpress.jetpack.plugin.modal.subtitle.switch" = "Please switch to the Jetpack app where we'll guide you through connecting the full Jetpack plugin so that you can use all the apps features for this site."; + +/* Jetpack Plugin Modal title in WordPress */ +"wordpress.jetpack.plugin.modal.title" = "Sorry, this site isn't supported by the WordPress app"; + +/* Description of the jetpack migration success card, used in My site. */ +"wp.migration.successCard.description" = "Welcome to the Jetpack app. You can uninstall the WordPress app."; + +/* Title of a button that displays a blog post in a web view. */ +"wp.migration.successCard.learnMore" = "Learn more"; + /* Placeholder for site url, if the url is unknown.Presented when logging in with a site address that does not have a valid Jetpack installation.The error would read: to use this app for your site you'll need... */ "your site" = "your site"; diff --git a/WordPress/Resources/en-CA.lproj/Localizable.strings b/WordPress/Resources/en-CA.lproj/Localizable.strings index aecf1dc12557..d8650148bd0a 100644 --- a/WordPress/Resources/en-CA.lproj/Localizable.strings +++ b/WordPress/Resources/en-CA.lproj/Localizable.strings @@ -1,4 +1,4 @@ -/* Translation-Revision-Date: 2023-04-07 20:35:28+0000 */ +/* Translation-Revision-Date: 2023-11-25 14:35:47+0000 */ /* Plural-Forms: nplurals=2; plural=n != 1; */ /* Generator: GlotPress/4.0.0-alpha.11 */ /* Language: en_CA */ @@ -152,9 +152,6 @@ /* Difference label for Period Overview stat, indicating change from previous period. Ex: +99.9K (5%) */ "%@%@ (%@%%)" = "%1$@%2$@ (%3$@%%)"; -/* Accessibility label for a post in the post list. The parameters are the title, and date respectively. For example, \"Let it Go, 1 hour ago.\" */ -"%@, %@." = "%1$@, %2$@."; - /* Accessibility value for a Stats' Posting Activity Month if the user selected a day with posts. The first parameter is day (e.g. November 2019). The second parameter is the number of posts. */ "%@. %d posts." = "%1$@. %2$d posts."; @@ -210,9 +207,6 @@ /* Age between dates over one year. */ "%d years" = "%d years"; -/* Max image size in pixels (e.g. 300x300px) */ -"%dx%dpx" = "%1$dx%2$dpx"; - /* One menu area available in the theme */ "%i menu area in this theme" = "%i menu area in this theme"; @@ -250,6 +244,9 @@ translators: Block name. %s: The localized block name */ /* translators: accessibility text for blocks with invalid content. %d: localized block title */ "%s block. This block has invalid content" = "%s block. This block has invalid content"; +/* translators: %s: name of the synced block */ +"%s detached" = "%s detached"; + /* translators: %s: embed block variant's label e.g: \"Twitter\". */ "%s embed block previews are coming soon" = "%s embed block previews are coming soon"; @@ -268,6 +265,9 @@ translators: Block name. %s: The localized block name */ /* translators: %s: social link name e.g: \"Instagram\". */ "%s social icon" = "%s social icon"; +/* translators: displayed right after the classic block is converted to blocks. %s: The localized classic block name */ +"'%s' block converted to blocks" = "'%s' block converted to blocks"; + /* translators: Missing block alert title. %s: The localized block name */ "'%s' is not fully-supported" = "'%s' is not fully supported"; @@ -418,6 +418,9 @@ translators: Block name. %s: The localized block name */ /* Label for selecting the Accelerated Mobile Pages (AMP) Blog Traffic Setting */ "Accelerated Mobile Pages (AMP)" = "Accelerated Mobile Pages (AMP)"; +/* No comment provided by engineer. */ +"Access this Paywall block on your web browser for advanced settings." = "Access this Paywall block on your web browser for advanced settings."; + /* Title for the account section in site settings screen */ "Account" = "Account"; @@ -472,13 +475,6 @@ translators: Block name. %s: The localized block name */ /* Label for the Activity Type filter button when there are more than 1 activity type selected */ "Activity Type (%1$d)" = "Activity Type (%1$d)"; -/* Accessibility label for add button to add items to the user's media library - Remove asset from media picker list */ -"Add" = "Add"; - -/* Action for Media Picker to indicate selection of media. The argument in the string represents the number of elements (as numeric digits) selected */ -"Add %@" = "Add %@"; - /* No comment provided by engineer. */ "Add Block After" = "Add Block After"; @@ -539,12 +535,21 @@ translators: Block name. %s: The localized block name */ /* Placeholder text. A call to action for the user to type any topic to which they would like to subscribe. */ "Add any topic" = "Add any topic"; +/* No comment provided by engineer. */ +"Add audio" = "Add audio"; + /* No comment provided by engineer. */ "Add blocks" = "Add blocks"; /* No comment provided by engineer. */ "Add button text" = "Add button text"; +/* No comment provided by engineer. */ +"Add description" = "Add description"; + +/* No comment provided by engineer. */ +"Add image" = "Add image"; + /* No comment provided by engineer. */ "Add image or video" = "Add image or video"; @@ -566,9 +571,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Add menu item to children" = "Add menu item to children"; -/* Accessibility hint for add button to add items to the user's media library */ -"Add new media" = "Add new media"; - /* Screen reader text for Menus button that adds a new menu to a site. */ "Add new menu" = "Add new menu"; @@ -606,6 +608,9 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Add title" = "Add title"; +/* No comment provided by engineer. */ +"Add video" = "Add video"; + /* User-facing string, presented to reflect that site assembly is underway. */ "Adding site features" = "Adding site features"; @@ -631,9 +636,6 @@ translators: Block name. %s: The localized block name */ /* Option to select the Airmail app when logging in with magic links */ "Airmail" = "Airmail"; -/* Description of albums in the photo libraries */ -"Albums" = "Albums"; - /* Image alignment option title. Title of the screen for choosing an image's alignment. */ "Alignment" = "Alignment"; @@ -711,6 +713,9 @@ translators: Block name. %s: The localized block name */ Label for the alt for a media asset (image) */ "Alt Text" = "Alt Text"; +/* No comment provided by engineer. */ +"Alternatively, you can convert the content to blocks." = "Alternatively, you can convert the content to blocks."; + /* Instruction text to explain to help users type their password instead of using magic link login option. */ "Alternatively, you may enter the password for this account." = "Alternatively, you may enter the password for this account."; @@ -858,15 +863,9 @@ translators: Block name. %s: The localized block name */ /* Message prompting the user to confirm that they want to disconnect Jetpack from the site. */ "Are you sure you want to disconnect Jetpack from the site?" = "Are you sure you want to disconnect Jetpack from the site?"; -/* Message prompting the user to confirm that they want to permanently delete a group of media items. */ -"Are you sure you want to permanently delete these items?" = "Are you sure you want to permanently delete these items?"; - /* Message prompting the user to confirm that they want to permanently delete a media item. Should match Calypso. */ "Are you sure you want to permanently delete this item?" = "Are you sure you want to permanently delete this item?"; -/* Message of the confirmation alert when deleting a page from the trash. */ -"Are you sure you want to permanently delete this page?" = "Are you sure you want to permanently delete this page?"; - /* Message of the confirmation alert when deleting a post from the trash. */ "Are you sure you want to permanently delete this post?" = "Are you sure you want to permanently delete this post?"; @@ -898,9 +897,6 @@ translators: Block name. %s: The localized block name */ /* Title of message shown when user taps submit for review. */ "Are you sure you want to submit for review?" = "Are you sure you want to submit for review?"; -/* Message of the trash page confirmation alert. */ -"Are you sure you want to trash this page?" = "Are you sure you want to bin this page?"; - /* Message of the trash confirmation alert. */ "Are you sure you want to trash this post?" = "Are you sure you want to trash this post?"; @@ -941,9 +937,6 @@ translators: Block name. %s: The localized block name */ /* translators: accessibility text. Empty Audio caption. */ "Audio caption. Empty" = "Audio caption. Empty"; -/* Accessibility label for audio items in the media collection view. The parameter is the creation date of the audio. */ -"Audio, %@" = "Audio, %@"; - /* No comment provided by engineer. */ "Authenticating" = "Authenticating"; @@ -1091,10 +1084,16 @@ translators: Block name. %s: The localized block name */ /* Notice that a page without content has been created */ "Blank page created" = "Blank page created"; +/* Noun. Links to a blog's Blaze screen. */ +"Blaze" = "Blaze"; + /* Accessibility label for block quote button on formatting toolbar. Discoverability title for block quote keyboard shortcut. */ "Block Quote" = "Block Quote"; +/* No comment provided by engineer. */ +"Block cannot be rendered because it is deeply nested. Tap here for more details." = "Block cannot be rendered because it is deeply nested. Tap here for more details."; + /* translators: displayed right after the block is copied. */ "Block copied" = "Block copied"; @@ -1107,6 +1106,9 @@ translators: Block name. %s: The localized block name */ /* Popup title about why this post is being opened in block editor */ "Block editor enabled" = "Block editor enabled"; +/* translators: displayed right after the block is grouped */ +"Block grouped" = "Block grouped"; + /* Jetpack Settings: Block malicious login attempts */ "Block malicious login attempts" = "Block malicious login attempts"; @@ -1122,9 +1124,15 @@ translators: Block name. %s: The localized block name */ /* The title of a button that triggers blocking a site from the user's reader. */ "Block this site" = "Block this site"; +/* translators: displayed right after the block is ungrouped. */ +"Block ungrouped" = "Block ungrouped"; + /* Notice title when blocking a site succeeds. */ "Blocked site" = "Blocked site"; +/* Notice title when blocking a user succeeds. */ +"Blocked user" = "Blocked user"; + /* Blocklist Title Settings: Comments Blocklist */ "Blocklist" = "Blocklist"; @@ -1135,6 +1143,9 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Blocks are pieces of content that you can insert, rearrange, and style without needing to know how to code. Blocks are an easy and modern way for you to create beautiful layouts." = "Blocks are pieces of content that you can insert, rearrange, and style without needing to know how to code. Blocks are an easy and modern way for you to create beautiful layouts."; +/* No comment provided by engineer. */ +"Blocks menu" = "Blocks menu"; + /* Title of a button that displays the WordPress.com blog */ "Blog" = "Blog"; @@ -1153,6 +1164,9 @@ translators: Block name. %s: The localized block name */ /* Blog's Viewer Profile. Displayed when the name is empty! */ "Blog's Viewer" = "Blog's Viewer"; +/* Title for the blogging section in site settings screen */ +"Blogging" = "Blogging"; + /* Label for the blogging reminders setting */ "Blogging Reminders" = "Blogging Reminders"; @@ -1208,9 +1222,6 @@ translators: Block name. %s: The localized block name */ /* Label for the post author in the post detail. */ "By " = "By "; -/* Accessibility label for the post author in the post list. The parameter is the author name. For example, \"By Elsa.\" */ -"By %@." = "By %@."; - /* Legal disclaimer for logging in. The underscores _..._ denote underline. */ "By continuing, you agree to our _Terms of Service_." = "By continuing, you agree to our _Terms of Service_."; @@ -1230,8 +1241,7 @@ translators: Block name. %s: The localized block name */ /* Label for size of media while it's being calculated. */ "Calculating..." = "Calculating…"; -/* Accessibility label for taking an image or video with the camera on formatting toolbar. - Accessibility label for the camera tile in the collection view */ +/* Accessibility label for taking an image or video with the camera on formatting toolbar. */ "Camera" = "Camera"; /* Title of an alert informing the user the camera permission for the app is disabled and its needed to proceed */ @@ -1282,10 +1292,7 @@ translators: Block name. %s: The localized block name */ Cancels an Action Cancels an alert. Cancels the mark all as read action. - Dismiss the post action sheet Editing GIF alert cancel action button. - Label for a cancel button - Label for the auto-upload cancelation button in the post list. Tapping will prevent the app from auto-uploading the post. Menus cancel button for deleting a menu. Menus cancel button within text bar while editing items. Menus: Cancel button title for canceling an edited menu item. @@ -1302,10 +1309,6 @@ translators: Block name. %s: The localized block name */ Verb. Title for Jetpack Restore cancel button. */ "Cancel" = "Cancel"; -/* Label for the Post List option that cancels automatic uploading of a post. - Media Library option to cancel an in-progress or failed upload. */ -"Cancel Upload" = "Cancel Upload"; - /* Menus alert button title to cancel discarding changes and not createa a new menu. Menus alert button title to cancel discarding changes and not select a new menu Menus alert button title to cancel discarding changes and not select a new menu location */ @@ -1364,9 +1367,6 @@ translators: Block name. %s: The localized block name */ Main title */ "Change Password" = "Change Password"; -/* Title of button that takes user to the system Settings section for the app */ -"Change Settings" = "Change Settings"; - /* Change Username title. */ "Change Username" = "Change Username"; @@ -1478,6 +1478,9 @@ translators: Block name. %s: The localized block name */ /* Select domain name. Title */ "Choose a domain" = "Choose a domain"; +/* No comment provided by engineer. */ +"Choose a file" = "Choose a time"; + /* OK Button title shown in alert informing users about the Reader Save for Later feature. */ "Choose a new app icon" = "Choose a new app icon"; @@ -1503,9 +1506,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Choose file" = "Choose file"; -/* Menu option for selecting media from the device's photo library. */ -"Choose from My Device" = "Choose from My Device"; - /* Explanatory text for Homepage Settings homepage type selection. */ "Choose from a homepage that displays your latest posts (classic blog) or a fixed \/ static page." = "Choose from a homepage that displays your latest posts (classic blog) or a fixed \/ static page."; @@ -1706,9 +1706,6 @@ translators: Block name. %s: The localized block name */ /* Community & Non-Profit site intent topic */ "Community & Non-Profit" = "Community & Non-Profit"; -/* Accessibility indication that the current Post List style is currently Compact. */ -"Compact" = "Compact"; - /* The action is completed */ "Completed" = "Completed"; @@ -1894,10 +1891,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Copied block" = "Copied block"; -/* Copy the post url and paste anywhere in phone - Label for page copy link. Tapping copy the url of page */ -"Copy Link" = "Copy Link"; - /* Copy link to oomment button title */ "Copy Link to Comment" = "Copy Link to Comment"; @@ -2006,9 +1999,6 @@ translators: Block name. %s: The localized block name */ /* Error title displayed when unable to close user account. */ "Couldn’t close account automatically" = "Couldn’t close account automatically"; -/* Message to show while media data source is finding the number of items available. */ -"Counting media items..." = "Counting media items…"; - /* Period Stats 'Countries' header */ "Countries" = "Countries"; @@ -2232,6 +2222,9 @@ translators: Block name. %s: The localized block name */ /* Only December needs to be translated */ "December 17, 2017" = "December 17, 2017"; +/* No comment provided by engineer. */ +"Deeply nested block" = "Deeply nested block"; + /* Description of the default paragraph formatting style in the editor. Placeholder text displayed in the share extension's summary view. It lets the user know the default category will be used on their post. */ "Default" = "Default"; @@ -2256,7 +2249,6 @@ translators: Block name. %s: The localized block name */ /* Delete Delete button title for the warning shown to the user when he refuses to re-login when the authToken is missing. Title for button that permanently deletes a media item (photo / video) - Title for button that permanently deletes one or more media items (photos / videos) Title of a delete button Title of the trash confirmation alert. */ "Delete" = "Delete"; @@ -2264,15 +2256,11 @@ translators: Block name. %s: The localized block name */ /* Menus confirmation button for deleting a menu. */ "Delete Menu" = "Delete Menu"; -/* Delete option in the confirmation alert when deleting a page from the trash. - Delete option in the confirmation alert when deleting a post from the trash. - Label for a button permanently deletes a page. - Label for the delete post option. Tapping permanently deletes a post. +/* Delete option in the confirmation alert when deleting a post from the trash. Title for button on the comment details page that deletes the comment when tapped. */ "Delete Permanently" = "Delete Permanently"; -/* Title of the confirmation alert when deleting a page from the trash. - Title of the confirmation alert when deleting a post from the trash. */ +/* Title of the confirmation alert when deleting a post from the trash. */ "Delete Permanently?" = "Delete Permanently?"; /* Button label for deleting the current site @@ -2301,6 +2289,9 @@ translators: Block name. %s: The localized block name */ /* Verb. Denies a 2fa authentication challenge. */ "Deny" = "Deny"; +/* No comment provided by engineer. */ +"Describe the purpose of the image. Leave empty if decorative." = "Describe the purpose of the image. Leave empty if decorative."; + /* Label for the description for a media asset (image / video) Section header for tag name in Tag Details View. Title of section that contains plugins' description */ @@ -2309,6 +2300,9 @@ translators: Block name. %s: The localized block name */ /* Shortened version of the main title to be used in back navigation. */ "Design" = "Design"; +/* Navigates to design system gallery only available in development builds */ +"Design System" = "Design System"; + /* Title for the desktop web preview */ "Desktop" = "Desktop"; @@ -2385,7 +2379,6 @@ translators: Block name. %s: The localized block name */ /* Accessibility label for button to dismiss a bottom sheet Accessibility label for the transparent space above the login dialog which acts as a button to dismiss the dialog. Accessibility label for the transparent space above the signup dialog which acts as a button to dismiss the dialog. - Action to show on alert when view asset fails. Dismiss a view. Verb */ "Dismiss" = "Dismiss"; @@ -2403,12 +2396,6 @@ translators: Block name. %s: The localized block name */ User's Display Name */ "Display Name" = "Display Name"; -/* Accessibility label for other media items in the media collection view. The parameter is the creation date of the document. */ -"Document, %@" = "Document, %@"; - -/* Accessibility label for other media items in the media collection view. The parameter is the filename file. */ -"Document: %@" = "Document: %@"; - /* Message shown when all tours have been completed */ "Doesn't it feel good to cross things off a list?" = "Doesn't it feel good to cross things off a list?"; @@ -2569,8 +2556,7 @@ translators: Block name. %s: The localized block name */ /* Description of a Quick Start Tour */ "Draft and publish a post." = "Draft and publish a post."; -/* Title of the drafts filter. This filter shows a list of draft posts. - Title of the drafts header in search list. */ +/* Title of the drafts filter. This filter shows a list of draft posts. */ "Drafts" = "Drafts"; /* No comment provided by engineer. */ @@ -2582,26 +2568,21 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Drag to adjust focal point" = "Drag to adjust focal point"; -/* Label for page duplicate option. Tapping creates a copy of the page. - Label for post duplicate option. Tapping creates a copy of the post. */ -"Duplicate" = "Duplicate"; - /* No comment provided by engineer. */ "Duplicate block" = "Duplicate block"; +/* No comment provided by engineer. */ +"Dynamic" = "Dynamic"; + /* Placeholder text for the search field int the Site Intent screen. */ "E.g. Fashion, Poetry, Politics" = "Eg. Fashion, Poetry, Politics"; /* No comment provided by engineer. */ "Each block has its own settings. To find them, tap on a block. Its settings will appear on the toolbar at the bottom of the screen." = "Each block has its own settings. To find them, tap on a block. Its settings will appear on the toolbar at the bottom of the screen."; -/* Accessibility label for edit button to enable multi selection mode in the user's media library - Edit the post. - Editing GIF alert default action button. +/* Editing GIF alert default action button. Edits a Comment Edits the comment - Label for a button that opens the Edit Page view controller - Label for the edit post button. Tapping displays the editor. User action to edit media details. Verb, edit a comment */ "Edit" = "Edit"; @@ -2609,9 +2590,6 @@ translators: Block name. %s: The localized block name */ /* Title for the edit more button section */ "Edit \"More\" button" = "Edit \"More\" button"; -/* Button that displays the media editor to the user */ -"Edit %@" = "Edit %@"; - /* Blocklist Keyword Edition Title */ "Edit Blocklist Word" = "Edit Blocklist Word"; @@ -2659,6 +2637,12 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Edit video" = "Edit video"; +/* translators: %s: name of the host app (e.g. WordPress) */ +"Editing synced patterns is not yet supported on %s for Android" = "Editing synced patterns is not yet supported on %s for Android"; + +/* translators: %s: name of the host app (e.g. WordPress) */ +"Editing synced patterns is not yet supported on %s for iOS" = "Editing synced patterns is not yet supported on %s for iOS"; + /* Editing GIF alert message. */ "Editing this GIF will remove its animation." = "Editing this GIF will remove its animation."; @@ -2798,9 +2782,6 @@ translators: Block name. %s: The localized block name */ /* Secondary message shown when there are no domains that match the user entered text. */ "Enter different words above and we'll look for an address that matches it." = "Enter different words above and we'll look for an address that matches it."; -/* Accessibility hint for edit button to enable multi selection mode in the user's media library */ -"Enter edit mode to enable multi select to delete" = "Enter edit mode to enable multi select to delete"; - /* (placeholder) Help enter WordPress password Placeholder of a field to type a password to protect the post. */ "Enter password" = "Enter password"; @@ -2956,9 +2937,6 @@ translators: Block name. %s: The localized block name */ /* Short title telling the user they will receive a blogging reminder every day of the week. */ "Every day at %@" = "Every day at %@"; -/* Label for the post author filter. This filter shows posts for all users on the blog. */ -"Everyone" = "Everyone"; - /* Example story title description */ "Example story title" = "Example story title"; @@ -2968,9 +2946,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Excerpt length (words)" = "Excerpt length (words)"; -/* Accessibility label for a post's excerpt in the post list. The parameter is the post excerpt. For example, \"Excerpt. This is the first paragraph.\" */ -"Excerpt. %@." = "Excerpt. %@."; - /* Should be the same as the text displayed if the user clicks the (i) in Calypso. */ "Excerpts are optional hand-crafted summaries of your content." = "Excerpts are optional hand-crafted summaries of your content."; @@ -2980,8 +2955,7 @@ translators: Block name. %s: The localized block name */ /* Accessibility Label for the exit full screen button on the full screen comment reply mode */ "Exit Full Screen" = "Exit Full Screen"; -/* Accessibility indication that the current Post List style is currently Expanded. - Screen reader text to represent the expanded state of a UI control */ +/* Screen reader text to represent the expanded state of a UI control */ "Expanded" = "Expanded"; /* Accessibility hint */ @@ -3031,9 +3005,6 @@ translators: Block name. %s: The localized block name */ The action failed */ "Failed" = "Failed"; -/* Error title when picked media cannot be imported into stories. */ -"Failed Media Export" = "Failed Media Export"; - /* Message for mark all as read success notice */ "Failed marking Notifications as read" = "Failed marking Notifications as read"; @@ -3552,8 +3523,7 @@ translators: Block name. %s: The localized block name */ "Home" = "Home"; /* Title for setting which shows the current page assigned as a site's homepage - Title for the homepage section in site settings screen - Title of the Homepage Badge */ + Title for the homepage section in site settings screen */ "Homepage" = "Homepage"; /* Label for Homepage Settings site settings section @@ -3650,9 +3620,6 @@ translators: Block name. %s: The localized block name */ /* Hint for image title on image settings. */ "Image title" = "Image title"; -/* Accessibility label for image thumbnails in the media collection view. The parameter is the creation date of the image. */ -"Image, %@" = "Image, %@"; - /* Undated post time label */ "Immediately" = "Immediately"; @@ -3680,6 +3647,9 @@ translators: Block name. %s: The localized block name */ /* The plugin is not active on the site and has enabled automatic updates */ "Inactive, Autoupdates on" = "Inactive, Autoupdates on"; +/* Title of the switch to turn on or off the blogging prompts feature. */ +"Include a Blogging Prompt" = "Include a Blogging Prompt"; + /* Describes a standard *.wordpress.com site domain */ "Included with Site" = "Included with Site"; @@ -3709,9 +3679,18 @@ translators: Block name. %s: The localized block name */ Button title used in media picker to insert media (photos / videos) into a post. Placeholder will be the number of items that will be inserted. */ "Insert %@" = "Insert %@"; +/* No comment provided by engineer. */ +"Insert Audio Block" = "Insert Audio Block"; + +/* No comment provided by engineer. */ +"Insert Gallery Block" = "Insert Gallery Block"; + /* Accessibility label for insert horizontal ruler button on formatting toolbar. */ "Insert Horizontal Ruler" = "Insert Horizontal Ruler"; +/* No comment provided by engineer. */ +"Insert Image Block" = "Insert Image Block"; + /* Accessibility label for insert link button on formatting toolbar. Discoverability title for insert link keyboard shortcut. Label action for inserting a link on the editor */ @@ -3720,6 +3699,9 @@ translators: Block name. %s: The localized block name */ /* Discoverability title for insert media keyboard shortcut. */ "Insert Media" = "Insert Media"; +/* No comment provided by engineer. */ +"Insert Video Block" = "Insert Video Block"; + /* No comment provided by engineer. */ "Insert crosspost" = "Insert crosspost"; @@ -3774,6 +3756,9 @@ translators: Block name. %s: The localized block name */ /* Interior Design site intent topic */ "Interior Design" = "Interior Design"; +/* Title displayed on the feature introduction view. */ +"Introducing Blogging Prompts" = "Introducing Blogging Prompts"; + /* Stories intro header title */ "Introducing Story Posts" = "Introducing Story Posts"; @@ -4120,33 +4105,27 @@ translators: Block name. %s: The localized block name */ Settings: Comments Approval settings */ "Links in comments" = "Links in comments"; -/* The accessibility label for the list style button in the Post List. */ -"List style" = "List style"; - /* Title of the screen that load selected the revisions. */ "Load" = "Load"; /* A short label. A call to action to load more posts. */ "Load more posts" = "Load more posts"; +/* No comment provided by engineer. */ +"Loading" = "Loading"; + /* Text displayed while loading the activity feed for a site */ "Loading Activities..." = "Loading Activities…"; /* Text displayed while loading the activity feed for a site */ "Loading Backups..." = "Loading Backups..."; -/* Phrase to show when the user has searched for GIFs and they are being loaded. */ -"Loading GIFs..." = "Loading GIFs…"; - /* Menus label text displayed while menus are loading */ "Loading Menus..." = "Loading Menus…"; /* Text displayed while loading site People. */ "Loading People..." = "Loading People…"; -/* Phrase to show when the user has searched for images and they are being loaded. */ -"Loading Photos..." = "Loading Photos…"; - /* Text displayed while loading plans details */ "Loading Plan..." = "Loading Plan…"; @@ -4207,8 +4186,7 @@ translators: Block name. %s: The localized block name */ /* Local Services site intent topic */ "Local Services" = "Local Services"; -/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. - Title of the Local Changes Badge */ +/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. */ "Local changes" = "Local changes"; /* Title for alert when a generic error happened when trying to find the location of the device */ @@ -4311,6 +4289,9 @@ translators: Block name. %s: The localized block name */ /* Return to blog screen action when theme activation succeeds */ "Manage site" = "Manage site"; +/* No comment provided by engineer. */ +"Manual" = "Manual"; + /* Section name for manual offsets in time zone selector */ "Manual Offsets" = "Manual Offsets"; @@ -4369,7 +4350,6 @@ translators: Block name. %s: The localized block name */ "Max Video Upload Size" = "Max Video Upload Size"; /* Accessibility label for the Me button in My Site. - Label for the post author filter. This filter shows posts only authored by the current user. Me page title Noun. Title. Links to the Me screen. The accessibility value of the me tab. @@ -4377,9 +4357,7 @@ translators: Block name. %s: The localized block name */ "Me" = "Me"; /* Noun. Title. Links to the blog's Media library. - Tab bar title for the Media tab in Media Picker The menu item to select during a guided tour. - Title for Media Library section of the app. Title for the media section in site settings screen Title label for the media settings section in the app settings */ "Media" = "Media"; @@ -4391,13 +4369,6 @@ translators: Block name. %s: The localized block name */ /* Label for size of media cache in the app. */ "Media Cache Size" = "Media Cache Size"; -/* Title for alert when access to media capture is not granted */ -"Media Capture" = "Media Capture"; - -/* Title for alert when a generic error happened when loading media - Title for alert when access to the media library is not granted by the user */ -"Media Library" = "Media Library"; - /* Title for action sheet with media options. */ "Media Options" = "Media Options"; @@ -4420,9 +4391,6 @@ translators: Block name. %s: The localized block name */ /* translators: %s: block title e.g: \"Paragraph\". */ "Media options" = "Media options"; -/* Alert title when there is issues loading an asset to preview. */ -"Media preview failed." = "Media preview failed."; - /* Alert displayed to the user when multiple media items have uploaded successfully. */ "Media uploaded (%ld files)" = "Media uploaded (%ld files)"; @@ -4460,9 +4428,6 @@ translators: Block name. %s: The localized block name */ Label for the share message field on the post settings. */ "Message" = "Message"; -/* Title of section containing image / video metadata such as size and file type */ -"Metadata" = "Metadata"; - /* Option to select the Microsft Outlook app when logging in with magic links */ "Microsoft Outlook" = "Microsoft Outlook"; @@ -4482,13 +4447,11 @@ translators: Block name. %s: The localized block name */ "Months and Years" = "Months and Years"; /* Accessibility label for more button in dashboard quick start card. - Accessibility label for the More button in Page List. Accessibility label for the More button in Post List (compact view). Accessibility label for the More button on formatting toolbar. Accessibility label for the More button on Reader Cell Accessibility label for the More button on Reader's post details - Action button to display more available options - Label for the more post button. Tapping displays an action sheet with post options. */ + Action button to display more available options */ "More" = "More"; /* Action button to display more available options @@ -4546,15 +4509,8 @@ translators: Block name. %s: The localized block name */ /* Screen reader text for button that will move the menu item */ "Move menu item" = "Move menu item"; -/* Label for a button that moves a page to the draft folder - Label for an option that moves a post to the draft folder */ -"Move to Draft" = "Move to Draft"; - -/* Label for a button that moves a page to the trash folder - Label for a option that moves a post to the trash folder - Title for button on the comment details page that moves the comment to trash when tapped. +/* Title for button on the comment details page that moves the comment to trash when tapped. Trash option in the trash confirmation alert. - Trash option in the trash page confirmation alert. Trashes the comment */ "Move to Trash" = "Move to Trash"; @@ -4586,7 +4542,8 @@ translators: Block name. %s: The localized block name */ Title of My Site tab */ "My Site" = "My Site"; -/* Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ +/* Title for site picker screen. + Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ "My Sites" = "My Sites"; /* Siri Suggestion to open My Sites */ @@ -4792,6 +4749,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Accessibility label for no currently selected range. */ "No date range selected" = "No date range selected"; +/* No comment provided by engineer. */ +"No description" = "No description"; + /* Title for the view when there aren't any fixed threats to display */ "No fixed threats" = "No fixed threats"; @@ -4833,9 +4793,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title for the view when there aren't any Activities to display in the Activity Log for a given filter. */ "No matching events found." = "No matching events found."; -/* Message displayed when no results are returned from a media library search. Should match Calypso. - Phrase to show when the user search for images but there are no result to show. - Phrase to show when the user searches for GIFs but there are no result to show. */ +/* Message displayed when no results are returned from a media library search. Should match Calypso. */ "No media matching your search" = "No media matching your search"; /* Menus text shown when no menus were available for loading the Menus editor. */ @@ -4853,8 +4811,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Displayed in the Notifications Tab as a title, when there are no notifications */ "No notifications yet" = "No notifications yet"; -/* Displayed when the user is searching the pages list and there are no matching pages - Text displayed when there's no matching with the text search */ +/* Text displayed when there's no matching with the text search */ "No pages matching your search" = "No pages matching your search"; /* Text displayed when search for plugins returns no results */ @@ -4875,9 +4832,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Message shown whent the reader finds no posts for the chosen tag */ "No posts have been made recently with this tag." = "No posts have been made recently with this tag."; -/* Displayed when the user is searching the posts list and there are no matching posts */ -"No posts matching your search" = "No posts matching your search"; - /* Accessibility value for a Stats' Posting Activity Month if there are no posts. */ "No posts." = "No posts."; @@ -4918,6 +4872,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Message for a notice informing the user their scan completed and no threats were found */ "No threats found" = "No threats found"; +/* No comment provided by engineer. */ +"No title" = "No title"; + /* Disabled No alignment for an image (default). Should be the same as in core WP. No comment will be autoapproved @@ -4975,9 +4932,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* A message title */ "Nothing liked yet" = "Nothing liked yet"; -/* Default message for empty media picker */ -"Nothing to show" = "Nothing to show"; - /* Notifications Details Accessibility Identifier */ "Notification Details Table" = "Notification Details Table"; @@ -5037,7 +4991,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Alert dismissal title Button title. Acknowledges a prompt. Button title. An acknowledgement of the message displayed in a prompt. - Confirmation of action Default action Dismisses the alert Marks all notifications as read. @@ -5099,9 +5052,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* No comment provided by engineer. */ "Only show excerpt" = "Only show excerpt"; -/* Message telling the user that they've only enabled limited photo library permissions for the app. */ -"Only the selected photos you've given access to are available." = "Only the selected photos you've given access to are available."; - /* An informal exclaimation that means `something went wrong`. Title for the view when there's an error loading a comment. Title for the view when there's an error loading Activity Log @@ -5136,9 +5086,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title of a button that opens the apps settings in the system Settings.app */ "Open Settings" = "Open Settings"; -/* Editor button to swich the media picker from quick mode to full picker */ -"Open full media picker" = "Open full media picker"; - /* No comment provided by engineer. */ "Open in Safari" = "Open in Safari"; @@ -5178,6 +5125,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Divider on initial auth view separating auth options. */ "Or" = "Or"; +/* Instruction text for other forms of two-factor auth methods. */ +"Or choose another form of authentication." = "Or choose another form of authentication."; + /* Label for button to log in using site address. Underscores _..._ denote underline. */ "Or log in by _entering your site address_." = "Or log in by _entering your site address_."; @@ -5236,15 +5186,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title shown when selecting a post type of Page from the Share Extension. */ "Page" = "Page"; -/* Prompts the user that a restored page was moved to the drafts list. */ -"Page Restored to Drafts" = "Page Restored to Drafts"; - -/* Prompts the user that a restored page was moved to the published list. */ -"Page Restored to Published" = "Page Restored to Published"; - -/* Prompts the user that a restored page was moved to the scheduled list. */ -"Page Restored to Scheduled" = "Page Restored to Scheduled"; - /* Name of the button to open the page settings The title of the Page Settings screen. */ "Page Settings" = "Page Settings"; @@ -5261,9 +5202,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of notification displayed when a page has failed to upload. */ "Page failed to upload" = "Page failed to upload"; -/* A short message explaining that a page was moved to the trash bin. */ -"Page moved to trash." = "Page moved to trash."; - /* Title of notification displayed when a page has been successfully saved as a draft. */ "Page pending review" = "Page pending review"; @@ -5335,8 +5273,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title of pending Comments filter. */ "Pending" = "Pending"; -/* Name for the status of a post pending review. - Title of the Pending Review Badge */ +/* Name for the status of a post pending review. */ "Pending review" = "Pending review"; /* Noun. Title of the people management feature. @@ -5365,12 +5302,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Photography site intent topic */ "Photography" = "Photography"; -/* Tab bar title for the Photos tab in Media Picker */ -"Photos" = "Photos"; - -/* Subtitle for placeholder in Free Photos. The company name 'Pexels' should always be written as it is. */ -"Photos provided by Pexels" = "Photos provided by Pexels"; - /* Title for selecting a new username in the site creation flow. */ "Pick username" = "Pick username"; @@ -5384,6 +5315,12 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* User action to play a video on the editor. */ "Play video" = "Play video"; +/* No comment provided by engineer. */ +"Playback Bar Color" = "Playback Bar Colour"; + +/* No comment provided by engineer. */ +"Playback Settings" = "Playback Settings"; + /* Suggestion to add content before trying to publish post or page */ "Please add some content before trying to publish." = "Please add some content before trying to publish."; @@ -5457,7 +5394,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Please enter the password for your WordPress.com account to log in with your Apple ID." = "Please enter the password for your WordPress.com account to log in with your Apple ID."; /* Instruction text on the two-factor screen. */ -"Please enter the verification code from your authenticator app, or tap the link below to receive a code via SMS." = "Please enter the verification code from your authenticator app, or tap the link below to receive a code via SMS."; +"Please enter the verification code from your authenticator app." = "Please enter the verification code from your authenticator app."; /* Popup message to ask for user credentials (fields shown below). */ "Please enter your credentials" = "Please enter your credentials"; @@ -5552,15 +5489,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ The post formats available for the post. Should be the same as in core WP. */ "Post Format" = "Post Format"; -/* Prompts the user that a restored post was moved to the drafts list. */ -"Post Restored to Drafts" = "Post Restored to Drafts"; - -/* Prompts the user that a restored post was moved to the published list. */ -"Post Restored to Published" = "Post Restored to Published"; - -/* Prompts the user that a restored post was moved to the scheduled list. */ -"Post Restored to Scheduled" = "Post Restored to Scheduled"; - /* Name of the button to open the post settings The title of the Post Settings screen. */ "Post Settings" = "Post Settings"; @@ -5580,9 +5508,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of notification displayed when a post has failed to upload. */ "Post failed to upload" = "Post failed to upload"; -/* A short message explaining that a post was moved to the trash bin. */ -"Post moved to trash." = "Post moved to trash."; - /* Title of notification displayed when a post has been successfully saved as a draft. */ "Post pending review" = "Post pending review"; @@ -5641,9 +5566,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Period Stats 'Posts and Pages' header */ "Posts and Pages" = "Posts and Pages"; -/* Title of the Posts Page Badge */ -"Posts page" = "Posts page"; - /* Message informing the user that their static homepage for posts was set successfully */ "Posts page successfully updated" = "Posts page successfully updated"; @@ -5656,9 +5578,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* A message explaining the Posts I Like feature in the reader */ "Posts that you like will appear here." = "Posts that you like will appear here."; -/* Subtitle for placeholder in Tenor picker. `The company name 'Tenor' should always be written as it is. */ -"Powered by Tenor" = "Powered by Tenor"; - /* Browse premium themes selection title */ "Premium" = "Premium"; @@ -5677,18 +5596,12 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title for screen to preview a static content. */ "Preview" = "Preview"; -/* Action for Media Picker to preview the selected media items. The argument in the string represents the number of elements (as numeric digits) selected */ -"Preview %@" = "Preview %@"; - /* Title for web preview device switching button */ "Preview Device" = "Preview Device"; /* Title on display preview error */ "Preview Unavailable" = "Preview Unavailable"; -/* Accessibility label for media item preview for user's viewing an item in their media library */ -"Preview media" = "Preview media"; - /* No comment provided by engineer. */ "Preview page" = "Preview page"; @@ -5728,12 +5641,14 @@ translators: %s: Select control button label e.g. \"Button width\" */ Privacy Settings Title */ "Privacy Settings" = "Privacy Settings"; +/* No comment provided by engineer. */ +"Privacy and Rating" = "Privacy and Rating"; + /* Link to the CCPA privacy notice for residents of California. */ "Privacy notice for California users" = "Privacy notice for California users"; /* Name for the status of a post that is marked private. - Privacy setting for posts set to 'Private'. Should be the same as in core WP. - Title of the Private Badge */ + Privacy setting for posts set to 'Private'. Should be the same as in core WP. */ "Private" = "Private"; /* No comment provided by engineer. */ @@ -5783,12 +5698,10 @@ translators: %s: Select control button label e.g. \"Button width\" */ Label for the publish date button. */ "Publish Date" = "Publish Date"; -/* A short phrase indicating a post is due to be immedately published. - Label for a button that moves a page to the published folder, publishing with the current date/time. */ +/* A short phrase indicating a post is due to be immedately published. */ "Publish Immediately" = "Publish Immediately"; /* Label for a button that publishes the post - Label for an option that moves a publishes a post immediately Title of button allowing the user to immediately publish the post they are editing. */ "Publish Now" = "Publish Now"; @@ -5806,8 +5719,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Name for the status of a published post. Period Stats 'Published' header - Title of the published filter. This filter shows a list of posts that the user has published. - Title of the published header in search list. */ + Title of the published filter. This filter shows a list of posts that the user has published. */ "Published" = "Published"; /* Precedes the name of the blog just posted on */ @@ -5907,6 +5819,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Message shwon to confirm a publicize connection has been successfully reconnected. */ "Reconnected" = "Reconnected"; +/* Action button to redo last change */ +"Redo" = "Redo"; + /* Label for link title in Referrers stat. */ "Referrer" = "Referrer"; @@ -5946,8 +5861,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of the completion screen of the Blogging Reminders Settings screen when the reminders are removed. */ "Reminders removed" = "Reminders removed"; -/* Add asset to media picker list - Alert button to confirm a plugin to be removed +/* Alert button to confirm a plugin to be removed Button label when removing a blog Label for the Remove Feature Image icon. Tapping will show a confirmation screen for removing the feature image from the post. Remove Action @@ -5989,6 +5903,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* No comment provided by engineer. */ "Remove block" = "Remove block"; +/* No comment provided by engineer. */ +"Remove blocks" = "Remove blocks"; + /* Option to remove Insight from view. */ "Remove from insights" = "Remove from insights"; @@ -6097,9 +6014,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of secondary button on alert prompting verify their accounts while attempting to publish */ "Resend" = "Resend"; -/* Title of the reset button */ -"Reset" = "Reset"; - /* Accessibility label for the reset activity type button */ "Reset Activity Type filter" = "Reset Activity Type filter"; @@ -6154,12 +6068,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ Button title, displayed when media has failed to upload. Allows the user to try the upload again. Button title. Retries uploading a post. If a user taps the button with this label, the action that evinced this error view will be retried. - Label for a button that attempts to re-upload a page that previously failed to upload. - Label for the retry post upload button. Tapping attempts to upload the post again. Opens the media library . Retries the upload of a user's post. Retry updating User's Role - Retry uploading the post. Retry. Action Retry. Verb – retry a failed media upload. The Jetpack view button title used when an error occurred @@ -6171,9 +6082,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Button title that triggers a scan */ "Retry Scan" = "Retry Scan"; -/* User action to retry media upload. */ -"Retry Upload" = "Retry Upload"; - /* User action to retry all failed media uploads. */ "Retry all" = "Retry all"; @@ -6271,9 +6179,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Accessibility label for the 'Save Post' button when a post has been saved. */ "Saved Post" = "Saved Post"; -/* Text displayed in HUD when a media item's metadata (title, etc) is saved successfully. */ -"Saved!" = "Saved!"; - /* Accessibility hint for the 'Save Post' button. */ "Saves this post for later." = "Saves this post for later."; @@ -6284,7 +6189,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Saving post…" = "Saving post…"; /* Menus save button title while it is saving a Menu. - Text displayed in HUD while a media item's metadata (title, etc) is being saved. Text displayed in HUD while a post is being saved as a draft. */ "Saving..." = "Saving…"; @@ -6366,27 +6270,18 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* title of the button that searches the first domain. */ "Search for a domain" = "Search for a domain"; +/* Select domain name. Subtitle */ +"Search for a short and memorable keyword to help people find and visit your website." = "Search for a short and memorable keyword to help people find and visit your website."; + /* No comment provided by engineer. */ "Search input field." = "Search input field."; /* No comment provided by engineer. */ "Search or type URL" = "Search or type URL"; -/* Text displayed when the search controller will be presented */ -"Search pages" = "Search pages"; - -/* Text displayed when the search controller will be presented */ -"Search posts" = "Search posts"; - /* No comment provided by engineer. */ "Search settings" = "Search settings"; -/* Title for placeholder in Tenor picker */ -"Search to find GIFs to add to your Media Library!" = "Search to find GIFs to add to your Media Library!"; - -/* Title for placeholder in Free Photos */ -"Search to find free photos to add to your Media Library!" = "Search to find free photos to add to your Media Library!"; - /* Menus search bar placeholder text. */ "Search..." = "Search…"; @@ -6457,9 +6352,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Register Domain - Domain contact information field placeholder for Country */ "Select Country" = "Select Country"; -/* Title of button that allows the user to select more photos to access within the app */ -"Select More" = "Select More"; - /* Blog Picker's Title */ "Select Site" = "Select Site"; @@ -6481,9 +6373,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Register domain - Title for the Choose domain button of Suggested domains screen */ "Select domain" = "Select domain"; -/* Accessibility hint for actions when displaying media items. */ -"Select media." = "Select media."; - /* Accessibility label for selecting paragraph style button on formatting toolbar. */ "Select paragraph style" = "Select paragraph style"; @@ -6587,19 +6476,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label for connected service in Publicize stat. */ "Service" = "Service"; -/* Label for a button that opens the Set Parent options view controller - Navigation title displayed on the navigation bar */ +/* Navigation title displayed on the navigation bar */ "Set Parent" = "Set Parent"; /* No comment provided by engineer. */ "Set as Featured Image" = "Set as Featured Image"; -/* Label for a button that sets the selected page as the site's Homepage */ -"Set as Homepage" = "Set as Homepage"; - -/* Label for a button that sets the selected page as the site's Posts page */ -"Set as Posts Page" = "Set as Posts Page"; - /* Notice confirming that an image has been set as the post's featured image. */ "Set as featured image" = "Set as featured image"; @@ -6643,7 +6525,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Accessibility label for share buttons in nav bars Button label to share a post Button label to share a web page - Share the post. Shares the comment URL Spoken accessibility label Title for a button that allows the user to share their answer to the prompt. @@ -7029,8 +6910,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Name of setting configured when a site uses a static page as its homepage */ "Static Homepage" = "Static Homepage"; -/* Label for post stats option. Tapping displays statistics for a post. - Noun. Abbreviation of Statistics. Name of the Stats feature +/* Noun. Abbreviation of Statistics. Name of the Stats feature Noun. Abbv. of Statistics. Links to a blog's Stats screen. Stats 3D Touch Shortcut Stats window title @@ -7061,9 +6941,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label text that defines a post marked as sticky */ "Sticky" = "Sticky"; -/* Accessibility label for a sticky post in the post list. */ -"Sticky." = "Sticky."; - /* User action to stop upload. */ "Stop upload" = "Stop upload"; @@ -7120,7 +6997,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ Theme Support action title */ "Support" = "Support"; -/* Button used to switch site */ +/* Title for back button that leads to the site picker screen. */ "Switch Site" = "Switch Site"; /* Switches the Editor to HTML Mode */ @@ -7165,6 +7042,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Accessibility Identifier for the Default Font Aztec Style. */ "Switches to the default Font Size" = "Switches to the default Font Size"; +/* No comment provided by engineer. */ +"Synced patterns" = "Synced patterns"; + /* Title for the app appearance setting (light / dark mode) that uses the system default value */ "System default" = "System default"; @@ -7205,9 +7085,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label explaining why users might want to add tags. */ "Tags help tell readers what a post is about. Separate different tags with commas." = "Tags help tell readers what a post is about. Separate different tags with commas."; -/* Menu option for taking an image or video with the device's camera. */ -"Take Photo or Video" = "Take Photo or Video"; - /* No comment provided by engineer. */ "Take a Photo" = "Take a Photo"; @@ -7226,6 +7103,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* No comment provided by engineer. */ "Tap here to show help" = "Tap here to show help"; +/* No comment provided by engineer. */ +"Tap here to show more details." = "Tap here to show more details."; + /* Accessibility hint for a button that opens a view that allows to add new stats cards. */ "Tap to add new stats cards." = "Tap to add new stats cards."; @@ -7275,12 +7155,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Accessibility hint */ "Tap to select the previous period" = "Tap to select the previous period"; -/* Accessibility hint for button used to switch site */ -"Tap to switch to another site, or add a new site" = "Tap to switch to another site, or add a new site"; - -/* Accessibility hint for media item preview for user's viewing an item in their media library */ -"Tap to view media in full screen" = "Tap to view media in full screen"; - /* Accessibility hint for a button that opens a new view with more details. */ "Tap to view more details." = "Tap to view more details."; @@ -7326,10 +7200,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* No comment provided by engineer. */ "Text formatting controls are located within the toolbar positioned above the keyboard while editing a text block" = "Text formatting controls are located within the toolbar positioned above the keyboard while editing a text block"; -/* Button title - The button's title text to send a 2FA code via SMS text message. */ +/* Button title */ "Text me a code instead" = "Text me a code instead"; +/* The button's title text to send a 2FA code via SMS text message. */ +"Text me a code via SMS" = "Text me a code via SMS"; + /* Message of alert when theme activation succeeds */ "Thanks for choosing %@ by %@" = "Thanks for choosing %1$@ by %2$@"; @@ -7357,9 +7233,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Error message shown to a user who is trying to share to Facebook but does not have any available Facebook Pages. */ "The Facebook connection cannot find any Pages. Publicize cannot connect to Facebook Profiles, only published Pages." = "The Facebook connection cannot find any Pages. Publicize cannot connect to Facebook Profiles, only published Pages."; -/* Message shown when a GIF failed to load while trying to add it to the Media library. */ -"The GIF could not be added to the Media Library." = "The GIF could not be added to the Media Library."; - /* Description shown when a user logs in with Google but no matching WordPress.com account is found */ "The Google account \"%@\" doesn't match any account on WordPress.com" = "The Google account \"%@\" doesn't match any account on WordPress.com"; @@ -7487,7 +7360,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Error message shown when user attempts to remove the site owner. */ "The user you are trying to remove is the owner of this site. Please contact support for assistance." = "The user you are trying to remove is the owner of this site. Please contact support for assistance."; -/* Error message informing a user about an invalid password. */ +/* No comment provided by engineer. */ "The username or password stored in the app may be out of date. Please re-enter your password in the settings and try again." = "The username or password stored in the app may be out of date. Please re-enter your password in the settings and try again."; /* Message shown when a video failed to load while trying to add it to the Media library. */ @@ -7555,9 +7428,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* A short error message letting the user know about a problem displaying a post. */ "There was a problem displaying this post." = "There was a problem displaying this post."; -/* Error message displayed when the Media Library is unable to load a full sized preview of an item. */ -"There was a problem loading the media item." = "There was a problem loading the media item."; - /* The loading view subtitle displayed when an error occurred */ "There was a problem loading your data, refresh your page to try again." = "There was a problem loading your data, refresh your page to try again."; @@ -7570,9 +7440,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Explaining to the user there was an error trying to obtain the current location of the user. */ "There was a problem when trying to access your location. Please try again later." = "There was a problem when trying to access your location. Please try again later."; -/* Explaining to the user there was an generic error accesing media. */ -"There was a problem when trying to access your media. Please try again later." = "There was a problem when trying to access your media. Please try again later."; - /* Message for stories unknown error. */ "There was a problem with the Stories editor. If the problem persists you can contact us via the Me > Help & Support screen." = "There was a problem with the Stories editor. If the problem persists you can contact us via the Me > Help & Support screen."; @@ -7643,9 +7510,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* A description informing the user in order to proceed with this feature we will need camera permissions, and how to enable it. */ "This app needs permission to access the Camera to scan login codes, tap on the Open Settings button to enable it." = "This app needs permission to access the Camera to scan login codes, tap on the Open Settings button to enable it."; -/* Explaining to the user why the app needs access to the device media library. */ -"This app needs permission to access your device media library in order to add photos and\/or video to your posts. Please change the privacy settings if you wish to allow this." = "This app needs permission to access your device media library in order to add photos and\/or video to your posts. Please change the privacy settings if you wish to allow this."; - /* No comment provided by engineer. */ "This color combination may be hard for people to read. Try using a brighter background color and\/or a darker text color." = "This colour combination may be hard for people to read. Try using a brighter background colour and\/or a darker text colour."; @@ -7755,6 +7619,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Description of a Quick Start Tour */ "Time to finish setting up your site! Our checklist walks you through the next steps." = "Time to finish setting up your site! Our checklist walks you through the next steps."; +/* Error when the uses takes more than 1 minute to submit a security key. */ +"Time's up, but don't worry, your security is our priority. Please try again!" = "Time's up, but don't worry, your security is our priority. Please try again!"; + /* WordPress.com Marketing Footer Text */ "Tips for getting the most out of WordPress.com." = "Tips for getting the most out of WordPress.com."; @@ -7878,24 +7745,20 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title for the traffic section in site settings screen */ "Traffic" = "Traffic"; +/* Describes a domain that was transferred from elsewhere to wordpress.com */ +"Transferred Domain" = "Transferred Domain"; + /* translators: %s: block title e.g: \"Paragraph\". */ "Transform %s to" = "Transform %s to"; /* No comment provided by engineer. */ "Transform block…" = "Transform block…"; -/* Accessibility label for trash button to delete items from the user's media library - Accessibility label for trash buttons in nav bars +/* Accessibility label for trash buttons in nav bars Trashes a comment Trashes the comment */ "Trash" = "Trash"; -/* Accessibility hint for trash button to delete items from the user's media library */ -"Trash selected media" = "Trash selected media"; - -/* Title of the trash page confirmation alert. */ -"Trash this page?" = "Trash this page?"; - /* Title of the trash confirmation alert. */ "Trash this post?" = "Trash this post?"; @@ -7953,6 +7816,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* When social login fails, this button offers to let them try tp login using a URL */ "Try with the site address" = "Try with the site address"; +/* Destructive menu title to remove the prompt card from the dashboard. */ +"Turn off prompts" = "Turn off prompts"; + /* Verb. An option to switch off site notifications. */ "Turn off site notifications" = "Turn off site notifications"; @@ -8010,9 +7876,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Shown when a user logs in with Google but it subsequently fails to work as login to WordPress.com */ "Unable To Connect" = "Unable To Connect"; -/* An error message. */ -"Unable to Connect" = "Unable to Connect"; - /* Title for stories unknown error. */ "Unable to Create Stories Editor" = "Unable to Create Stories Editor"; @@ -8028,9 +7891,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* An error message shown when there is an issue creating new invite links. */ "Unable to create new invite links." = "Unable to create new invite links."; -/* Text displayed in HUD if there was an error attempting to delete a group of media items. */ -"Unable to delete all media items." = "Unable to delete all media items."; - /* Text displayed in HUD if there was an error attempting to delete a media item. */ "Unable to delete media item." = "Unable to delete media item."; @@ -8094,12 +7954,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Message displayed when sharing a link to the downloadable backup fails. */ "Unable to share link" = "Unable to share link"; -/* Message that appears when a user tries to trash a page while their device is offline. */ -"Unable to trash pages while offline. Please try again later." = "Unable to bin pages while offline. Please try again later."; - -/* Message that appears when a user tries to trash a post while their device is offline. */ -"Unable to trash posts while offline. Please try again later." = "Unable to trash posts while offline. Please try again later."; - /* Notice title when turning site notifications off fails. */ "Unable to turn off site notifications" = "Unable to turn off site notifications"; @@ -8172,8 +8026,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ Button title. Reverts the previous notification operation Revert an operation Revert enabling notification after successfully subcribing to the comments for the post. - The title of an 'undo' button. Tapping the button moves a trashed page out of the trash folder. - The title of an 'undo' button. Tapping the button moves a trashed post out of the trash folder. Undo action */ "Undo" = "Undo"; @@ -8203,6 +8055,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* VoiceOver accessibility hint, informing the user the button can be used to unfollow a blog. */ "Unfollows the blog." = "Unfollows the blog."; +/* No comment provided by engineer. */ +"Ungroup block" = "Ungroup block"; + /* Unhides a site from the site picker list */ "Unhide" = "Unhide"; @@ -8213,9 +8068,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title for Unknown HTML Editor */ "Unknown HTML" = "Unknown HTML"; -/* Label to use when creation date from media asset is not know. */ -"Unknown creation date" = "Unknown creation date"; - /* No comment provided by engineer. */ "Unknown error" = "Unknown error"; @@ -8225,6 +8077,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Search Terms label for 'unknown search terms'. */ "Unknown search terms" = "Unknown search terms"; +/* translators: %s: the hex color value */ +"Unlabeled color. %s" = "Unlabeled colour. %s"; + /* VoiceOver accessibility hint, informing the user the button can be used to stop liking a comment */ "Unlike the Comment." = "Unlike the Comment."; @@ -8378,6 +8233,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of a row displayed on the debug screen used to configure the sandbox store use in the App. */ "Use Sandbox Store" = "Use Sandbox Store"; +/* The button's title text to use a security key. */ +"Use a security key" = "Use a security key"; + /* Option to enable the block editor for new posts */ "Use block editor" = "Use block editor"; @@ -8453,15 +8311,10 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of an alert informing users that the video they are trying to select is not allowed. */ "Video not uploaded" = "Video not uploaded"; -/* Accessibility label for video thumbnails in the media collection view. The parameter is the creation date of the video. */ -"Video, %@" = "Video, %@"; - /* Period Stats 'Videos' header */ "Videos" = "Videos"; /* Button title. Displays a summary / sharing screen for a specific post. - Label for a button that opens the page when tapped. - Label for the view post button. Tapping displays the post as it appears on the web. Opens the post epilogue screen to allow sharing / viewing of a post. Theme View action title Verb. The screen title shown when viewing a post inside the app. */ @@ -8576,6 +8429,10 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Message shown on screen while waiting for Google to finish its signup process. */ "Waiting for Google to complete…" = "Waiting for Google to complete…"; +/* Text while the webauthn signature is being verified + Text while waiting for a security key challenge */ +"Waiting for security key" = "Waiting for security key"; + /* View title during the Google auth process. */ "Waiting..." = "Waiting…"; @@ -8587,6 +8444,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ Title for Jetpack Restore Warning screen */ "Warning" = "Warning"; +/* No comment provided by engineer. */ +"Warning message" = "Warning message"; + /* Caption displayed in promotional screens shown during the login flow. */ "Watch your audience grow with in-depth analytics." = "Watch your audience grow with in-depth analytics."; @@ -8947,6 +8807,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* An error message shown when a wpcom user provides the wrong password. */ "Whoops, something went wrong and we couldn't log you in. Please try again!" = "Whoops, something went wrong and we couldn't log you in. Please try again!"; +/* Generic error on the 2FA screen */ +"Whoops, something went wrong. Please try again!" = "Whoops, something went wrong. Please try again!"; + +/* Error when the uses chooses an invalid security key on the 2FA screen. */ +"Whoops, that security key does not seem valid. Please try again with another one" = "Whoops, that security key does not seem valid. Please try again with another one"; + /* Error message shown when an incorrect two factor code is provided. */ "Whoops, that's not a valid two-factor verification code. Double-check your code and try again!" = "Whoops, that's not a valid two-factor verification code. Double-check your code and try again!"; @@ -8974,9 +8840,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Siri Suggestion to open Support */ "WordPress Help" = "WordPress Help"; -/* Name for the WordPress Media Library */ -"WordPress Media" = "WordPress Media"; - /* Accessibility label for selecting an image or video from the user's WordPress media library on formatting toolbar. */ "WordPress Media Library" = "WordPress Media Library"; @@ -9129,6 +8992,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Information shown below the optional password field after new account creation. */ "You can always log in with a link like the one you just used, but you can also set up a password if you prefer." = "You can always log in with a link like the one you just used, but you can also set up a password if you prefer."; +/* Note displayed in the Feature Introduction view. */ +"You can control Blogging Prompts and Reminders at any time in My Site > Settings > Blogging" = "You can control Blogging Prompts and Reminders at any time in My Site > Settings > Blogging"; + +/* Accessibility hint for Note displayed in the Feature Introduction view. */ +"You can control Blogging Prompts and Reminders at any time in My Site, Settings, Blogging" = "You can control Blogging Prompts and Reminders at any time in My Site, Settings, Blogging"; + /* No comment provided by engineer. */ "You can edit this block using the web version of the editor." = "You can edit this block using the web version of the editor."; @@ -9285,9 +9154,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Share extension error dialog text. */ "Your account does not have permission to upload media to this site. The Site Administrator can change these permissions." = "Your account does not have permission to upload media to this site. The Site Administrator can change these permissions."; -/* Explaining to the user why the app needs access to the device media library. */ -"Your app is not authorized to access media library due to active restrictions such as parental controls. Please check your parental control settings in this device." = "Your app is not authorized to access media library due to active restrictions such as parental controls. Please check your parental control settings in this device."; - /* Title for the Jetpack Backup Complete message. */ "Your backup is now available for download" = "Your backup is now available for download"; @@ -9306,9 +9172,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of the site address section in the Domains Dashboard. */ "Your free WordPress.com address is" = "Your free WordPress.com address is"; -/* Error message when picked media cannot be imported into stories. */ -"Your media could not be exported. If the problem persists you can contact us via the Me > Help & Support screen." = "Your media could not be exported. If the problem persists you can contact us via the Me > Help & Support screen."; - /* Details about recently acquired domain on domain credit redemption success screen */ "Your new domain %@ is being set up. It may take up to 30 minutes for your domain to start working." = "Your new domain %@ is being set up. It may take up to 30 minutes for your domain to start working."; @@ -9426,30 +9289,174 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Age between dates equaling one hour. */ "an hour" = "an hour"; -/* Label displayed on audio media items. */ -"audio" = "audio"; +/* This is the string we display when prompting the user to review the Jetpack app */ +"appRatings.jetpack.prompt" = "What do you think about Jetpack?"; + +/* This is the string we display when prompting the user to review the WordPress app */ +"appRatings.wordpress.prompt" = "What do you think about WordPress?"; /* translators: displays audio file extension. e.g. MP3 audio file */ "audio file" = "audio file"; +/* Alert message when something goes wrong with the selected image. */ +"avatarMenu.failedToSetAvatarAlertMessage" = "Unable to load the image. Please choose a different one or try again later."; + +/* Title for menu that is shown when you tap your gravatar */ +"avatarMenu.title" = "Update Gravatar"; + /* The title of a button to close the classic editor deprecation notice alert dialog. */ "aztecPost.deprecationNotice.dismiss" = "Dismiss"; /* User action to dismiss media options. */ "aztecPost.mediaAttachmentActionSheet.dismiss" = "Dismiss"; +/* Button title for the button that shows the Blaze flow when tapped. */ +"blaze.campaigns.create.button.title" = "Promote"; + +/* Text displayed when there are no Blaze campaigns to display. */ +"blaze.campaigns.empty.subtitle" = "You have not created any campaigns yet. Click promote to get started."; + +/* Title displayed when there are no Blaze campaigns to display. */ +"blaze.campaigns.empty.title" = "You have no campaigns"; + +/* Text displayed when there is a failure loading Blaze campaigns. */ +"blaze.campaigns.errorMessage" = "There was an error loading campaigns."; + +/* Title for the view when there's an error loading Blaze campiagns. */ +"blaze.campaigns.errorTitle" = "Oops"; + +/* Displayed while Blaze campaigns are being loaded. */ +"blaze.campaigns.loading.title" = "Loading campaigns..."; + +/* Title for the screen that allows users to manage their Blaze campaigns. */ +"blaze.campaigns.title" = "Blaze Campaigns"; + +/* Description for the Blaze dashboard card. */ +"blaze.dashboard.card.description" = "Display your work across millions of sites."; + +/* Title for a menu action in the context menu on the Blaze card. */ +"blaze.dashboard.card.menu.hide" = "Hide this"; + +/* Title for a menu action in the context menu on the Blaze card. */ +"blaze.dashboard.card.menu.learnMore" = "Learn more"; + +/* Title for the Blaze dashboard card. */ +"blaze.dashboard.card.title" = "Promote your content with Blaze"; + +/* Button title for a Blaze overlay prompting users to select a post to blaze. */ +"blaze.overlay.buttonTitle" = "Blaze a post now"; + +/* Description for the Blaze overlay. */ +"blaze.overlay.descriptionOne" = "Promote any post or page in only a few minutes for just a few dollars a day."; + +/* Description for the Blaze overlay. */ +"blaze.overlay.descriptionThree" = "Track your campaign's performance and cancel at anytime."; + +/* Description for the Blaze overlay. */ +"blaze.overlay.descriptionTwo" = "Your content will appear on millions of WordPress and Tumblr sites."; + +/* Title for the Blaze overlay. */ +"blaze.overlay.title" = "Drive more traffic to your site with Blaze"; + +/* Button title for the Blaze overlay prompting users to blaze the selected page. */ +"blaze.overlay.withPage.buttonTitle" = "Blaze this page"; + +/* Button title for the Blaze overlay prompting users to blaze the selected post. */ +"blaze.overlay.withPost.buttonTitle" = "Blaze this post"; + +/* Short status description */ +"blazeCampaign.status.active" = "Active"; + +/* Short status description */ +"blazeCampaign.status.approved" = "Approved"; + +/* Short status description */ +"blazeCampaign.status.canceled" = "Canceled"; + +/* Short status description */ +"blazeCampaign.status.completed" = "Completed"; + +/* Short status description */ +"blazeCampaign.status.inmoderation" = "In Moderation"; + +/* Short status description */ +"blazeCampaign.status.processing" = "Processing"; + +/* Short status description */ +"blazeCampaign.status.rejected" = "Rejected"; + +/* Short status description */ +"blazeCampaign.status.scheduled" = "Scheduled"; + +/* Title for budget stats view */ +"blazeCampaigns.budget" = "Budget"; + +/* Title for impressions stats view */ +"blazeCampaigns.clicks" = "Clicks"; + +/* Title for impressions stats view */ +"blazeCampaigns.impressions" = "Impressions"; + +/* Title for the context menu action that hides the dashboard card. */ +"blogDashboard.contextMenu.hideThis" = "Hide this"; + /* Action shown in a bottom notice to dismiss it. */ "blogDashboard.dismiss" = "Dismiss"; +/* Context menu button title */ +"blogHeader.actionCopyURL" = "Copy URL"; + /* Verb. Dismisses the blogging prompt notification. */ "bloggingPrompt.pushNotification.customActionDescription.dismiss" = "Dismiss"; /* Used when displaying author of a plugin. */ "by %@" = "by %@"; +/* Option for users to rate a chat bot answer as helpful. */ +"chat.rateHelpful" = "Rate as helpful"; + /* Displayed in the confirmation alert when marking comment notifications as read. */ "comment" = "comment"; +/* Sentence fragment. +The full phrase is 'Comments on' followed by the title of the post on a separate line. */ +"comment.header.subText.commentThread" = "Comments on"; + +/* Provides a hint that the current screen displays a comment on a post. +The title of the post will be displayed below this text. +Example: Comment on + My First Post */ +"comment.header.subText.post" = "Comment on"; + +/* Provides a hint that the current screen displays a reply to a comment. +%1$@ is a placeholder for the comment author's name that's been replied to. +Example: Reply to Pamela Nguyen */ +"comment.header.subText.reply" = "Reply to %1$@"; + +/* Error message informing a user about an invalid password. */ +"common.reEnterPasswordMessage" = "The username or password stored in the app may be out of date. Please re-enter your password in the settings and try again."; + +/* An error message. */ +"common.unableToConnect" = "Unable to Connect"; + +/* Footnote for the privacy compliance popover. */ +"compliance.analytics.popover.footnote" = "These cookies allow us to optimize performance by collecting information on how users interact with our websites."; + +/* Save Button Title for the privacy compliance popover. */ +"compliance.analytics.popover.save.button" = "Save"; + +/* Settings Button Title for the privacy compliance popover. */ +"compliance.analytics.popover.settings.button" = "Go to Settings"; + +/* Subtitle for the privacy compliance popover. */ +"compliance.analytics.popover.subtitle" = "We process your personal data to optimize our website and marketing activities based on your consent and our legitimate interest."; + +/* Title for the privacy compliance popover. */ +"compliance.analytics.popover.title" = "Manage privacy"; + +/* Toggle Title for the privacy compliance popover. */ +"compliance.analytics.popover.toggle" = "Analytics"; + /* The menu item to select during a guided tour. */ "connections" = "connections"; @@ -9471,39 +9478,309 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Customize Insights button title */ "customizeInsightsCell.tryItButton.title" = "Try it now"; -/* No comment provided by engineer. */ -"double-tap to change unit" = "double tap to change unit"; +/* Title for an empty state view when no cards are displayed */ +"dasboard.emptyView.subtitle" = "Add cards that fit your needs to see information about your site."; -/* Register Domain - Address information field Number placeholder */ -"eg. 1122334455" = "eg. 1122334455"; +/* Title for an empty state view when no cards are displayed */ +"dasboard.emptyView.title" = "No cards to display"; -/* Register Domain - Address information field Country Code placeholder */ -"eg. 44" = "eg. 44"; +/* Personialize home tab button title */ +"dasboard.personalizeHomeButtonTitle" = "Personalize your home tab"; -/* Accessibility label for more button in dashboard quick start card. */ -"ellipsisButton.AccessibilityLabel" = "More"; +/* Title for a menu action in the context menu on the Jetpack Social dashboard card. */ +"dashboard.card.social.menu.hide" = "Hide this"; -/* Placeholder for the site url textfield. - Provides a sample of what a domain name looks like. - Site Address placeholder */ -"example.com" = "example.com"; +/* Title for the Jetpack Social dashboard card when the user has no social connections. */ +"dashboard.card.social.noconnections.title" = "Share across your social networks"; -/* Displayed in the confirmation alert when marking follow notifications as read. */ -"follow" = "follow"; +/* Title for the Jetpack Social dashboard card when the user has no social shares left. */ +"dashboard.card.social.noshares.title" = "You’re out of shares!"; -/* Title for button that will open up the blogging reminders screen. */ -"growAudienceCell.bloggingReminders.actionButton" = "Set up blogging reminders"; +/* Title for comments button on dashboard. */ +"dashboard.menu.comments" = "Comments"; -/* Title for button that will open up the blogging reminders screen. */ -"growAudienceCell.bloggingReminders.completed.actionButton" = "Edit reminders"; +/* Title for media button on dashboard. */ +"dashboard.menu.media" = "Media"; -/* A detailed message to users indicating that they've set up blogging reminders. */ -"growAudienceCell.bloggingReminders.completed.details" = "Keep blogging and check back to see visitors arriving at your site."; +/* Title for more button on dashboard. */ +"dashboard.menu.more" = "More"; -/* A hint to users that they've set up blogging reminders. */ -"growAudienceCell.bloggingReminders.completed.tip" = "You set up blogging reminders"; +/* Title for pages button on dashboard. */ +"dashboard.menu.pages" = "Pages"; -/* A detailed message to users about growing the audience for their site through blogging reminders. */ +/* Title for posts button on dashboard. */ +"dashboard.menu.posts" = "Posts"; + +/* Title for stats button on dashboard. */ +"dashboard.menu.stats" = "Stats"; + +/* Title for the Activity Log dashboard card context menu item that navigates the user to the full Activity Logs screen. */ +"dashboardCard.ActivityLog.contextMenu.allActivity" = "All activity"; + +/* Title for the Activity Log dashboard card. */ +"dashboardCard.ActivityLog.title" = "Recent activity"; + +/* Title for an action that opens the full pages list. */ +"dashboardCard.Pages.contextMenu.allPages" = "All pages"; + +/* Title for the Pages dashboard card. */ +"dashboardCard.Pages.title" = "Pages"; + +/* Title for impressions stats view */ +"dashboardCard.blazeCampaigns.clicks" = "Clicks"; + +/* Title of a button that starts the campaign creation flow. */ +"dashboardCard.blazeCampaigns.createCampaignButton" = "Create campaign"; + +/* Title for impressions stats view */ +"dashboardCard.blazeCampaigns.impressions" = "Impressions"; + +/* Title for the Learn more button in the More menu. */ +"dashboardCard.blazeCampaigns.learnMore" = "Learn more"; + +/* Title for the card displaying blaze campaigns. */ +"dashboardCard.blazeCampaigns.title" = "Blaze campaign"; + +/* Title for the View All Campaigns button in the More menu */ +"dashboardCard.blazeCampaigns.viewAllCampaigns" = "View all campaigns"; + +/* Title of a button that starts the page creation flow. */ +"dashboardCard.pages.add.button.title" = "Add pages to your site"; + +/* Title of label marking a draft page */ +"dashboardCard.pages.cell.status.draft" = "Draft"; + +/* Title of label marking a published page */ +"dashboardCard.pages.cell.status.publish" = "Published"; + +/* Title of label marking a scheduled page */ +"dashboardCard.pages.cell.status.schedule" = "Scheduled"; + +/* Title of a button that starts the page creation flow. */ +"dashboardCard.pages.create.button.title" = "Create another page"; + +/* Title of a label that encourages the user to create a new page. */ +"dashboardCard.pages.create.description" = "Start with bespoke, mobile friendly layouts."; + +/* Title for the View stats button in the More menu */ +"dashboardCard.stats.viewStats" = "View stats"; + +/* Feature flags menu item */ +"debugMenu.featureFlags" = "Feature Flags"; + +/* General section title */ +"debugMenu.generalSectionTitle" = "General"; + +/* Remote config params debug menu footer explaining the meaning of a cell with a checkmark. */ +"debugMenu.remoteConfig.footer" = "Overridden parameters are denoted by a checkmark."; + +/* Hint for overriding remote config params */ +"debugMenu.remoteConfig.hint" = "Override the chosen param by defining a new value here."; + +/* Placeholder for overriding remote config params */ +"debugMenu.remoteConfig.placeholder" = "No remote or default value"; + +/* Remote Config debug menu title */ +"debugMenu.remoteConfig.title" = "Remote Config"; + +/* Remove current quick start tour menu item */ +"debugMenu.removeQuickStart" = "Remove Current Tour"; + +/* Title for a menu action in the context menu on the Jetpack install card. */ +"domain.dashboard.card.menu.hide" = "Hide this"; + +/* Search domain - Title for the Suggested domains screen */ +"domain.management.addDomain.search.title" = "Search for a domain"; + +/* Domain management buy domain card button title */ +"domain.management.buy.card.button.title" = "Get Domain"; + +/* Domain management buy domain card subtitle */ +"domain.management.buy.card.subtitle" = "Add a site later."; + +/* Domain management buy domain card title */ +"domain.management.buy.card.title" = "Just buy a domain"; + +/* The expired label of the domain card in All Domains screen. */ +"domain.management.card.expired.label" = "Expired"; + +/* The renews label of the domain card in All Domains screen. */ +"domain.management.card.renews.label" = "Renews"; + +/* The empty state button title in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.button.title" = "Find a domain"; + +/* The empty state description in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.description" = "Tap below to find your perfect domain."; + +/* The empty state title in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.title" = "You don't have any domains"; + +/* The empty state description in All Domains screen when an error occurs */ +"domain.management.error.empty.state.description" = "We encountered an error while loading your domains. Please contact support if the issue persists."; + +/* The empty state title in All Domains screen when an error occurs */ +"domain.management.error.empty.state.title" = "Something went wrong"; + +/* The empty state button title in All Domains screen when an error occurs */ +"domain.management.error.state.button.title" = "Try again"; + +/* The empty state description in All Domains screen when the user is offline */ +"domain.management.offline.empty.state.description" = "Please check your network connection and try again."; + +/* The empty state title in All Domains screen when the user is offline */ +"domain.management.offline.empty.state.title" = "No Internet Connection"; + +/* Domain management choose site card button title */ +"domain.management.purchase.footer" = "*A free domain for one year is included with all paid annual plans"; + +/* Domain management purchase domain screen title */ +"domain.management.purchase.subtitle" = "Don't worry, you can easily add a site later."; + +/* Domain management purchase domain screen title. */ +"domain.management.purchase.title" = "Choose how to use your domain"; + +/* The search bar title in All Domains screen. */ +"domain.management.search-bar.title" = "Search domains"; + +/* The empty state description in All Domains screen when the are no domains matching the search criteria */ +"domain.management.search.empty.state.description" = "We couldn't find any domains that match your search for '%@'"; + +/* The empty state title in All Domains screen when the are no domains matching the search criteria */ +"domain.management.search.empty.state.title" = "No Matching Domains Found"; + +/* Domain management choose site card button title */ +"domain.management.site.card.button.title" = "Choose Site"; + +/* Domain management choose site card subtitle */ +"domain.management.site.card.footer" = "Free domain for the first year*"; + +/* Domain management choose site card subtitle */ +"domain.management.site.card.subtitle" = "Use with a site you already started."; + +/* Domain management choose site card title */ +"domain.management.site.card.title" = "Existing WordPress.com site"; + +/* Domain Management Screen Title */ +"domain.management.title" = "All Domains"; + +/* Domain Purchase Completion footer */ +"domain.purchase.preview.footer" = "It may take up to 30 minutes for your custom domain to start working."; + +/* Domain Purchase Completion description (only for FREE domains). */ +"domain.purchase.preview.free.description" = "Next, we'll help you get it ready to be browsed."; + +/* Domain Purchase Completion description (only for PAID domains). */ +"domain.purchase.preview.paid.description" = "We’ve emailed your receipt. Next, we'll help you get it ready for everyone."; + +/* Reflects that site is live when domain purchase feature flag is ON. */ +"domain.purchase.preview.title" = "Kudos, your site is live!"; + +/* The 'Best Alternative' label under the domain name in 'Choose a domain' screen */ +"domain.suggestions.row.best-alternative" = "Best Alternative"; + +/* The text to display for paid domains on sale in 'Site Creation > Choose a domain' screen */ +"domain.suggestions.row.first-year" = "for the first year"; + +/* The text to display for free domains in 'Site Creation > Choose a domain' screen */ +"domain.suggestions.row.free" = "Free"; + +/* The text to display for paid domains that are free for the first year with the paid plan in 'Site Creation > Choose a domain' screen */ +"domain.suggestions.row.free-with-plan" = "Free for the first year with annual paid plans"; + +/* The 'Recommended' label under the domain name in 'Choose a domain' screen */ +"domain.suggestions.row.recommended" = "Recommended"; + +/* The 'Sale' label under the domain name in 'Choose a domain' screen */ +"domain.suggestions.row.sale" = "Sale"; + +/* The text to display for paid domains in 'Site Creation > Choose a domain' screen */ +"domain.suggestions.row.yearly" = "per year"; + +/* Title for the checkout screen. */ +"domains.checkout.title" = "Checkout"; + +/* Action shown in a bottom notice to dismiss it. */ +"domains.failure.dismiss" = "Dismiss"; + +/* Content show when the domain selection action fails. */ +"domains.failure.title" = "Sorry, the domain you are trying to add cannot be bought on the Jetpack app at this time."; + +/* Title for the screen where the user can choose how to use the domain they're end up purchasing. */ +"domains.purchase.choice.title" = "Purchase Domain"; + +/* Back button title that navigates back to the search domains screen. */ +"domains.search.backButton.title" = "Search"; + +/* Title of screen where user chooses a site to connect to their selected domain */ +"domains.sitePicker.title" = "Choose Site"; + +/* No comment provided by engineer. */ +"double-tap to change unit" = "double tap to change unit"; + +/* Register Domain - Address information field Number placeholder */ +"eg. 1122334455" = "eg. 1122334455"; + +/* Register Domain - Address information field Country Code placeholder */ +"eg. 44" = "eg. 44"; + +/* Accessibility label for more button in dashboard quick start card. */ +"ellipsisButton.AccessibilityLabel" = "More"; + +/* Placeholder for the site url textfield. + Provides a sample of what a domain name looks like. + Site Address placeholder */ +"example.com" = "example.com"; + +/* Title of screen the displays the details of an advertisement campaign. */ +"feature.blaze.campaignDetails.title" = "Campaign Details"; + +/* Name of a feature that allows the user to promote their posts. */ +"feature.blaze.title" = "Blaze"; + +/* Displayed in the confirmation alert when marking follow notifications as read. */ +"follow" = "follow"; + +/* Description for the Free to Paid plans dashboard card. */ +"freeToPaidPlans.dashboard.card.description" = "Get a free domain for the first year, remove ads on your site, and increase your storage."; + +/* Title for a menu action in the context menu on the Free to Paid plans dashboard card. */ +"freeToPaidPlans.dashboard.card.menu.hide" = "Hide this"; + +/* Title for the Free to Paid plans dashboard card. */ +"freeToPaidPlans.dashboard.card.shortTitle" = "Free domain with an annual plan"; + +/* Done button title on the domain purchase result screen. Closes the screen. */ +"freeToPaidPlans.resultView.done" = "Done"; + +/* Notice on the domain purchase result screen. Tells user how long it might take for their domain to be ready. */ +"freeToPaidPlans.resultView.notice" = "It may take up to 30 minutes for your domain to start working properly"; + +/* Sub-title for the domain purchase result screen. Tells user their domain is being set up. */ +"freeToPaidPlans.resultView.subtitle" = "Your new domain %@ is being set up."; + +/* Title for the domain purchase result screen. Tells user their domain was obtained. */ +"freeToPaidPlans.resultView.title" = "All ready to go!"; + +/* A generic error message for a footer view in a list with pagination */ +"general.pagingFooterView.errorMessage" = "An error occurred"; + +/* A footer retry button */ +"general.pagingFooterView.retry" = "Retry"; + +/* Title for button that will open up the blogging reminders screen. */ +"growAudienceCell.bloggingReminders.actionButton" = "Set up blogging reminders"; + +/* Title for button that will open up the blogging reminders screen. */ +"growAudienceCell.bloggingReminders.completed.actionButton" = "Edit reminders"; + +/* A detailed message to users indicating that they've set up blogging reminders. */ +"growAudienceCell.bloggingReminders.completed.details" = "Keep blogging and check back to see visitors arriving at your site."; + +/* A hint to users that they've set up blogging reminders. */ +"growAudienceCell.bloggingReminders.completed.tip" = "You set up blogging reminders"; + +/* A detailed message to users about growing the audience for their site through blogging reminders. */ "growAudienceCell.bloggingReminders.details" = "Posting regularly can help build an audience. Reminders help keep you on track."; /* Title for button that will dismiss the Grow Your Audience card. */ @@ -9554,9 +9831,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* (placeholder) Help the user enter a URL into the field */ "http:\/\/my-site-address (URL)" = "http:\/\/my-site-address (URL)"; -/* Label displayed on image media items. */ -"image" = "image"; - /* Sentence to justify why the app is asking permission from the user to use their camera. */ "infoplist.NSCameraUsageDescription" = "To take photos or videos to use in your posts."; @@ -9617,6 +9891,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of a badge indicating when a feature in singular form will be removed. First argument is the feature name. Second argument is the number of days/weeks it will be removed in. Ex: Reader is moving in 2 weeks */ "jetpack.branding.badge_banner.moving_in.singular" = "%1$@ is moving in %2$@"; +/* Title of a badge or banner indicating that this feature will be moved in a few days. */ +"jetpack.branding.badge_banner.moving_in_days.plural" = "Moving to the Jetpack app in a few days."; + /* Title of a badge indicating that a feature in plural form will be removed soon. First argument is the feature name. Ex: Notifications are moving soon */ "jetpack.branding.badge_banner.moving_soon.plural" = "%@ are moving soon"; @@ -9647,6 +9924,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of a button that displays a blog post in a web view. */ "jetpack.fullscreen.overlay.learnMore" = "Learn more at jetpack.com"; +/* Description of the Notifications feature. */ +"jetpack.fullscreen.overlay.newUsers.notifications.subtitle" = "Get notifications for new comments, likes, views, and more."; + +/* Description of the Reader feature. */ +"jetpack.fullscreen.overlay.newUsers.reader.subtitle" = "Find and follow your favourite sites and communities, and share you content."; + /* Description of the Statistics feature. */ "jetpack.fullscreen.overlay.newUsers.stats.subtitle" = "Watch your traffic grow with helpful insights and comprehensive stats."; @@ -9671,6 +9954,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of a screen that prompts the user to switch the Jetpack app. */ "jetpack.fullscreen.overlay.phaseFour.title" = "Jetpack features have moved."; +/* Subtitle of a screen displayed when the user accesses the Notifications screen from the WordPress app. The screen showcases the Jetpack app. */ +"jetpack.fullscreen.overlay.phaseOne.notifications.subtitle" = "Switch to the Jetpack app to keep receiving real-time notifications on your device."; + /* Title of a screen displayed when the user accesses the Notifications screen from the WordPress app. The screen showcases the Jetpack app. */ "jetpack.fullscreen.overlay.phaseOne.notifications.title" = "Get your notifications with the Jetpack app"; @@ -9737,6 +10023,21 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of a button that dismisses an overlay and displays the Stats screen. */ "jetpack.fullscreen.overlay.stats.continue.title" = "Continue to Stats"; +/* The description text shown after the user has successfully installed the Jetpack plugin. */ +"jetpack.install-flow.success.description" = "Ready to use this site with the app."; + +/* Title of the primary button shown after the Jetpack plugin has been installed. Tapping on the button dismisses the installation screen. */ +"jetpack.install-flow.success.primaryButtonText" = "Done"; + +/* Title of a button for connecting user account to Jetpack. */ +"jetpack.install.connectUser.button.title" = "Connect your user account"; + +/* Message asking the user if they want to set up Jetpack from notifications */ +"jetpack.install.connectUser.notifications.description" = "To get helpful notifications on your phone from your WordPress site, you'll need to connect to your user account."; + +/* Message asking the user if they want to set up Jetpack from stats by connecting their user account */ +"jetpack.install.connectUser.stats.description" = "To use stats on your site, you'll need to connect the Jetpack plugin to your user account."; + /* Description inside a menu card communicating that features are moving to the Jetpack app. */ "jetpack.menuCard.description" = "Stats, Reader, Notifications and other features will move to the Jetpack mobile app soon."; @@ -9755,6 +10056,24 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Menu item title to hide the card for now and show it later. */ "jetpack.menuCard.remindLater" = "Remind me later"; +/* Jetpack Plugin Modal footnote */ +"jetpack.plugin.modal.footnote" = "By setting up Jetpack you agree to our %@"; + +/* Jetpack Plugin Modal primary button title */ +"jetpack.plugin.modal.primary.button.title" = "Install the full plugin"; + +/* Jetpack Plugin Modal secondary button title */ +"jetpack.plugin.modal.secondary.button.title" = "Contact Support"; + +/* The 'full Jetpack plugin' string in the subtitle */ +"jetpack.plugin.modal.subtitle.jetpack.plugin" = "full Jetpack plugin"; + +/* Jetpack Plugin Modal footnote terms and conditions */ +"jetpack.plugin.modal.terms" = "Terms and Conditions"; + +/* Jetpack Plugin Modal title */ +"jetpack.plugin.modal.title" = "Please install the full Jetpack plugin"; + /* Add an author prompt for the jetpack prologue */ "jetpack.prologue.prompt.addAuthor" = "Add an author"; @@ -9791,6 +10110,18 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Write a blog prompt for the jetpack prologue */ "jetpack.prologue.prompt.writeBlog" = "Write a blog"; +/* Title for a call-to-action button on the Jetpack install card. */ +"jetpackinstallcard.button.learn" = "Learn more"; + +/* Title for a menu action in the context menu on the Jetpack install card. */ +"jetpackinstallcard.menu.hide" = "Hide this"; + +/* Text displayed in the Jetpack install card on the Home screen and Menu screen when a user has an individual Jetpack plugin installed but not the full plugin. %1$@ is a placeholder for the plugin the user has installed. %1$@ is bold. */ +"jetpackinstallcard.notice.individual" = "This site is using the %1$@ plugin, which doesn't support all features of the app yet. Please install the full Jetpack plugin."; + +/* Text displayed in the Jetpack install card on the Home screen and Menu screen when a user has multiple installed individual Jetpack plugins but not the full plugin. */ +"jetpackinstallcard.notice.multiple" = "This site is using individual Jetpack plugins, which don’t support all features of the app yet. Please install the full Jetpack plugin."; + /* Later today */ "later today" = "later today"; @@ -9800,23 +10131,149 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Indicating that referrer was marked as spam */ "marked as spam" = "marked as spam"; -/* Verb. User action to dismiss error alert when failing to load media item. */ -"mediaItemTable.errorAlert.dismissButton" = "Dismiss"; +/* Products header text in Me Screen. */ +"me.products.header" = "Products"; + +/* Title of error prompt shown when a sync fails. */ +"media.syncFailed" = "Unable to sync media"; + +/* An error message the app shows if media import fails */ +"mediaExporter.error.unknown" = "The item could not be added to the Media library"; + +/* An error message the app shows if media import fails */ +"mediaExporter.error.unsupportedContentType" = "Unsupported content type"; + +/* Message of an alert informing users that the video they are trying to select is not allowed. */ +"mediaExporter.videoLimitExceededError" = "Uploading videos longer than 5 minutes requires a paid plan."; + +/* Accessibility hint for add button to add items to the user's media library */ +"mediaLibrary.addButtonAccessibilityHint" = "Add new media"; + +/* Accessibility label for add button to add items to the user's media library */ +"mediaLibrary.addButtonAccessibilityLabel" = "Add"; + +/* Button name in the more menu */ +"mediaLibrary.aspectRatioGrid" = "Aspect Ratio Grid"; + +/* Context menu button */ +"mediaLibrary.buttonDelete" = "Delete"; + +/* Media screen navigation bar button Select title */ +"mediaLibrary.buttonSelect" = "Select"; + +/* Context menu button */ +"mediaLibrary.buttonShare" = "Share"; + +/* Verb. Button title. Tapping cancels an action. */ +"mediaLibrary.deleteConfirmationCancel" = "Cancel"; + +/* Title for button that permanently deletes one or more media items (photos / videos) */ +"mediaLibrary.deleteConfirmationConfirm" = "Delete"; + +/* Message prompting the user to confirm that they want to permanently delete a group of media items. */ +"mediaLibrary.deleteConfirmationMessageMany" = "Are you sure you want to permanently delete these items?"; + +/* Message prompting the user to confirm that they want to permanently delete a media item. Should match Calypso. */ +"mediaLibrary.deleteConfirmationMessageOne" = "Are you sure you want to permanently delete this item?"; + +/* Text displayed in HUD if there was an error attempting to delete a group of media items. */ +"mediaLibrary.deletionFailureMessage" = "Unable to delete all media items."; + +/* Text displayed in HUD while a media item is being deleted. */ +"mediaLibrary.deletionProgressViewTitle" = "Deleting..."; + +/* Text displayed in HUD after successfully deleting a media item */ +"mediaLibrary.deletionSuccessMessage" = "Deleted!"; + +/* User action to delete un-uploaded media. */ +"mediaLibrary.retryOptionsAlert.delete" = "Delete"; /* Verb. Button title. Tapping dismisses a prompt. */ "mediaLibrary.retryOptionsAlert.dismissButton" = "Dismiss"; +/* User action to retry media upload. */ +"mediaLibrary.retryOptionsAlert.retry" = "Retry Upload"; + +/* Message displayed when no results are returned from a media library search. Should match Calypso. */ +"mediaLibrary.searchResultsEmptyTitle" = "No media matching your search"; + +/* Text displayed in HUD if there was an error attempting to share a group of media items. */ +"mediaLibrary.sharingFailureMessage" = "Unable to share the selected items."; + +/* Button name in the more menu */ +"mediaLibrary.squareGrid" = "Square Grid"; + +/* Media screen navigation title */ +"mediaLibrary.title" = "Media"; + +/* Bottom toolbar title in the selection mode */ +"mediaLibrary.toolbarSelectImagesMany" = "%d Images Selected"; + +/* Bottom toolbar title in the selection mode */ +"mediaLibrary.toolbarSelectImagesOne" = "1 Image Selected"; + +/* Bottom toolbar title in the selection mode */ +"mediaLibrary.toolbarSelectItemsMany" = "%d Items Selected"; + +/* Bottom toolbar title in the selection mode */ +"mediaLibrary.toolbarSelectItemsOne" = "1 Item Selected"; + +/* Bottom toolbar title in the selection mode */ +"mediaLibrary.toolbarSelectItemsPrompt" = "Select Items"; + /* The title of the button to dismiss the alert shown when the picked media cannot be imported into stories. */ "mediaPicker.failedMediaExportAlert.dismissButton" = "Dismiss"; +/* Message for alert when access to camera is not granted */ +"mediaPicker.noCameraAccessMessage" = "This app needs permission to access the Camera to capture new media, please change the privacy settings if you wish to allow this."; + +/* Title for alert when access to camera is not granted */ +"mediaPicker.noCameraAccessTitle" = "Media Capture"; + +/* Button that opens the Settings app */ +"mediaPicker.openSettings" = "Open Settings"; + +/* The name of the action in the context menu for selecting photos from Tenor (free GIF library) */ +"mediaPicker.pickFromFreeGIFLibrary" = "Free GIF Library"; + +/* The name of the action in the context menu (user's WordPress Media Library */ +"mediaPicker.pickFromMediaLibrary" = "Choose from Media"; + +/* The name of the action in the context menu for selecting photos from other apps (Files app) */ +"mediaPicker.pickFromOtherApps" = "Other Files"; + +/* The name of the action in the context menu */ +"mediaPicker.pickFromPhotosLibrary" = "Choose from Device"; + +/* The name of the action in the context menu for selecting photos from free stock photos */ +"mediaPicker.pickFromStockPhotos" = "Free Photo Library"; + +/* The name of the action in the context menu */ +"mediaPicker.takePhoto" = "Take Photo"; + +/* The name of the action in the context menu */ +"mediaPicker.takePhotoOrVideo" = "Take Photo or Video"; + +/* The name of the action in the context menu */ +"mediaPicker.takeVideo" = "Take Video"; + +/* The description in the Delete WordPress screen */ +"migration.deleteWordpress.description" = "It looks like you still have the WordPress app installed."; + /* The primary button title in the Delete WordPress screen */ "migration.deleteWordpress.primaryButton" = "Got it"; /* The secondary button title in the Delete WordPress screen */ "migration.deleteWordpress.secondaryButton" = "Need help?"; -/* Primary button title in the migration done screen. */ -"migration.done.actions.primary.title" = "Finish"; +/* The title in the Delete WordPress screen */ +"migration.deleteWordpress.title" = "You no longer need the WordPress app on your device"; + +/* Footer for the migration done screen. */ +"migration.done.footer" = "We recommend uninstalling the WordPress app on your device to avoid data conflicts."; + +/* Highlighted text in the footer of the migration done screen. */ +"migration.done.footer.highlighted" = "uninstalling the WordPress app"; /* Primary description in the migration done screen. */ "migration.done.primaryDescription" = "We’ve transferred all your data and settings. Everything is right where you left it."; @@ -9869,21 +10326,228 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* The title in the migration welcome screen */ "migration.welcome.title" = "Welcome to Jetpack!"; +/* Description for the static screen displayed prompting users to switch the Jetpack app. */ +"movedToJetpack.description" = "The Jetpack app has all the WordPress app’s functionality, and now exclusive access to Stats, Reader, Notifications and more."; + +/* Hint for the static screen displayed prompting users to switch the Jetpack app. */ +"movedToJetpack.hint" = "Switching is free and only takes a minute."; + +/* Title for a button that prompts users to switch to the Jetpack app. */ +"movedToJetpack.jetpackButtonTitle" = "Switch to the Jetpack app"; + +/* Title for a button that displays a blog post in a web view. */ +"movedToJetpack.learnMoreButtonTitle" = "Learn more at jetpack.com"; + +/* Title for the static screen displayed in the Stats screen prompting users to switch to the Jetpack app. */ +"movedToJetpack.notifications.title" = "Use WordPress with Notifications in the Jetpack app."; + +/* Title for the static screen displayed in the Reader screen prompting users to switch to the Jetpack app. */ +"movedToJetpack.reader.title" = "Use WordPress with Reader in the Jetpack app."; + +/* Title for the static screen displayed in the Stats screen prompting users to switch to the Jetpack app. */ +"movedToJetpack.stats.title" = "Use WordPress with Stats in the Jetpack app."; + +/* Section title for the content table section in the blog details screen */ +"my-site.menu.content.section.title" = "Content"; + +/* Section title for the maintenance table section in the blog details screen */ +"my-site.menu.maintenance.section.title" = "Maintenance"; + +/* Title for the social row in the blog details screen */ +"my-site.menu.social.row.title" = "Social"; + +/* Section title for the traffic table section in the blog details screen */ +"my-site.menu.traffic.section.title" = "Traffic"; + +/* Title for the card displaying draft posts. */ +"my-sites.drafts.card.title" = "Work on a draft post"; + +/* Title for the View all drafts button in the More menu */ +"my-sites.drafts.card.viewAllDrafts" = "View all drafts"; + +/* Title for the View all scheduled drafts button in the More menu */ +"my-sites.scheduled.card.viewAllScheduledPosts" = "View all scheduled posts"; + +/* Title for the card displaying today's stats. */ +"my-sites.stats.card.title" = "Today's Stats"; + +/* Title for the domain focus card on My Site */ +"mySite.domain.focus.cardCell.title" = "News"; + +/* Button title of the domain focus card on My Site */ +"mySite.domain.focus.cardView.button.title" = "Transfer your domains"; + +/* Description of the domain focus card on My Site */ +"mySite.domain.focus.cardView.description" = "As you may know, Google Domains has been sold to Squarespace. Transfer your domains to WordPress.com now, and we'll pay all transfer fees plus an extra year of your domain registration."; + +/* Title of the domain focus card on My Site */ +"mySite.domain.focus.cardView.title" = "Reclaim your Google Domains"; + +/* Action sheet button title. Launches the flow to a add self-hosted site. */ +"mySite.noSites.actionSheet.addSelfHostedSite" = "Add self-hosted site"; + +/* Action sheet button title. Launches the flow to create a WordPress.com site. */ +"mySite.noSites.actionSheet.createWPComSite" = "Create WordPress.com site"; + +/* Button title. Displays the account and setting screen. */ +"mySite.noSites.button.accountAndSettings" = "Account and settings"; + +/* Button title. Displays a screen to add a new site when tapped. */ +"mySite.noSites.button.addNewSite" = "Add new site"; + +/* Message description for when a user has no sites. */ +"mySite.noSites.description" = "Create a new site for your business, magazine, or personal blog; or connect an existing WordPress installation."; + +/* Message title for when a user has no sites. */ +"mySite.noSites.title" = "You don't have any sites"; + /* Dismiss button title. */ "noResultsViewController.dismissButton" = "Dismiss"; -/* Word separating the current index from the total amount. I.e.: 7 of 9 */ -"of" = "of"; +/* This is one of the buttons we display inside of the prompt to review the app */ +"notifications.appRatings.prompt.no.buttonTitle" = "Could improve"; + +/* This is one of the buttons we display inside of the prompt to review the app */ +"notifications.appRatings.prompt.yes.buttonTitle" = "I like it"; + +/* This is one of the buttons we display when prompting the user for a review */ +"notifications.appRatings.sendFeedback.no.buttonTitle" = "No thanks"; + +/* This is one of the buttons we display when prompting the user for a review */ +"notifications.appRatings.sendFeedback.yes.buttonTitle" = "Send feedback"; + +/* Badge for page cells */ +"pageList.badgeHomepage" = "Homepage"; + +/* Badge for page cells */ +"pageList.badgeLocalChanges" = "Local changes"; + +/* Badge for page cells */ +"pageList.badgePendingReview" = "Pending review"; + +/* Badge for page cells */ +"pageList.badgePosts" = "Posts page"; -/* Label displayed on media items that are not video, image, or audio. */ -"other" = "other"; +/* Badge for page cells */ +"pageList.badgePrivate" = "Private"; + +/* Subtitle of the theme template homepage cell */ +"pages.template.subtitle" = "Your homepage is using a Theme template and will open in the web editor."; + +/* Title of the theme template homepage cell */ +"pages.template.title" = "Homepage"; + +/* Message informing the user that their static homepage page was set successfully */ +"pages.updatePage.successTitle" = "Page successfully updated"; + +/* Delete option in the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.actionTitle" = "Delete Permanently"; + +/* Message of the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.alertMessage" = "Are you sure you want to permanently delete this page?"; + +/* Title of the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.alertTitle" = "Delete Permanently?"; + +/* Menu option for filtering posts by everyone */ +"pagesList.pagesByEveryone" = "Pages by everyone"; + +/* Menu option for filtering posts by me */ +"pagesList.pagesByMe" = "Pages by me"; + +/* Trash option in the trash page confirmation alert. */ +"pagesList.trash.actionTitle" = "Move to Trash"; + +/* Message of the trash page confirmation alert. */ +"pagesList.trash.alertMessage" = "Are you sure you want to trash this page?"; + +/* Title of the trash page confirmation alert. */ +"pagesList.trash.alertTitle" = "Trash this page?"; + +/* Cancels an Action */ +"pagesList.trash.cancel" = "Cancel"; /* No comment provided by engineer. */ "password" = "password"; +/* Section footer displayed below the list of toggles */ +"personalizeHome.cardsSectionFooter" = "Cards may show different content depending on what's happening on your site. We're working on more cards and controls."; + +/* Section header */ +"personalizeHome.cardsSectionHeader" = "Add or hide cards"; + +/* Card title for the pesonalization menu */ +"personalizeHome.dashboardCard.activityLog" = "Recent activity"; + +/* Card title for the pesonalization menu */ +"personalizeHome.dashboardCard.blaze" = "Blaze"; + +/* Card title for the pesonalization menu */ +"personalizeHome.dashboardCard.draftPosts" = "Draft posts"; + +/* Card title for the pesonalization menu */ +"personalizeHome.dashboardCard.pages" = "Pages"; + +/* Card title for the pesonalization menu */ +"personalizeHome.dashboardCard.prompts" = "Blogging prompts"; + +/* Card title for the pesonalization menu */ +"personalizeHome.dashboardCard.scheduledPosts" = "Scheduled posts"; + +/* Card title for the pesonalization menu */ +"personalizeHome.dashboardCard.todaysStats" = "Today's stats"; + +/* Section header for shortcuts */ +"personalizeHome.shortcutsSectionHeader" = "Show or hide shortcuts"; + +/* Page title */ +"personalizeHome.title" = "Personalize Home Tab"; + /* Register Domain - Domain contact information field Phone */ "phone number" = "phone number"; +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.createdTimeAgo" = "Created %@"; + +/* Status mesasge for post cells */ +"post.deletingPostPermanentlyStatusMessage" = "Deleting post..."; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.editedTimeAgo" = "Edited %@"; + +/* Status mesasge for post cells */ +"post.movingToTrashStatusMessage" = "Moving post to trash..."; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.publishedTimeAgo" = "Published %@"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.scheduledForDate" = "Scheduled %@"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.trashedTimeAgo" = "Trashed %@"; + +/* Accessibility label for the post author in the post list. The parameter is the author name. For example, \"By Elsa.\" */ +"postList.a11y.authorChunkFormat" = "By %@."; + +/* Accessibility label for a post's excerpt in the post list. The parameter is the post excerpt. For example, \"Excerpt. This is the first paragraph.\" */ +"postList.a11y.exerptChunkFormat" = "Excerpt. %@."; + +/* Accessibility label for a sticky post in the post list. */ +"postList.a11y.sticky" = "Sticky."; + +/* Accessibility label for a post in the post list. The first placeholder is the post title. The second placeholder is the date. */ +"postList.a11y.titleAndDateChunkFormat" = "%1$@, %2$@."; + +/* Title for the 'Trash' post list row swipe action */ +"postList.swipeActionDelete" = "Trash"; + +/* Title for the 'Share' post list row swipe action */ +"postList.swipeActionShare" = "Share"; + +/* Title for the 'View' post list row swipe action */ +"postList.swipeActionView" = "View"; + /* User action to dismiss featured media options. */ "postSettings.featuredImageUploadActionSheet.dismiss" = "Dismiss"; @@ -9896,6 +10560,163 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title for action sheet with featured media options. */ "postSettings.featuredImageUploadActionSheet.title" = "Featured Image Options"; +/* Section title for the disabled Twitter service in the Post Settings screen */ +"postSettings.section.disabledTwitter.header" = "Twitter Auto-Sharing Is No Longer Available"; + +/* Button in Post Settings */ +"postSettings.setFeaturedImageButton" = "Set Featured Image"; + +/* Error message on post/page settings screen */ +"postSettings.updateFailedMessage" = "Failed to update the post settings"; + +/* Promote the post with Blaze. */ +"posts.blaze.actionTitle" = "Promote with Blaze"; + +/* Label for the Post List option that cancels automatic uploading of a post. */ +"posts.cancelUpload.actionTitle" = "Cancel upload"; + +/* Label for post comments option. Tapping displays comments for a post. */ +"posts.comments.actionTitle" = "Comments"; + +/* Label for the delete post option. Tapping permanently deletes a post. */ +"posts.delete.actionTitle" = "Delete permanently"; + +/* Label for an option that moves a post to the draft folder */ +"posts.draft.actionTitle" = "Move to draft"; + +/* Label for post duplicate option. Tapping creates a copy of the post. */ +"posts.duplicate.actionTitle" = "Duplicate"; + +/* Opens a submenu for page attributes. */ +"posts.pageAttributes.actionTitle" = "Page attributes"; + +/* Label for the preview post button. Tapping displays the post as it appears on the web. */ +"posts.preview.actionTitle" = "Preview"; + +/* Label for an option that moves a publishes a post immediately */ +"posts.publish.actionTitle" = "Publish now"; + +/* Retry uploading the post. */ +"posts.retry.actionTitle" = "Retry"; + +/* Set the selected page as the homepage. */ +"posts.setHomepage.actionTitle" = "Set as homepage"; + +/* Set the parent page for the selected page. */ +"posts.setParent.actionTitle" = "Set parent"; + +/* Set the selected page as a posts page. */ +"posts.setPostsPage.actionTitle" = "Set as posts page"; + +/* Set the selected page as a regular page. */ +"posts.setRegularPage.actionTitle" = "Set as regular page"; + +/* Label for post settings option. Tapping displays settings for a post. */ +"posts.settings.actionTitle" = "Settings"; + +/* Share the post. */ +"posts.share.actionTitle" = "Share"; + +/* Label for post stats option. Tapping displays statistics for a post. */ +"posts.stats.actionTitle" = "Stats"; + +/* Label for a option that moves a post to the trash folder */ +"posts.trash.actionTitle" = "Move to trash"; + +/* Label for the view post button. Tapping displays the post as it appears on the web. */ +"posts.view.actionTitle" = "View"; + +/* A short message explaining that a page was deleted permanently. */ +"postsList.deletePage.message" = "Page deleted permanently"; + +/* A short message explaining that a post was deleted permanently. */ +"postsList.deletePost.message" = "Post deleted permanently"; + +/* A short message explaining that a page was moved to the trash bin. */ +"postsList.movePageToTrash.message" = "Page moved to trash"; + +/* A short message explaining that a post was moved to the trash bin. */ +"postsList.movePostToTrash.message" = "Post moved to trash"; + +/* Menu option for filtering posts by everyone */ +"postsList.postsByEveryone" = "Posts by everyone"; + +/* Menu option for filtering posts by me */ +"postsList.postsByMe" = "Posts by me"; + +/* Title for the button to subscribe to Jetpack Social on the remaining shares view */ +"postsettings.social.remainingshares.subscribe" = "Subscribe now to share more"; + +/* The second half of the remaining social shares a user has. This is only displayed when there is no social limit warning. */ +"postsettings.social.remainingshares.text.part" = " in the next 30 days"; + +/* Beginning text of the remaining social shares a user has left. %1$d is their current remaining shares. This text is combined with ' in the next 30 days' if there is no warning displayed. */ +"postsettings.social.shares.text.format" = "%1$d social shares remaining"; + +/* The primary label for the auto-sharing row on the pre-publishing sheet. +Indicates the number of social accounts that will be sharing the blog post. +%1$d is a placeholder for the number of social network accounts that will be auto-shared. +Example: Sharing to 3 accounts */ +"prepublishing.social.label.multipleConnections" = "Sharing to %1$d accounts"; + +/* The primary label for the auto-sharing row on the pre-publishing sheet. +Indicates the blog post will not be shared to any social accounts. */ +"prepublishing.social.label.notSharing" = "Not sharing to social"; + +/* The primary label for the auto-sharing row on the pre-publishing sheet. +Indicates the number of social accounts that will be sharing the blog post. +This string is displayed when some of the social accounts are turned off for auto-sharing. +%1$d is a placeholder for the number of social media accounts that will be sharing the blog post. +%2$d is a placeholder for the total number of social media accounts connected to the user's blog. +Example: Sharing to 2 of 3 accounts */ +"prepublishing.social.label.partialConnections" = "Sharing to %1$d of %2$d accounts"; + +/* The primary label for the auto-sharing row on the pre-publishing sheet. +Indicates the blog post will be shared to a social media account. +%1$@ is a placeholder for the account name. +Example: Sharing to @wordpress */ +"prepublishing.social.label.singleConnection" = "Sharing to %1$@"; + +/* A subtext that's shown below the primary label in the auto-sharing row on the pre-publishing sheet. +Informs the remaining limit for post auto-sharing. +%1$d is a placeholder for the remaining shares. +Example: 27 social shares remaining */ +"prepublishing.social.remainingShares.format" = "%1$d social shares remaining"; + +/* a VoiceOver description for the warning icon to hint that the remaining shares are low. */ +"prepublishing.social.warningIcon.accessibilityHint" = "Warning"; + +/* The navigation title for a screen that edits the sharing message for the post. */ +"prepublishing.socialAccounts.editMessage.navigationTitle" = "Customize message"; + +/* The label for a call-to-action button in the social accounts' footer section. */ +"prepublishing.socialAccounts.footer.button.text" = "Subscribe to share more"; + +/* Text shown below the list of social accounts to indicate how many social shares available for the site. +Note that the '30 days' part is intended to be a static value. +%1$d is a placeholder for the amount of remaining shares. +Example: 27 social shares remaining in the next 30 days */ +"prepublishing.socialAccounts.footer.remainingShares.text" = "%1$d social shares remaining in the next 30 days"; + +/* a VoiceOver description for the warning icon to hint that the remaining shares are low. */ +"prepublishing.socialAccounts.footer.warningIcon.accessibilityHint" = "Warning"; + +/* The label displayed for a table row that displays the sharing message for the post. +Tapping on this row allows the user to edit the sharing message. */ +"prepublishing.socialAccounts.message.label" = "Message"; + +/* The navigation title for the pre-publishing social accounts screen. */ +"prepublishing.socialAccounts.navigationTitle" = "Social"; + +/* Title for a tappable string that opens the reader with a prompts tag */ +"prompts.card.viewprompts.title" = "View all responses"; + +/* Subtitle of the notification when prompts are hidden from the dashboard card */ +"prompts.notification.removed.subtitle" = "Visit Site Settings to turn back on"; + +/* Title of the notification when prompts are hidden from the dashboard card */ +"prompts.notification.removed.title" = "Blogging Prompts hidden"; + /* Button label that dismisses the qr log in flow and returns the user back to the previous screen */ "qrLoginVerifyAuthorization.completedInstructions.dismiss" = "Dismiss"; @@ -9905,6 +10726,107 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title for the success view when the user has successfully logged in */ "qrLoginVerifyAuthorization.completedInstructions.title" = "You're logged in!"; +/* The quick tour actions item to select during a guided tour. */ +"quickStart.moreMenu" = "More"; + +/* Accessibility hint to inform that the author section can be tapped to see posts from the site. */ +"reader.detail.header.authorInfo.a11y.hint" = "Views posts from the site"; + +/* Title for the Comment button on the Reader Detail toolbar. +Note: Since the display space is limited, a short or concise translation is preferred. */ +"reader.detail.toolbar.comment.button" = "Comment"; + +/* Title for the Like button in the Reader Detail toolbar. +This is shown when the user has not liked the post yet. +Note: Since the display space is limited, a short or concise translation is preferred. */ +"reader.detail.toolbar.like.button" = "Like"; + +/* Accessibility hint for the Like button state. The button shows that the user has not liked the post, +but tapping on this button will add a Like to the post. */ +"reader.detail.toolbar.like.button.a11y.hint" = "Likes this post."; + +/* Title for the Like button in the Reader Detail toolbar. +This is shown when the user has already liked the post. +Note: Since the display space is limited, a short or concise translation is preferred. */ +"reader.detail.toolbar.liked.button" = "Liked"; + +/* Accessibility hint for the Liked button state. The button shows that the user has liked the post, +but tapping on this button will remove their like from the post. */ +"reader.detail.toolbar.liked.button.a11y.hint" = "Unlikes this post."; + +/* Accessibility hint for the 'Save Post' button. */ +"reader.detail.toolbar.save.button.a11y.hint" = "Saves this post for later."; + +/* Accessibility label for the 'Save Post' button. */ +"reader.detail.toolbar.save.button.a11y.label" = "Save post"; + +/* Accessibility hint for the 'Save Post' button when a post is already saved. */ +"reader.detail.toolbar.saved.button.a11y.hint" = "Unsaves this post."; + +/* Accessibility label for the 'Save Post' button when a post has been saved. */ +"reader.detail.toolbar.saved.button.a11y.label" = "Saved Post"; + +/* Reader search button accessibility label. */ +"reader.navigation.search.button.label" = "Search"; + +/* Reader settings button accessibility label. */ +"reader.navigation.settings.button.label" = "Reader Settings"; + +/* A default value used to fill in the site name when the followed site somehow has missing site name or URL. +Example: given a notice format "Following %@" and empty site name, this will be "Following this site". */ +"reader.notice.follow.site.unknown" = "this site"; + +/* Notice title when blocking a user fails. */ +"reader.notice.user.blocked" = "reader.notice.user.block.failed"; + +/* Text for the 'Comment' button on the reader post card cell. */ +"reader.post.button.comment" = "Comment"; + +/* Accessibility hint for the comment button on the reader post card cell */ +"reader.post.button.comment.accessibility.hint" = "Opens the comments for the post."; + +/* Text for the 'Like' button on the reader post card cell. */ +"reader.post.button.like" = "Like"; + +/* Accessibility hint for the like button on the reader post card cell */ +"reader.post.button.like.accessibility.hint" = "Likes the post."; + +/* Text for the 'Liked' button on the reader post card cell. */ +"reader.post.button.liked" = "Liked"; + +/* Accessibility hint for the liked button on the reader post card cell */ +"reader.post.button.liked.accessibility.hint" = "Unlikes the post."; + +/* Accessibility hint for the site header on the reader post card cell */ +"reader.post.button.menu.accessibility.hint" = "Opens a menu with more actions."; + +/* Accessibility label for the more menu button on the reader post card cell */ +"reader.post.button.menu.accessibility.label" = "More"; + +/* Text for the 'Reblog' button on the reader post card cell. */ +"reader.post.button.reblog" = "Reblog"; + +/* Accessibility hint for the reblog button on the reader post card cell */ +"reader.post.button.reblog.accessibility.hint" = "Reblogs the post."; + +/* Accessibility hint for the site header on the reader post card cell */ +"reader.post.header.accessibility.hint" = "Opens the site details for the post."; + +/* The title of a button that triggers blocking a user from the user's reader. */ +"reader.post.menu.block.user" = "Block this user"; + +/* The title of a button that removes a saved post. */ +"reader.post.menu.remove.post" = "Remove Saved Post"; + +/* The title of a button that triggers the reporting of a post's author. */ +"reader.post.menu.report.user" = "Report this user"; + +/* The title of a button that saves a post. */ +"reader.post.menu.save.post" = "Save"; + +/* The formatted number of posts and followers for a site. '%1$@' is a placeholder for the site post count. '%2$@' is a placeholder for the site follower count. Example: `5,000 posts • 10M followers` */ +"reader.site.header.counts" = "%1$@ posts • %2$@ followers"; + /* Spoken accessibility label */ "readerDetail.backButton.accessibilityLabel" = "Back"; @@ -9938,6 +10860,54 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title for the tooltip anchor. */ "readerDetail.tooltipAnchorTitle.accessibilityLabel" = "New"; +/* The button title for the transfer footer view in Register Domain screen */ +"register.domain.transfer.button.title" = "Transfer domain"; + +/* The title for the transfer footer view in Register Domain screen */ +"register.domain.transfer.title" = "Looking to transfer a domain you already own?"; + +/* Information of what related post are and how they are presented */ +"relatedPostsSettings.optionsFooter" = "Related Posts displays relevant content from your site below your posts."; + +/* Text for related post cell preview */ +"relatedPostsSettings.preview1.details" = "in \"Mobile\""; + +/* Text for related post cell preview */ +"relatedPostsSettings.preview1.title" = "Big iPhone\/iPad Update Now Available"; + +/* Text for related post cell preview */ +"relatedPostsSettings.preview2.details" = "in \"Apps\""; + +/* Text for related post cell preview */ +"relatedPostsSettings.preview2.title" = "The WordPress for Android App Gets a Big Facelift"; + +/* Text for related post cell preview */ +"relatedPostsSettings.preview3.details" = "in \"Upgrade\""; + +/* Text for related post cell preview */ +"relatedPostsSettings.preview3.title" = "Upgrade Focus: VideoPress For Weddings"; + +/* Section title for related posts section preview */ +"relatedPostsSettings.previewsHeaders" = "Preview"; + +/* Label for Related Post header preview */ +"relatedPostsSettings.relatedPostsHeader" = "Related Posts"; + +/* Message to show when setting save failed */ +"relatedPostsSettings.settingsUpdateFailed" = "Settings update failed"; + +/* Label for configuration switch to show/hide the header for the related posts section */ +"relatedPostsSettings.showHeader" = "Show Header"; + +/* Label for configuration switch to enable/disable related posts */ +"relatedPostsSettings.showRelatedPosts" = "Show Related Posts"; + +/* Label for configuration switch to show/hide images thumbnail for the related posts */ +"relatedPostsSettings.showThumbnail" = "Show Images"; + +/* Title for screen that allows configuration of your blog/site related posts settings. */ +"relatedPostsSettings.title" = "Related Posts"; + /* User action to dismiss media options. */ "shareExtension.editor.attachmentActions.dismiss" = "Dismiss"; @@ -9959,6 +10929,108 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Share extension error dialog title. */ "shareModularViewController.retryAlert.title" = "Sharing Error"; +/* Template site address for the search bar. */ +"site.cration.domain.site.address" = "https:\/\/yoursitename.com"; + +/* The error message to show in the 'Site Creation > Assembly Step' when the domain checkout fails for unknown reasons. */ +"site.creation.assembly.step.domain.checkout.error.subtitle" = "Your website has been created successfully, but we encountered an issue while preparing your custom domain for checkout. Please try again or contact support for assistance."; + +/* Site name description that sits in the template website view. */ +"site.creation.domain.tooltip.description" = "Like the example above, a domain allows people to find and visit your site from their web browser."; + +/* Site name that is placed in the tooltip view. */ +"site.creation.domain.tooltip.site.name" = "YourSiteName.com"; + +/* Back button title shown in Site Creation flow to come back from Plan selection to Domain selection */ +"siteCreation.domain.backButton.title" = "Domains"; + +/* Button to progress to the next step after selecting domain in Site Creation */ +"siteCreation.domains.buttons.selectDomain" = "Select domain"; + +/* Accessibility label for audio items in the media collection view. The parameter is the creation date of the audio. */ +"siteMedia.accessibilityLabelAudio" = "Audio, %@"; + +/* Accessibility label for other media items in the media collection view. The parameter is the filename file. */ +"siteMedia.accessibilityLabelDocument" = "Document, %@"; + +/* Accessibility label for image thumbnails in the media collection view. The parameter is the creation date of the image. */ +"siteMedia.accessibilityLabelImage" = "Image, %@"; + +/* Accessibility label for video thumbnails in the media collection view. The parameter is the creation date of the video. */ +"siteMedia.accessibilityLabelVideo" = "Video, %@"; + +/* Accessibility label to use when creation date from media asset is not know. */ +"siteMedia.accessibilityUnknownCreationDate" = "Unknown creation date"; + +/* Accessibility hint for actions when displaying media items. */ +"siteMedia.cellAccessibilityHint" = "Select media."; + +/* Media screen navigation title */ +"siteMediaPicker.title" = "Media"; + +/* Title for screen to select the privacy options for a blog */ +"siteSettings.privacy.title" = "Privacy"; + +/* Hint for users when hidden privacy setting is set */ +"siteVisibility.hidden.hint" = "Your site is hidden from visitors behind a \"Coming Soon\" notice until it is ready for viewing."; + +/* Text for privacy settings: Hidden */ +"siteVisibility.hidden.title" = "Hidden"; + +/* Hint for users when private privacy setting is set */ +"siteVisibility.private.hint" = "Your site is only visible to you and users you approve."; + +/* Text for privacy settings: Private */ +"siteVisibility.private.title" = "Private"; + +/* Hint for users when public privacy setting is set */ +"siteVisibility.public.hint" = "Your site is visible to everyone, and it may be indexed by search engines."; + +/* Text for privacy settings: Public */ +"siteVisibility.public.title" = "Public"; + +/* Text for unknown privacy setting */ +"siteVisibility.unknown.hint" = "Unknown"; + +/* Text for unknown privacy setting */ +"siteVisibility.unknown.title" = "Unknown"; + +/* Label for the blogging reminders setting */ +"sitesettings.reminders.title" = "Reminders"; + +/* Body text for the Jetpack Social no connection view */ +"social.noconnection.body" = "Increase your traffic by auto-sharing your posts with your friends on social media."; + +/* Title for the connect button to add social sharing for the Jetpack Social no connection view */ +"social.noconnection.connectAccounts" = "Connect accounts"; + +/* Accessibility label for the social media icons in the Jetpack Social no connection view */ +"social.noconnection.icons.accessibility.label" = "Social media icons"; + +/* Title for the not now button to hide the Jetpack Social no connection view */ +"social.noconnection.notnow" = "Not now"; + +/* Plural body text for the Jetpack Social no shares dashboard card. %1$d is the number of social accounts the user has. */ +"social.noshares.body.plural" = "Your posts won’t be shared to your %1$d social accounts."; + +/* Singular body text for the Jetpack Social no shares dashboard card. */ +"social.noshares.body.singular" = "Your posts won’t be shared to your social account."; + +/* Accessibility label for the social media icons in the Jetpack Social no shares dashboard card */ +"social.noshares.icons.accessibility.label" = "Social media icons"; + +/* Title for the button to subscribe to Jetpack Social on the no shares dashboard card */ +"social.noshares.subscribe" = "Subscribe to share more"; + +/* Section title for the disabled Twitter service in the Social screen */ +"social.section.disabledTwitter.header" = "Twitter Auto-Sharing Is No Longer Available"; + +/* Text for a hyperlink that allows the user to learn more about the Twitter deprecation. */ +"social.twitterDeprecation.link" = "Find out more"; + +/* A smallprint that hints the reason behind why Twitter is deprecated. */ +"social.twitterDeprecation.text" = "Twitter auto-sharing is no longer available due to Twitter's changes in terms and pricing."; + /* Accessibility label used for distinguishing Views and Visitors in the Stats → Insights Views Visitors Line chart. */ "stats.insights.accessibility.label.viewsVisitorsLastDays" = "Last 7-days"; @@ -10052,6 +11124,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Hint displayed on the 'Most Popular Time' stats card when a user's site hasn't yet received enough traffic. */ "stats.insights.mostPopularTime.noData" = "Not enough activity. Check back later when your site's had more visitors!"; +/* A hint shown to the user in stats informing the user how many likes one of their posts has received. The %1$@ placeholder will be replaced with the title of a post, the %2$@ with the number of likes. */ +"stats.insights.totalLikes.guideText.plural" = "Your latest post %1$@ has received %2$@ likes."; + +/* A hint shown to the user in stats informing the user that one of their posts has received a like. The %1$@ placeholder will be replaced with the title of a post, and the %2$@ will be replaced by the numeral one. */ +"stats.insights.totalLikes.guideText.singular" = "Your latest post %1$@ has received %2$@ like."; + /* Dismiss the AlertView */ "stockPhotos.strings.dismiss" = "Dismiss"; @@ -10061,6 +11139,96 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Section title for regular suggestions */ "suggestions.section.regular" = "Site members"; +/* Dismiss the current view */ +"support.button.close.title" = "Done"; + +/* Accessibility hint, informing user the button can be used to visit the Jetpack migration FAQ website. */ +"support.button.jetpackMigation.accessibilityHint" = "Tap to visit the Jetpack app FAQ in an external browser"; + +/* Option in Support view to visit the Jetpack migration FAQ website. */ +"support.button.jetpackMigration.title" = "Visit our FAQ"; + +/* Button for confirming logging out from WordPress.com account */ +"support.button.logOut.title" = "Log Out"; + +/* Accessibility hint, informing user the button can be used to visit the support forums website. */ +"support.button.visitForum.accessibilityHint" = "Tap to visit the community forum website in an external browser"; + +/* Option in Support view to visit the WordPress.org support forums. */ +"support.button.visitForum.title" = "Visit WordPress.org"; + +/* Indicator that the chat bot is processing user's input. */ +"support.chatBot.botThinkingIndicator" = "Thinking..."; + +/* Dismiss the current view */ +"support.chatBot.close.title" = "Close"; + +/* Button for users to contact the support team directly. */ +"support.chatBot.contactSupport" = "Contact support"; + +/* Initial message shown to the user when the chat starts. */ +"support.chatBot.firstMessage" = "Hi there, I'm the Jetpack AI Assistant.\\n\\nWhat can we help you with?\\n\\nIf I can't answer your question, I'll help you open a support ticket with our team!"; + +/* Placeholder text for the chat input field. */ +"support.chatBot.inputPlaceholder" = "Send a message..."; + +/* An example question shown to a user seeking support */ +"support.chatBot.questionFive" = "I forgot my login information"; + +/* An example question shown to a user seeking support */ +"support.chatBot.questionFour" = "Why can't I login?"; + +/* An example question shown to a user seeking support */ +"support.chatBot.questionOne" = "What is my site address?"; + +/* An example question shown to a user seeking support */ +"support.chatBot.questionSix" = "How can I use my custom domain in the app?"; + +/* An example question shown to a user seeking support */ +"support.chatBot.questionThree" = "I can't upload photos\/videos"; + +/* An example question shown to a user seeking support */ +"support.chatBot.questionTwo" = "Help, my site is down!"; + +/* Option for users to report a chat bot answer as inaccurate. */ +"support.chatBot.reportInaccuracy" = "Report as inaccurate"; + +/* Button title referring to the sources of information. */ +"support.chatBot.sources" = "Sources"; + +/* Prompt for users suggesting to select a default question from the list to start a support chat. */ +"support.chatBot.suggestionsPrompt" = "Not sure what to ask?"; + +/* Notice informing user that there was an error submitting their support ticket. */ +"support.chatBot.ticketCreationFailure" = "Error submitting support ticket"; + +/* Notice informing user that their support ticket is being created. */ +"support.chatBot.ticketCreationLoading" = "Creating support ticket..."; + +/* Notice informing user that their support ticket has been created. */ +"support.chatBot.ticketCreationSuccess" = "Ticket created"; + +/* Title of the view that shows support chat bot. */ +"support.chatBot.title" = "Contact Support"; + +/* A title for a text that displays a transcript of an answer in a support chat */ +"support.chatBot.zendesk.answer" = "Answer"; + +/* A title for a text that displays a transcript of user's question in a support chat */ +"support.chatBot.zendesk.question" = "Question"; + +/* A title for a text that displays a transcript from a conversation between Jetpack Mobile Bot (chat bot) and a user */ +"support.chatBot.zendesk.transcript" = "Jetpack Mobile Bot transcript"; + +/* Suggestion in Support view to visit the Forums. */ +"support.row.communityForum.title" = "Ask a question in the community forum and get help from our group of volunteers."; + +/* Accessibility hint describing what happens if the Contact Email button is tapped. */ +"support.row.contactEmail.accessibilityHint" = "Shows a dialogue for changing the Contact Email."; + +/* Display value for Support email field if there is no user email address. */ +"support.row.contactEmail.emailNoteSet.detail" = "Not Set"; + /* Support email label. */ "support.row.contactEmail.title" = "Email"; @@ -10079,6 +11247,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Option in Support view to launch the Help Center. */ "support.row.helpCenter.title" = "WordPress Help Center"; +/* An informational card description in Support view explaining what tapping the link on card does */ +"support.row.jetpackMigration.description" = "Our FAQ provides answers to common questions you may have."; + +/* An informational card title in Support view */ +"support.row.jetpackMigration.title" = "Thank you for switching to the Jetpack app!"; + /* Option in Support view to see activity logs. */ "support.row.logs.title" = "Logs"; @@ -10121,18 +11295,33 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Displayed in the confirmation alert when marking unread notifications as read. */ "unread" = "unread"; -/* Label displayed on video media items. */ -"video" = "video"; - /* Portion of a message for Jetpack users that have multisite WP installation, thus Restore is not available. This part is a link, colored with a different color. */ "visit our documentation page" = "visit our documentation page"; /* Verb. Dismiss the web view screen. */ "webKit.button.dismiss" = "Dismiss"; +/* Preview title of all-time posts and most views widget */ +"widget.allTimePostViews.previewTitle" = "All-Time Posts & Most Views"; + +/* Preview title of all-time views widget */ +"widget.allTimeViews.previewTitle" = "All-Time Views"; + +/* Preview title of all-time views and visitors widget */ +"widget.allTimeViewsVisitors.previewTitle" = "All-Time Views & Visitors"; + /* Title of best views ever label in all time widget */ "widget.alltime.bestviews.label" = "Best views ever"; +/* Title of the label which displays the number of the most daily views the site has ever had. Keep the translation as short as possible. */ +"widget.alltime.bestviewsshort.label" = "Most Views"; + +/* Title of the no data view in all time widget */ +"widget.alltime.nodata.view.title" = "Unable to load all time stats."; + +/* Title of the no site view in all time widget */ +"widget.alltime.nosite.view.title" = "Create or add a site to see all time stats."; + /* Title of posts label in all time widget */ "widget.alltime.posts.label" = "Posts"; @@ -10154,6 +11343,18 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of the unconfigured view in today widget */ "widget.jetpack.today.unconfigured.view.title" = "Log in to Jetpack to see today's stats."; +/* Title of the one-liner information consist of views field and all time date range in lock screen all time views widget */ +"widget.lockscreen.alltimeview.label" = "All-Time Views"; + +/* Title of the one-liner information consist of views field and today date range in lock screen today views widget */ +"widget.lockscreen.todayview.label" = "Views Today"; + +/* Title of the no data view in this week widget */ +"widget.thisweek.nodata.view.title" = "Unable to load this week's stats."; + +/* Title of the no site view in this week widget */ +"widget.thisweek.nosite.view.title" = "Create or add a site to see this week's stats."; + /* Description of all time widget in the preview */ "widget.thisweek.preview.description" = "Stay up to date with this week activity on your WordPress site."; @@ -10166,9 +11367,21 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of comments label in today widget */ "widget.today.comments.label" = "Comments"; +/* Title of the disabled view in today widget */ +"widget.today.disabled.view.title" = "Stats have moved to the Jetpack app. Switching is free and only takes a minute."; + /* Title of likes label in today widget */ "widget.today.likes.label" = "Likes"; +/* Fallback title of the no data view in the stats widget */ +"widget.today.nodata.view.fallbackTitle" = "Unable to load site stats."; + +/* Title of the no data view in today widget */ +"widget.today.nodata.view.title" = "Unable to load today's stats."; + +/* Title of the no site view in today widget */ +"widget.today.nosite.view.title" = "Create or add a site to see today's stats."; + /* Description of today widget in the preview */ "widget.today.preview.description" = "Stay up to date with today's activity on your WordPress site."; @@ -10187,6 +11400,15 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of visitors label in today widget */ "widget.today.visitors.label" = "Visitors"; +/* Preview title of today's likes and commnets widget */ +"widget.todayLikesComments.previewTitle" = "Today's Likes & Comments"; + +/* Preview title of today's views widget */ +"widget.todayViews.previewTitle" = "Today's Views"; + +/* Preview title of today's views and visitors widget */ +"widget.todayViewsVisitors.previewTitle" = "Today's Views & Visitors"; + /* Second part of delete screen title stating [the site] will be unavailable in the future. */ "will be unavailable in the future." = "will be unavailable in the future."; @@ -10211,6 +11433,30 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* This is a comma separated list of keywords used for spotlight indexing of the 'My Sites' tab. */ "wordpress, sites, site, blogs, blog" = "wordpress, sites, site, blogs, blog"; +/* Jetpack Plugin Modal on WordPress primary button title */ +"wordpress.jetpack.plugin.modal.primary.button.title" = "Switch to the Jetpack app"; + +/* Jetpack Plugin Modal on WordPress secondary button title */ +"wordpress.jetpack.plugin.modal.secondary.button.title" = "Continue without Jetpack"; + +/* Jetpack Plugin Modal (multiple plugins) on WordPress subtitle with formatted texts. %1$@ is for the site name. */ +"wordpress.jetpack.plugin.modal.subtitle.plural" = "%1$@ is using individual Jetpack plugins, which isn't supported by the WordPress App."; + +/* Jetpack Plugin Modal on WordPress (single plugin) subtitle with formatted texts. %1$@ is for the site name and %2$@ is for the specific plugin name. */ +"wordpress.jetpack.plugin.modal.subtitle.singular" = "%1$@ is using the %2$@ plugin, which isn't supported by the WordPress App."; + +/* Second paragraph of the Jetpack Plugin Modal on WordPress asking the user to switch to Jetpack. */ +"wordpress.jetpack.plugin.modal.subtitle.switch" = "Please switch to the Jetpack app where we'll guide you through connecting the full Jetpack plugin so that you can use all the apps features for this site."; + +/* Jetpack Plugin Modal title in WordPress */ +"wordpress.jetpack.plugin.modal.title" = "Sorry, this site isn't supported by the WordPress app"; + +/* Description of the jetpack migration success card, used in My site. */ +"wp.migration.successCard.description" = "Welcome to the Jetpack app. You can uninstall the WordPress app."; + +/* Title of a button that displays a blog post in a web view. */ +"wp.migration.successCard.learnMore" = "Learn more"; + /* Placeholder for site url, if the url is unknown.Presented when logging in with a site address that does not have a valid Jetpack installation.The error would read: to use this app for your site you'll need... */ "your site" = "your site"; diff --git a/WordPress/Resources/en-GB.lproj/Localizable.strings b/WordPress/Resources/en-GB.lproj/Localizable.strings index f306c5355a19..749d6cdd5e9b 100644 --- a/WordPress/Resources/en-GB.lproj/Localizable.strings +++ b/WordPress/Resources/en-GB.lproj/Localizable.strings @@ -1,4 +1,4 @@ -/* Translation-Revision-Date: 2023-06-04 17:31:04+0000 */ +/* Translation-Revision-Date: 2024-01-03 09:18:47+0000 */ /* Plural-Forms: nplurals=2; plural=n != 1; */ /* Generator: GlotPress/4.0.0-alpha.11 */ /* Language: en_GB */ @@ -152,9 +152,6 @@ /* Difference label for Period Overview stat, indicating change from previous period. Ex: +99.9K (5%) */ "%@%@ (%@%%)" = "%1$@%2$@ (%3$@%%)"; -/* Accessibility label for a post in the post list. The parameters are the title, and date respectively. For example, \"Let it Go, 1 hour ago.\" */ -"%@, %@." = "%1$@, %2$@."; - /* Accessibility value for a Stats' Posting Activity Month if the user selected a day with posts. The first parameter is day (e.g. November 2019). The second parameter is the number of posts. */ "%@. %d posts." = "%1$@. %2$d posts."; @@ -210,9 +207,6 @@ /* Age between dates over one year. */ "%d years" = "%d years"; -/* Max image size in pixels (e.g. 300x300px) */ -"%dx%dpx" = "%1$dx%2$dpx"; - /* One menu area available in the theme */ "%i menu area in this theme" = "%i menu area in this theme"; @@ -250,6 +244,9 @@ translators: Block name. %s: The localized block name */ /* translators: accessibility text for blocks with invalid content. %d: localized block title */ "%s block. This block has invalid content" = "%s block. This block has invalid content"; +/* translators: %s: name of the synced block */ +"%s detached" = "%s detached"; + /* translators: %s: embed block variant's label e.g: \"Twitter\". */ "%s embed block previews are coming soon" = "%s embed block previews are coming soon"; @@ -268,6 +265,9 @@ translators: Block name. %s: The localized block name */ /* translators: %s: social link name e.g: \"Instagram\". */ "%s social icon" = "%s social icon"; +/* translators: displayed right after the classic block is converted to blocks. %s: The localized classic block name */ +"'%s' block converted to blocks" = "'%s' block converted to blocks"; + /* translators: Missing block alert title. %s: The localized block name */ "'%s' is not fully-supported" = "'%s' is not fully supported"; @@ -418,6 +418,9 @@ translators: Block name. %s: The localized block name */ /* Label for selecting the Accelerated Mobile Pages (AMP) Blog Traffic Setting */ "Accelerated Mobile Pages (AMP)" = "Accelerated Mobile Pages (AMP)"; +/* No comment provided by engineer. */ +"Access this Paywall block on your web browser for advanced settings." = "Access this Paywall block on your web browser for advanced settings."; + /* Title for the account section in site settings screen */ "Account" = "Account"; @@ -472,13 +475,6 @@ translators: Block name. %s: The localized block name */ /* Label for the Activity Type filter button when there are more than 1 activity type selected */ "Activity Type (%1$d)" = "Activity Type (%1$d)"; -/* Accessibility label for add button to add items to the user's media library - Remove asset from media picker list */ -"Add" = "Add"; - -/* Action for Media Picker to indicate selection of media. The argument in the string represents the number of elements (as numeric digits) selected */ -"Add %@" = "Add %@"; - /* No comment provided by engineer. */ "Add Block After" = "Add Block After"; @@ -539,6 +535,9 @@ translators: Block name. %s: The localized block name */ /* Placeholder text. A call to action for the user to type any topic to which they would like to subscribe. */ "Add any topic" = "Add any topic"; +/* No comment provided by engineer. */ +"Add audio" = "Add audio"; + /* No comment provided by engineer. */ "Add blocks" = "Add blocks"; @@ -548,6 +547,9 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Add description" = "Add description"; +/* No comment provided by engineer. */ +"Add image" = "Add image"; + /* No comment provided by engineer. */ "Add image or video" = "Add image or video"; @@ -569,9 +571,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Add menu item to children" = "Add menu item to children"; -/* Accessibility hint for add button to add items to the user's media library */ -"Add new media" = "Add new media"; - /* Screen reader text for Menus button that adds a new menu to a site. */ "Add new menu" = "Add new menu"; @@ -609,6 +608,9 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Add title" = "Add title"; +/* No comment provided by engineer. */ +"Add video" = "Add video"; + /* User-facing string, presented to reflect that site assembly is underway. */ "Adding site features" = "Adding site features"; @@ -634,9 +636,6 @@ translators: Block name. %s: The localized block name */ /* Option to select the Airmail app when logging in with magic links */ "Airmail" = "Airmail"; -/* Description of albums in the photo libraries */ -"Albums" = "Albums"; - /* Image alignment option title. Title of the screen for choosing an image's alignment. */ "Alignment" = "Alignment"; @@ -650,6 +649,9 @@ translators: Block name. %s: The localized block name */ Title of the drafts filter. This filter shows a list of draft posts. */ "All" = "All"; +/* Information about redeeming domain credit on site dashboard. */ +"All WordPress.com annual plans include a custom domain name. Register your free domain now." = "All WordPress.com annual plans include a custom domain name. Register your free domain now."; + /* Footer of the free domain registration section for a paid plan. Information about redeeming domain credit on site dashboard. */ "All WordPress.com plans include a custom domain name. Register your free premium domain now." = "All WordPress.com plans include a custom domain name. Register your free premium domain now."; @@ -714,6 +716,15 @@ translators: Block name. %s: The localized block name */ Label for the alt for a media asset (image) */ "Alt Text" = "Alt Text"; +/* No comment provided by engineer. */ +"Alternatively, you can convert the content to blocks." = "Alternatively, you can convert the content to blocks."; + +/* No comment provided by engineer. */ +"Alternatively, you can detach and edit this block separately by tapping “Detach”." = "Alternatively, you can detach and edit this block separately by tapping “Detach”."; + +/* translators: Alternative option included in a warning related to having blocks deeply nested. */ +"Alternatively, you can flatten the content by ungrouping the block." = "Alternatively, you can flatten the content by ungrouping the block."; + /* Instruction text to explain to help users type their password instead of using magic link login option. */ "Alternatively, you may enter the password for this account." = "Alternatively, you may enter the password for this account."; @@ -861,15 +872,9 @@ translators: Block name. %s: The localized block name */ /* Message prompting the user to confirm that they want to disconnect Jetpack from the site. */ "Are you sure you want to disconnect Jetpack from the site?" = "Are you sure you want to disconnect Jetpack from the site?"; -/* Message prompting the user to confirm that they want to permanently delete a group of media items. */ -"Are you sure you want to permanently delete these items?" = "Are you sure you want to permanently delete these items?"; - /* Message prompting the user to confirm that they want to permanently delete a media item. Should match Calypso. */ "Are you sure you want to permanently delete this item?" = "Are you sure you want to permanently delete this item?"; -/* Message of the confirmation alert when deleting a page from the trash. */ -"Are you sure you want to permanently delete this page?" = "Are you sure you want to permanently delete this page?"; - /* Message of the confirmation alert when deleting a post from the trash. */ "Are you sure you want to permanently delete this post?" = "Are you sure you want to permanently delete this post?"; @@ -901,9 +906,6 @@ translators: Block name. %s: The localized block name */ /* Title of message shown when user taps submit for review. */ "Are you sure you want to submit for review?" = "Are you sure you want to submit for review?"; -/* Message of the trash page confirmation alert. */ -"Are you sure you want to trash this page?" = "Are you sure you want to bin this page?"; - /* Message of the trash confirmation alert. */ "Are you sure you want to trash this post?" = "Are you sure you want to bin this post?"; @@ -944,9 +946,6 @@ translators: Block name. %s: The localized block name */ /* translators: accessibility text. Empty Audio caption. */ "Audio caption. Empty" = "Audio caption. Empty"; -/* Accessibility label for audio items in the media collection view. The parameter is the creation date of the audio. */ -"Audio, %@" = "Audio, %@"; - /* No comment provided by engineer. */ "Authenticating" = "Authenticating"; @@ -1101,6 +1100,9 @@ translators: Block name. %s: The localized block name */ Discoverability title for block quote keyboard shortcut. */ "Block Quote" = "Block Quote"; +/* No comment provided by engineer. */ +"Block cannot be rendered because it is deeply nested. Tap here for more details." = "Block cannot be rendered because it is deeply nested. Tap here for more details."; + /* translators: displayed right after the block is copied. */ "Block copied" = "Block copied"; @@ -1153,6 +1155,9 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Blocks menu" = "Blocks menu"; +/* translators: Warning related to having blocks deeply nested. %d: The deepest nesting level. */ +"Blocks nested deeper than %d levels may not render properly in the mobile editor." = "Blocks nested deeper than %d levels may not render properly in the mobile editor."; + /* Title of a button that displays the WordPress.com blog */ "Blog" = "Blog"; @@ -1229,9 +1234,6 @@ translators: Block name. %s: The localized block name */ /* Label for the post author in the post detail. */ "By " = "By "; -/* Accessibility label for the post author in the post list. The parameter is the author name. For example, \"By Elsa.\" */ -"By %@." = "By %@."; - /* Legal disclaimer for logging in. The underscores _..._ denote underline. */ "By continuing, you agree to our _Terms of Service_." = "By continuing, you agree to our _Terms of Service_."; @@ -1251,8 +1253,7 @@ translators: Block name. %s: The localized block name */ /* Label for size of media while it's being calculated. */ "Calculating..." = "Calculating..."; -/* Accessibility label for taking an image or video with the camera on formatting toolbar. - Accessibility label for the camera tile in the collection view */ +/* Accessibility label for taking an image or video with the camera on formatting toolbar. */ "Camera" = "Camera"; /* Title of an alert informing the user the camera permission for the app is disabled and its needed to proceed */ @@ -1303,10 +1304,7 @@ translators: Block name. %s: The localized block name */ Cancels an Action Cancels an alert. Cancels the mark all as read action. - Dismiss the post action sheet Editing GIF alert cancel action button. - Label for a cancel button - Label for the auto-upload cancelation button in the post list. Tapping will prevent the app from auto-uploading the post. Menus cancel button for deleting a menu. Menus cancel button within text bar while editing items. Menus: Cancel button title for canceling an edited menu item. @@ -1323,10 +1321,6 @@ translators: Block name. %s: The localized block name */ Verb. Title for Jetpack Restore cancel button. */ "Cancel" = "Cancel"; -/* Label for the Post List option that cancels automatic uploading of a post. - Media Library option to cancel an in-progress or failed upload. */ -"Cancel Upload" = "Cancel Upload"; - /* Menus alert button title to cancel discarding changes and not createa a new menu. Menus alert button title to cancel discarding changes and not select a new menu Menus alert button title to cancel discarding changes and not select a new menu location */ @@ -1385,9 +1379,6 @@ translators: Block name. %s: The localized block name */ Main title */ "Change Password" = "Change Password"; -/* Title of button that takes user to the system Settings section for the app */ -"Change Settings" = "Change Settings"; - /* Change Username title. */ "Change Username" = "Change Username"; @@ -1499,6 +1490,9 @@ translators: Block name. %s: The localized block name */ /* Select domain name. Title */ "Choose a domain" = "Choose a domain"; +/* No comment provided by engineer. */ +"Choose a file" = "Choose a file"; + /* OK Button title shown in alert informing users about the Reader Save for Later feature. */ "Choose a new app icon" = "Choose a new app icon"; @@ -1524,9 +1518,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Choose file" = "Choose file"; -/* Menu option for selecting media from the device's photo library. */ -"Choose from My Device" = "Choose from My Device"; - /* Explanatory text for Homepage Settings homepage type selection. */ "Choose from a homepage that displays your latest posts (classic blog) or a fixed \/ static page." = "Choose from a homepage that displays your latest posts (classic blog) or a fixed\/static page."; @@ -1727,9 +1718,6 @@ translators: Block name. %s: The localized block name */ /* Community & Non-Profit site intent topic */ "Community & Non-Profit" = "Community & Non-Profit"; -/* Accessibility indication that the current Post List style is currently Compact. */ -"Compact" = "Compact"; - /* The action is completed */ "Completed" = "Completed"; @@ -1915,10 +1903,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Copied block" = "Copied block"; -/* Copy the post url and paste anywhere in phone - Label for page copy link. Tapping copy the url of page */ -"Copy Link" = "Copy Link"; - /* Copy link to oomment button title */ "Copy Link to Comment" = "Copy Link to Comment"; @@ -2027,9 +2011,6 @@ translators: Block name. %s: The localized block name */ /* Error title displayed when unable to close user account. */ "Couldn’t close account automatically" = "Couldn’t close account automatically"; -/* Message to show while media data source is finding the number of items available. */ -"Counting media items..." = "Counting media items..."; - /* Period Stats 'Countries' header */ "Countries" = "Countries"; @@ -2253,6 +2234,9 @@ translators: Block name. %s: The localized block name */ /* Only December needs to be translated */ "December 17, 2017" = "December 17, 2017"; +/* No comment provided by engineer. */ +"Deeply nested block" = "Deeply nested block"; + /* Description of the default paragraph formatting style in the editor. Placeholder text displayed in the share extension's summary view. It lets the user know the default category will be used on their post. */ "Default" = "Default"; @@ -2277,7 +2261,6 @@ translators: Block name. %s: The localized block name */ /* Delete Delete button title for the warning shown to the user when he refuses to re-login when the authToken is missing. Title for button that permanently deletes a media item (photo / video) - Title for button that permanently deletes one or more media items (photos / videos) Title of a delete button Title of the trash confirmation alert. */ "Delete" = "Delete"; @@ -2285,15 +2268,11 @@ translators: Block name. %s: The localized block name */ /* Menus confirmation button for deleting a menu. */ "Delete Menu" = "Delete Menu"; -/* Delete option in the confirmation alert when deleting a page from the trash. - Delete option in the confirmation alert when deleting a post from the trash. - Label for a button permanently deletes a page. - Label for the delete post option. Tapping permanently deletes a post. +/* Delete option in the confirmation alert when deleting a post from the trash. Title for button on the comment details page that deletes the comment when tapped. */ "Delete Permanently" = "Delete Permanently"; -/* Title of the confirmation alert when deleting a page from the trash. - Title of the confirmation alert when deleting a post from the trash. */ +/* Title of the confirmation alert when deleting a post from the trash. */ "Delete Permanently?" = "Delete Permanently?"; /* Button label for deleting the current site @@ -2412,7 +2391,6 @@ translators: Block name. %s: The localized block name */ /* Accessibility label for button to dismiss a bottom sheet Accessibility label for the transparent space above the login dialog which acts as a button to dismiss the dialog. Accessibility label for the transparent space above the signup dialog which acts as a button to dismiss the dialog. - Action to show on alert when view asset fails. Dismiss a view. Verb */ "Dismiss" = "Dismiss"; @@ -2430,12 +2408,6 @@ translators: Block name. %s: The localized block name */ User's Display Name */ "Display Name" = "Display Name"; -/* Accessibility label for other media items in the media collection view. The parameter is the creation date of the document. */ -"Document, %@" = "Document, %@"; - -/* Accessibility label for other media items in the media collection view. The parameter is the filename file. */ -"Document: %@" = "Document: %@"; - /* Message shown when all tours have been completed */ "Doesn't it feel good to cross things off a list?" = "Doesn't it feel good to cross things off a list?"; @@ -2596,8 +2568,7 @@ translators: Block name. %s: The localized block name */ /* Description of a Quick Start Tour */ "Draft and publish a post." = "Draft and publish a post."; -/* Title of the drafts filter. This filter shows a list of draft posts. - Title of the drafts header in search list. */ +/* Title of the drafts filter. This filter shows a list of draft posts. */ "Drafts" = "Drafts"; /* No comment provided by engineer. */ @@ -2609,10 +2580,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Drag to adjust focal point" = "Drag to adjust focal point"; -/* Label for page duplicate option. Tapping creates a copy of the page. - Label for post duplicate option. Tapping creates a copy of the post. */ -"Duplicate" = "Duplicate"; - /* No comment provided by engineer. */ "Duplicate block" = "Duplicate block"; @@ -2625,13 +2592,9 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Each block has its own settings. To find them, tap on a block. Its settings will appear on the toolbar at the bottom of the screen." = "Each block has its own settings. To find them, tap on a block. Its settings will appear on the toolbar at the bottom of the screen."; -/* Accessibility label for edit button to enable multi selection mode in the user's media library - Edit the post. - Editing GIF alert default action button. +/* Editing GIF alert default action button. Edits a Comment Edits the comment - Label for a button that opens the Edit Page view controller - Label for the edit post button. Tapping displays the editor. User action to edit media details. Verb, edit a comment */ "Edit" = "Edit"; @@ -2639,9 +2602,6 @@ translators: Block name. %s: The localized block name */ /* Title for the edit more button section */ "Edit \"More\" button" = "Edit \"More\" button"; -/* Button that displays the media editor to the user */ -"Edit %@" = "Edit %@"; - /* Blocklist Keyword Edition Title */ "Edit Blocklist Word" = "Edit Blocklist Word"; @@ -2689,6 +2649,12 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Edit video" = "Edit video"; +/* translators: %s: name of the host app (e.g. WordPress) */ +"Editing synced patterns is not yet supported on %s for Android" = "Editing synced patterns is not yet supported on %s for Android"; + +/* translators: %s: name of the host app (e.g. WordPress) */ +"Editing synced patterns is not yet supported on %s for iOS" = "Editing synced patterns is not yet supported on %s for iOS"; + /* Editing GIF alert message. */ "Editing this GIF will remove its animation." = "Editing this GIF will remove its animation."; @@ -2828,9 +2794,6 @@ translators: Block name. %s: The localized block name */ /* Secondary message shown when there are no domains that match the user entered text. */ "Enter different words above and we'll look for an address that matches it." = "Enter different words above and we'll look for an address that matches it."; -/* Accessibility hint for edit button to enable multi selection mode in the user's media library */ -"Enter edit mode to enable multi select to delete" = "Enter edit mode to enable multi select to delete"; - /* (placeholder) Help enter WordPress password Placeholder of a field to type a password to protect the post. */ "Enter password" = "Enter password"; @@ -2986,9 +2949,6 @@ translators: Block name. %s: The localized block name */ /* Short title telling the user they will receive a blogging reminder every day of the week. */ "Every day at %@" = "Every day at %@"; -/* Label for the post author filter. This filter shows posts for all users on the blog. */ -"Everyone" = "Everyone"; - /* Example story title description */ "Example story title" = "Example story title"; @@ -2998,9 +2958,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Excerpt length (words)" = "Excerpt length (words)"; -/* Accessibility label for a post's excerpt in the post list. The parameter is the post excerpt. For example, \"Excerpt. This is the first paragraph.\" */ -"Excerpt. %@." = "Excerpt. %@."; - /* Should be the same as the text displayed if the user clicks the (i) in Calypso. */ "Excerpts are optional hand-crafted summaries of your content." = "Excerpts are optional hand-crafted summaries of your content."; @@ -3010,8 +2967,7 @@ translators: Block name. %s: The localized block name */ /* Accessibility Label for the exit full screen button on the full screen comment reply mode */ "Exit Full Screen" = "Exit Full Screen"; -/* Accessibility indication that the current Post List style is currently Expanded. - Screen reader text to represent the expanded state of a UI control */ +/* Screen reader text to represent the expanded state of a UI control */ "Expanded" = "Expanded"; /* Accessibility hint */ @@ -3061,9 +3017,6 @@ translators: Block name. %s: The localized block name */ The action failed */ "Failed" = "Failed"; -/* Error title when picked media cannot be imported into stories. */ -"Failed Media Export" = "Failed Media Export"; - /* Message for mark all as read success notice */ "Failed marking Notifications as read" = "Failed marking Notifications as read"; @@ -3265,6 +3218,12 @@ translators: Block name. %s: The localized block name */ /* An example tag used in the login prologue screens. */ "Football" = "Football"; +/* translators: Recommendation included in a warning related to having blocks deeply nested. */ +"For this reason, we recommend editing the block using the web editor." = "For this reason, we recommend editing the block using the web editor."; + +/* translators: Recommendation included in a warning related to having blocks deeply nested. */ +"For this reason, we recommend editing the block using your web browser." = "For this reason, we recommend editing the block using your web browser."; + /* Register Domain - Domain contact information section header description */ "For your convenience, we have pre-filled your WordPress.com contact information. Please review to be sure it’s the correct information you want to use for this domain." = "For your convenience, we have pre-filled your WordPress.com contact information. Please review to be sure it’s the correct information you want to use for this domain."; @@ -3582,8 +3541,7 @@ translators: Block name. %s: The localized block name */ "Home" = "Home"; /* Title for setting which shows the current page assigned as a site's homepage - Title for the homepage section in site settings screen - Title of the Homepage Badge */ + Title for the homepage section in site settings screen */ "Homepage" = "Homepage"; /* Label for Homepage Settings site settings section @@ -3680,9 +3638,6 @@ translators: Block name. %s: The localized block name */ /* Hint for image title on image settings. */ "Image title" = "Image title"; -/* Accessibility label for image thumbnails in the media collection view. The parameter is the creation date of the image. */ -"Image, %@" = "Image, %@"; - /* Undated post time label */ "Immediately" = "Immediately"; @@ -3742,9 +3697,18 @@ translators: Block name. %s: The localized block name */ Button title used in media picker to insert media (photos / videos) into a post. Placeholder will be the number of items that will be inserted. */ "Insert %@" = "Insert %@"; +/* No comment provided by engineer. */ +"Insert Audio Block" = "Insert Audio Block"; + +/* No comment provided by engineer. */ +"Insert Gallery Block" = "Insert Gallery Block"; + /* Accessibility label for insert horizontal ruler button on formatting toolbar. */ "Insert Horizontal Ruler" = "Insert Horizontal Ruler"; +/* No comment provided by engineer. */ +"Insert Image Block" = "Insert Image Block"; + /* Accessibility label for insert link button on formatting toolbar. Discoverability title for insert link keyboard shortcut. Label action for inserting a link on the editor */ @@ -3753,6 +3717,9 @@ translators: Block name. %s: The localized block name */ /* Discoverability title for insert media keyboard shortcut. */ "Insert Media" = "Insert Media"; +/* No comment provided by engineer. */ +"Insert Video Block" = "Insert Video Block"; + /* No comment provided by engineer. */ "Insert crosspost" = "Insert crosspost"; @@ -4156,9 +4123,6 @@ translators: Block name. %s: The localized block name */ Settings: Comments Approval settings */ "Links in comments" = "Links in comments"; -/* The accessibility label for the list style button in the Post List. */ -"List style" = "List style"; - /* Title of the screen that load selected the revisions. */ "Load" = "Load"; @@ -4174,18 +4138,12 @@ translators: Block name. %s: The localized block name */ /* Text displayed while loading the activity feed for a site */ "Loading Backups..." = "Loading Backups..."; -/* Phrase to show when the user has searched for GIFs and they are being loaded. */ -"Loading GIFs..." = "Loading GIFs..."; - /* Menus label text displayed while menus are loading */ "Loading Menus..." = "Loading Menus..."; /* Text displayed while loading site People. */ "Loading People..." = "Loading People..."; -/* Phrase to show when the user has searched for images and they are being loaded. */ -"Loading Photos..." = "Loading Photos..."; - /* Text displayed while loading plans details */ "Loading Plan..." = "Loading Plan..."; @@ -4246,8 +4204,7 @@ translators: Block name. %s: The localized block name */ /* Local Services site intent topic */ "Local Services" = "Local Services"; -/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. - Title of the Local Changes Badge */ +/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. */ "Local changes" = "Local changes"; /* Title for alert when a generic error happened when trying to find the location of the device */ @@ -4411,7 +4368,6 @@ translators: Block name. %s: The localized block name */ "Max Video Upload Size" = "Max Video Upload Size"; /* Accessibility label for the Me button in My Site. - Label for the post author filter. This filter shows posts only authored by the current user. Me page title Noun. Title. Links to the Me screen. The accessibility value of the me tab. @@ -4419,9 +4375,7 @@ translators: Block name. %s: The localized block name */ "Me" = "Me"; /* Noun. Title. Links to the blog's Media library. - Tab bar title for the Media tab in Media Picker The menu item to select during a guided tour. - Title for Media Library section of the app. Title for the media section in site settings screen Title label for the media settings section in the app settings */ "Media" = "Media"; @@ -4433,13 +4387,6 @@ translators: Block name. %s: The localized block name */ /* Label for size of media cache in the app. */ "Media Cache Size" = "Media Cache Size"; -/* Title for alert when access to media capture is not granted */ -"Media Capture" = "Media Capture"; - -/* Title for alert when a generic error happened when loading media - Title for alert when access to the media library is not granted by the user */ -"Media Library" = "Media Library"; - /* Title for action sheet with media options. */ "Media Options" = "Media Options"; @@ -4462,9 +4409,6 @@ translators: Block name. %s: The localized block name */ /* translators: %s: block title e.g: \"Paragraph\". */ "Media options" = "Media options"; -/* Alert title when there is issues loading an asset to preview. */ -"Media preview failed." = "Media preview failed."; - /* Alert displayed to the user when multiple media items have uploaded successfully. */ "Media uploaded (%ld files)" = "Media uploaded (%ld files)"; @@ -4502,9 +4446,6 @@ translators: Block name. %s: The localized block name */ Label for the share message field on the post settings. */ "Message" = "Message"; -/* Title of section containing image / video metadata such as size and file type */ -"Metadata" = "Metadata"; - /* Option to select the Microsft Outlook app when logging in with magic links */ "Microsoft Outlook" = "Microsoft Outlook"; @@ -4524,13 +4465,11 @@ translators: Block name. %s: The localized block name */ "Months and Years" = "Months and Years"; /* Accessibility label for more button in dashboard quick start card. - Accessibility label for the More button in Page List. Accessibility label for the More button in Post List (compact view). Accessibility label for the More button on formatting toolbar. Accessibility label for the More button on Reader Cell Accessibility label for the More button on Reader's post details - Action button to display more available options - Label for the more post button. Tapping displays an action sheet with post options. */ + Action button to display more available options */ "More" = "More"; /* Action button to display more available options @@ -4588,15 +4527,8 @@ translators: Block name. %s: The localized block name */ /* Screen reader text for button that will move the menu item */ "Move menu item" = "Move menu item"; -/* Label for a button that moves a page to the draft folder - Label for an option that moves a post to the draft folder */ -"Move to Draft" = "Move to Draft"; - -/* Label for a button that moves a page to the trash folder - Label for a option that moves a post to the trash folder - Title for button on the comment details page that moves the comment to trash when tapped. +/* Title for button on the comment details page that moves the comment to trash when tapped. Trash option in the trash confirmation alert. - Trash option in the trash page confirmation alert. Trashes the comment */ "Move to Trash" = "Move to Bin"; @@ -4628,7 +4560,8 @@ translators: Block name. %s: The localized block name */ Title of My Site tab */ "My Site" = "My Site"; -/* Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ +/* Title for site picker screen. + Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ "My Sites" = "My Sites"; /* Siri Suggestion to open My Sites */ @@ -4834,6 +4767,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Accessibility label for no currently selected range. */ "No date range selected" = "No date range selected"; +/* No comment provided by engineer. */ +"No description" = "No description"; + /* Title for the view when there aren't any fixed threats to display */ "No fixed threats" = "No fixed threats"; @@ -4875,9 +4811,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title for the view when there aren't any Activities to display in the Activity Log for a given filter. */ "No matching events found." = "No matching events found."; -/* Message displayed when no results are returned from a media library search. Should match Calypso. - Phrase to show when the user search for images but there are no result to show. - Phrase to show when the user searches for GIFs but there are no result to show. */ +/* Message displayed when no results are returned from a media library search. Should match Calypso. */ "No media matching your search" = "No media matching your search"; /* Menus text shown when no menus were available for loading the Menus editor. */ @@ -4895,8 +4829,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Displayed in the Notifications Tab as a title, when there are no notifications */ "No notifications yet" = "No notifications yet"; -/* Displayed when the user is searching the pages list and there are no matching pages - Text displayed when there's no matching with the text search */ +/* Text displayed when there's no matching with the text search */ "No pages matching your search" = "No pages matching your search"; /* Text displayed when search for plugins returns no results */ @@ -4917,9 +4850,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Message shown whent the reader finds no posts for the chosen tag */ "No posts have been made recently with this tag." = "No posts have been made recently with this tag."; -/* Displayed when the user is searching the posts list and there are no matching posts */ -"No posts matching your search" = "No posts matching your search"; - /* Accessibility value for a Stats' Posting Activity Month if there are no posts. */ "No posts." = "No posts."; @@ -4960,6 +4890,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Message for a notice informing the user their scan completed and no threats were found */ "No threats found" = "No threats found"; +/* No comment provided by engineer. */ +"No title" = "No title"; + /* Disabled No alignment for an image (default). Should be the same as in core WP. No comment will be autoapproved @@ -5017,9 +4950,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* A message title */ "Nothing liked yet" = "Nothing liked yet"; -/* Default message for empty media picker */ -"Nothing to show" = "Nothing to show"; - /* Notifications Details Accessibility Identifier */ "Notification Details Table" = "Notification Details Table"; @@ -5079,7 +5009,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Alert dismissal title Button title. Acknowledges a prompt. Button title. An acknowledgement of the message displayed in a prompt. - Confirmation of action Default action Dismisses the alert Marks all notifications as read. @@ -5141,9 +5070,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* No comment provided by engineer. */ "Only show excerpt" = "Only show excerpt"; -/* Message telling the user that they've only enabled limited photo library permissions for the app. */ -"Only the selected photos you've given access to are available." = "Only the selected photos to which you've given access are available."; - /* An informal exclaimation that means `something went wrong`. Title for the view when there's an error loading a comment. Title for the view when there's an error loading Activity Log @@ -5178,9 +5104,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title of a button that opens the apps settings in the system Settings.app */ "Open Settings" = "Open Settings"; -/* Editor button to swich the media picker from quick mode to full picker */ -"Open full media picker" = "Open full media picker"; - /* No comment provided by engineer. */ "Open in Safari" = "Open in Safari"; @@ -5220,6 +5143,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Divider on initial auth view separating auth options. */ "Or" = "Or"; +/* Instruction text for other forms of two-factor auth methods. */ +"Or choose another form of authentication." = "Or choose another form of authentication."; + /* Label for button to log in using site address. Underscores _..._ denote underline. */ "Or log in by _entering your site address_." = "Or log in by _entering your site address_."; @@ -5278,15 +5204,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title shown when selecting a post type of Page from the Share Extension. */ "Page" = "Page"; -/* Prompts the user that a restored page was moved to the drafts list. */ -"Page Restored to Drafts" = "Page Restored to Drafts"; - -/* Prompts the user that a restored page was moved to the published list. */ -"Page Restored to Published" = "Page Restored to Published"; - -/* Prompts the user that a restored page was moved to the scheduled list. */ -"Page Restored to Scheduled" = "Page Restored to Scheduled"; - /* Name of the button to open the page settings The title of the Page Settings screen. */ "Page Settings" = "Page Settings"; @@ -5303,9 +5220,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of notification displayed when a page has failed to upload. */ "Page failed to upload" = "Page failed to upload"; -/* A short message explaining that a page was moved to the trash bin. */ -"Page moved to trash." = "Page moved to bin."; - /* Title of notification displayed when a page has been successfully saved as a draft. */ "Page pending review" = "Page pending review"; @@ -5377,8 +5291,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title of pending Comments filter. */ "Pending" = "Pending"; -/* Name for the status of a post pending review. - Title of the Pending Review Badge */ +/* Name for the status of a post pending review. */ "Pending review" = "Pending review"; /* Noun. Title of the people management feature. @@ -5407,12 +5320,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Photography site intent topic */ "Photography" = "Photography"; -/* Tab bar title for the Photos tab in Media Picker */ -"Photos" = "Photos"; - -/* Subtitle for placeholder in Free Photos. The company name 'Pexels' should always be written as it is. */ -"Photos provided by Pexels" = "Photos provided by Pexels"; - /* Title for selecting a new username in the site creation flow. */ "Pick username" = "Pick username"; @@ -5505,7 +5412,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Please enter the password for your WordPress.com account to log in with your Apple ID." = "Please enter the password for your WordPress.com account to log in with your Apple ID."; /* Instruction text on the two-factor screen. */ -"Please enter the verification code from your authenticator app, or tap the link below to receive a code via SMS." = "Please enter the verification code from your authenticator app, or tap the link below to receive a code via SMS."; +"Please enter the verification code from your authenticator app." = "Please enter the verification code from your authenticator app."; /* Popup message to ask for user credentials (fields shown below). */ "Please enter your credentials" = "Please enter your credentials"; @@ -5600,15 +5507,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ The post formats available for the post. Should be the same as in core WP. */ "Post Format" = "Post Format"; -/* Prompts the user that a restored post was moved to the drafts list. */ -"Post Restored to Drafts" = "Post Restored to Drafts"; - -/* Prompts the user that a restored post was moved to the published list. */ -"Post Restored to Published" = "Post Restored to Published"; - -/* Prompts the user that a restored post was moved to the scheduled list. */ -"Post Restored to Scheduled" = "Post Restored to Scheduled"; - /* Name of the button to open the post settings The title of the Post Settings screen. */ "Post Settings" = "Post Settings"; @@ -5628,9 +5526,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of notification displayed when a post has failed to upload. */ "Post failed to upload" = "Post failed to upload"; -/* A short message explaining that a post was moved to the trash bin. */ -"Post moved to trash." = "Post moved to bin."; - /* Title of notification displayed when a post has been successfully saved as a draft. */ "Post pending review" = "Post pending review"; @@ -5689,9 +5584,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Period Stats 'Posts and Pages' header */ "Posts and Pages" = "Posts and Pages"; -/* Title of the Posts Page Badge */ -"Posts page" = "Posts page"; - /* Message informing the user that their static homepage for posts was set successfully */ "Posts page successfully updated" = "Posts page successfully updated"; @@ -5704,9 +5596,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* A message explaining the Posts I Like feature in the reader */ "Posts that you like will appear here." = "Posts that you like will appear here."; -/* Subtitle for placeholder in Tenor picker. `The company name 'Tenor' should always be written as it is. */ -"Powered by Tenor" = "Powered by Tenor"; - /* Browse premium themes selection title */ "Premium" = "Premium"; @@ -5725,18 +5614,12 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title for screen to preview a static content. */ "Preview" = "Preview"; -/* Action for Media Picker to preview the selected media items. The argument in the string represents the number of elements (as numeric digits) selected */ -"Preview %@" = "Preview %@"; - /* Title for web preview device switching button */ "Preview Device" = "Preview Device"; /* Title on display preview error */ "Preview Unavailable" = "Preview Unavailable"; -/* Accessibility label for media item preview for user's viewing an item in their media library */ -"Preview media" = "Preview media"; - /* No comment provided by engineer. */ "Preview page" = "Preview page"; @@ -5783,8 +5666,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Privacy notice for California users" = "Privacy notice for California users"; /* Name for the status of a post that is marked private. - Privacy setting for posts set to 'Private'. Should be the same as in core WP. - Title of the Private Badge */ + Privacy setting for posts set to 'Private'. Should be the same as in core WP. */ "Private" = "Private"; /* No comment provided by engineer. */ @@ -5834,12 +5716,10 @@ translators: %s: Select control button label e.g. \"Button width\" */ Label for the publish date button. */ "Publish Date" = "Publish Date"; -/* A short phrase indicating a post is due to be immedately published. - Label for a button that moves a page to the published folder, publishing with the current date/time. */ +/* A short phrase indicating a post is due to be immedately published. */ "Publish Immediately" = "Publish Immediately"; /* Label for a button that publishes the post - Label for an option that moves a publishes a post immediately Title of button allowing the user to immediately publish the post they are editing. */ "Publish Now" = "Publish Now"; @@ -5857,8 +5737,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Name for the status of a published post. Period Stats 'Published' header - Title of the published filter. This filter shows a list of posts that the user has published. - Title of the published header in search list. */ + Title of the published filter. This filter shows a list of posts that the user has published. */ "Published" = "Published"; /* Precedes the name of the blog just posted on */ @@ -5958,6 +5837,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Message shwon to confirm a publicize connection has been successfully reconnected. */ "Reconnected" = "Reconnected"; +/* Action button to redo last change */ +"Redo" = "Redo"; + /* Label for link title in Referrers stat. */ "Referrer" = "Referrer"; @@ -5997,8 +5879,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of the completion screen of the Blogging Reminders Settings screen when the reminders are removed. */ "Reminders removed" = "Reminders removed"; -/* Add asset to media picker list - Alert button to confirm a plugin to be removed +/* Alert button to confirm a plugin to be removed Button label when removing a blog Label for the Remove Feature Image icon. Tapping will show a confirmation screen for removing the feature image from the post. Remove Action @@ -6151,9 +6032,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of secondary button on alert prompting verify their accounts while attempting to publish */ "Resend" = "Resend"; -/* Title of the reset button */ -"Reset" = "Reset"; - /* Accessibility label for the reset activity type button */ "Reset Activity Type filter" = "Reset Activity Type filter"; @@ -6208,12 +6086,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ Button title, displayed when media has failed to upload. Allows the user to try the upload again. Button title. Retries uploading a post. If a user taps the button with this label, the action that evinced this error view will be retried. - Label for a button that attempts to re-upload a page that previously failed to upload. - Label for the retry post upload button. Tapping attempts to upload the post again. Opens the media library . Retries the upload of a user's post. Retry updating User's Role - Retry uploading the post. Retry. Action Retry. Verb – retry a failed media upload. The Jetpack view button title used when an error occurred @@ -6225,9 +6100,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Button title that triggers a scan */ "Retry Scan" = "Retry Scan"; -/* User action to retry media upload. */ -"Retry Upload" = "Retry Upload"; - /* User action to retry all failed media uploads. */ "Retry all" = "Retry all"; @@ -6325,9 +6197,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Accessibility label for the 'Save Post' button when a post has been saved. */ "Saved Post" = "Saved Post"; -/* Text displayed in HUD when a media item's metadata (title, etc) is saved successfully. */ -"Saved!" = "Saved!"; - /* Accessibility hint for the 'Save Post' button. */ "Saves this post for later." = "Saves this post for later."; @@ -6338,7 +6207,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Saving post…" = "Saving post…"; /* Menus save button title while it is saving a Menu. - Text displayed in HUD while a media item's metadata (title, etc) is being saved. Text displayed in HUD while a post is being saved as a draft. */ "Saving..." = "Saving..."; @@ -6429,21 +6297,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* No comment provided by engineer. */ "Search or type URL" = "Search or type URL"; -/* Text displayed when the search controller will be presented */ -"Search pages" = "Search pages"; - -/* Text displayed when the search controller will be presented */ -"Search posts" = "Search posts"; - /* No comment provided by engineer. */ "Search settings" = "Search settings"; -/* Title for placeholder in Tenor picker */ -"Search to find GIFs to add to your Media Library!" = "Search to find GIFs to add to your Media Library!"; - -/* Title for placeholder in Free Photos */ -"Search to find free photos to add to your Media Library!" = "Search to find free photos to add to your Media Library!"; - /* Menus search bar placeholder text. */ "Search..." = "Search..."; @@ -6514,9 +6370,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Register Domain - Domain contact information field placeholder for Country */ "Select Country" = "Select Country"; -/* Title of button that allows the user to select more photos to access within the app */ -"Select More" = "Select More"; - /* Blog Picker's Title */ "Select Site" = "Select Site"; @@ -6538,9 +6391,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Register domain - Title for the Choose domain button of Suggested domains screen */ "Select domain" = "Select domain"; -/* Accessibility hint for actions when displaying media items. */ -"Select media." = "Select media."; - /* Accessibility label for selecting paragraph style button on formatting toolbar. */ "Select paragraph style" = "Select paragraph style"; @@ -6644,19 +6494,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label for connected service in Publicize stat. */ "Service" = "Service"; -/* Label for a button that opens the Set Parent options view controller - Navigation title displayed on the navigation bar */ +/* Navigation title displayed on the navigation bar */ "Set Parent" = "Set Parent"; /* No comment provided by engineer. */ "Set as Featured Image" = "Set as Featured Image"; -/* Label for a button that sets the selected page as the site's Homepage */ -"Set as Homepage" = "Set as Homepage"; - -/* Label for a button that sets the selected page as the site's Posts page */ -"Set as Posts Page" = "Set as Posts Page"; - /* Notice confirming that an image has been set as the post's featured image. */ "Set as featured image" = "Set as featured image"; @@ -6700,7 +6543,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Accessibility label for share buttons in nav bars Button label to share a post Button label to share a web page - Share the post. Shares the comment URL Spoken accessibility label Title for a button that allows the user to share their answer to the prompt. @@ -7086,8 +6928,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Name of setting configured when a site uses a static page as its homepage */ "Static Homepage" = "Static Homepage"; -/* Label for post stats option. Tapping displays statistics for a post. - Noun. Abbreviation of Statistics. Name of the Stats feature +/* Noun. Abbreviation of Statistics. Name of the Stats feature Noun. Abbv. of Statistics. Links to a blog's Stats screen. Stats 3D Touch Shortcut Stats window title @@ -7118,9 +6959,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label text that defines a post marked as sticky */ "Sticky" = "Sticky"; -/* Accessibility label for a sticky post in the post list. */ -"Sticky." = "Sticky."; - /* User action to stop upload. */ "Stop upload" = "Stop upload"; @@ -7177,7 +7015,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ Theme Support action title */ "Support" = "Support"; -/* Button used to switch site */ +/* Title for back button that leads to the site picker screen. */ "Switch Site" = "Switch Site"; /* Switches the Editor to HTML Mode */ @@ -7222,6 +7060,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Accessibility Identifier for the Default Font Aztec Style. */ "Switches to the default Font Size" = "Switches to the default Font Size"; +/* No comment provided by engineer. */ +"Synced patterns" = "Synced patterns"; + /* Title for the app appearance setting (light / dark mode) that uses the system default value */ "System default" = "System default"; @@ -7262,9 +7103,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label explaining why users might want to add tags. */ "Tags help tell readers what a post is about. Separate different tags with commas." = "Tags help tell readers what a post is about. Separate different tags with commas."; -/* Menu option for taking an image or video with the device's camera. */ -"Take Photo or Video" = "Take Photo or Video"; - /* No comment provided by engineer. */ "Take a Photo" = "Take A Photo"; @@ -7283,6 +7121,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* No comment provided by engineer. */ "Tap here to show help" = "Tap here to show help"; +/* No comment provided by engineer. */ +"Tap here to show more details." = "Tap here to show more details."; + /* Accessibility hint for a button that opens a view that allows to add new stats cards. */ "Tap to add new stats cards." = "Tap to add new stats cards."; @@ -7332,12 +7173,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Accessibility hint */ "Tap to select the previous period" = "Tap to select the previous period"; -/* Accessibility hint for button used to switch site */ -"Tap to switch to another site, or add a new site" = "Tap to switch to another site, or add a new site"; - -/* Accessibility hint for media item preview for user's viewing an item in their media library */ -"Tap to view media in full screen" = "Tap to view media in full screen"; - /* Accessibility hint for a button that opens a new view with more details. */ "Tap to view more details." = "Tap to view more details."; @@ -7383,10 +7218,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* No comment provided by engineer. */ "Text formatting controls are located within the toolbar positioned above the keyboard while editing a text block" = "Text formatting controls are located within the toolbar positioned above the keyboard while editing a text block"; -/* Button title - The button's title text to send a 2FA code via SMS text message. */ +/* Button title */ "Text me a code instead" = "Text me a code instead"; +/* The button's title text to send a 2FA code via SMS text message. */ +"Text me a code via SMS" = "Text me a code via SMS"; + /* Message of alert when theme activation succeeds */ "Thanks for choosing %@ by %@" = "Thanks for choosing %1$@ by %2$@"; @@ -7414,9 +7251,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Error message shown to a user who is trying to share to Facebook but does not have any available Facebook Pages. */ "The Facebook connection cannot find any Pages. Publicize cannot connect to Facebook Profiles, only published Pages." = "The Facebook connection cannot find any Pages. Publicise cannot connect to Facebook Profiles, only published Pages."; -/* Message shown when a GIF failed to load while trying to add it to the Media library. */ -"The GIF could not be added to the Media Library." = "The GIF could not be added to the Media Library."; - /* Description shown when a user logs in with Google but no matching WordPress.com account is found */ "The Google account \"%@\" doesn't match any account on WordPress.com" = "The Google account \"%@\" doesn't match any account on WordPress.com"; @@ -7544,7 +7378,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Error message shown when user attempts to remove the site owner. */ "The user you are trying to remove is the owner of this site. Please contact support for assistance." = "The user you are trying to remove is the owner of this site. Please contact support for assistance."; -/* Error message informing a user about an invalid password. */ +/* No comment provided by engineer. */ "The username or password stored in the app may be out of date. Please re-enter your password in the settings and try again." = "The username or password stored in the app may be out of date. Please re-enter your password in the settings and try again."; /* Message shown when a video failed to load while trying to add it to the Media library. */ @@ -7612,9 +7446,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* A short error message letting the user know about a problem displaying a post. */ "There was a problem displaying this post." = "There was a problem displaying this post."; -/* Error message displayed when the Media Library is unable to load a full sized preview of an item. */ -"There was a problem loading the media item." = "There was a problem loading the media item."; - /* The loading view subtitle displayed when an error occurred */ "There was a problem loading your data, refresh your page to try again." = "There was a problem loading your data, refresh your page to try again."; @@ -7627,9 +7458,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Explaining to the user there was an error trying to obtain the current location of the user. */ "There was a problem when trying to access your location. Please try again later." = "There was a problem when trying to access your location. Please try again later."; -/* Explaining to the user there was an generic error accesing media. */ -"There was a problem when trying to access your media. Please try again later." = "There was a problem when trying to access your media. Please try again later."; - /* Message for stories unknown error. */ "There was a problem with the Stories editor. If the problem persists you can contact us via the Me > Help & Support screen." = "There was a problem with the Stories editor. If the problem persists, you can contact us via the Me > Help and Support screen."; @@ -7700,9 +7528,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* A description informing the user in order to proceed with this feature we will need camera permissions, and how to enable it. */ "This app needs permission to access the Camera to scan login codes, tap on the Open Settings button to enable it." = "This app needs permission to access the Camera to scan log-in codes, tap on the Open Settings button to enable it."; -/* Explaining to the user why the app needs access to the device media library. */ -"This app needs permission to access your device media library in order to add photos and\/or video to your posts. Please change the privacy settings if you wish to allow this." = "This app needs permission to access your device media library in order to add photos and\/or video to your posts. Please change the privacy settings if you wish to allow this."; - /* No comment provided by engineer. */ "This color combination may be hard for people to read. Try using a brighter background color and\/or a darker text color." = "This colour combination may be hard for people to read. Try using a brighter background colour and\/or a darker text colour."; @@ -7812,6 +7637,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Description of a Quick Start Tour */ "Time to finish setting up your site! Our checklist walks you through the next steps." = "Time to finish setting up your site! Our checklist walks you through the next steps."; +/* Error when the uses takes more than 1 minute to submit a security key. */ +"Time's up, but don't worry, your security is our priority. Please try again!" = "Time's up, but don't worry, your security is our priority. Please try again!"; + /* WordPress.com Marketing Footer Text */ "Tips for getting the most out of WordPress.com." = "Tips for getting the most out of WordPress.com."; @@ -7935,24 +7763,20 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title for the traffic section in site settings screen */ "Traffic" = "Traffic"; +/* Describes a domain that was transferred from elsewhere to wordpress.com */ +"Transferred Domain" = "Transferred Domain"; + /* translators: %s: block title e.g: \"Paragraph\". */ "Transform %s to" = "Transform %s to"; /* No comment provided by engineer. */ "Transform block…" = "Transform block…"; -/* Accessibility label for trash button to delete items from the user's media library - Accessibility label for trash buttons in nav bars +/* Accessibility label for trash buttons in nav bars Trashes a comment Trashes the comment */ "Trash" = "Bin"; -/* Accessibility hint for trash button to delete items from the user's media library */ -"Trash selected media" = "Bin selected media"; - -/* Title of the trash page confirmation alert. */ -"Trash this page?" = "Bin this page?"; - /* Title of the trash confirmation alert. */ "Trash this post?" = "Bin this post?"; @@ -8070,9 +7894,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Shown when a user logs in with Google but it subsequently fails to work as login to WordPress.com */ "Unable To Connect" = "Unable To Connect"; -/* An error message. */ -"Unable to Connect" = "Unable to Connect"; - /* Title for stories unknown error. */ "Unable to Create Stories Editor" = "Unable to Create Stories Editor"; @@ -8088,9 +7909,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* An error message shown when there is an issue creating new invite links. */ "Unable to create new invite links." = "Unable to create new invite links."; -/* Text displayed in HUD if there was an error attempting to delete a group of media items. */ -"Unable to delete all media items." = "Unable to delete all media items."; - /* Text displayed in HUD if there was an error attempting to delete a media item. */ "Unable to delete media item." = "Unable to delete media item."; @@ -8154,12 +7972,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Message displayed when sharing a link to the downloadable backup fails. */ "Unable to share link" = "Unable to share link"; -/* Message that appears when a user tries to trash a page while their device is offline. */ -"Unable to trash pages while offline. Please try again later." = "Unable to bin pages while offline. Please try again later."; - -/* Message that appears when a user tries to trash a post while their device is offline. */ -"Unable to trash posts while offline. Please try again later." = "Unable to bin posts while offline. Please try again later."; - /* Notice title when turning site notifications off fails. */ "Unable to turn off site notifications" = "Unable to turn off site notifications"; @@ -8232,8 +8044,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ Button title. Reverts the previous notification operation Revert an operation Revert enabling notification after successfully subcribing to the comments for the post. - The title of an 'undo' button. Tapping the button moves a trashed page out of the trash folder. - The title of an 'undo' button. Tapping the button moves a trashed post out of the trash folder. Undo action */ "Undo" = "Undo"; @@ -8263,6 +8073,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* VoiceOver accessibility hint, informing the user the button can be used to unfollow a blog. */ "Unfollows the blog." = "Unfollows the blog."; +/* No comment provided by engineer. */ +"Ungroup block" = "Ungroup block"; + /* Unhides a site from the site picker list */ "Unhide" = "Unhide"; @@ -8273,9 +8086,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title for Unknown HTML Editor */ "Unknown HTML" = "Unknown HTML"; -/* Label to use when creation date from media asset is not know. */ -"Unknown creation date" = "Unknown creation date"; - /* No comment provided by engineer. */ "Unknown error" = "Unknown error"; @@ -8441,6 +8251,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of a row displayed on the debug screen used to configure the sandbox store use in the App. */ "Use Sandbox Store" = "Use Sandbox Store"; +/* The button's title text to use a security key. */ +"Use a security key" = "Use a security key"; + /* Option to enable the block editor for new posts */ "Use block editor" = "Use block editor"; @@ -8516,15 +8329,10 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of an alert informing users that the video they are trying to select is not allowed. */ "Video not uploaded" = "Video not uploaded"; -/* Accessibility label for video thumbnails in the media collection view. The parameter is the creation date of the video. */ -"Video, %@" = "Video, %@"; - /* Period Stats 'Videos' header */ "Videos" = "Videos"; /* Button title. Displays a summary / sharing screen for a specific post. - Label for a button that opens the page when tapped. - Label for the view post button. Tapping displays the post as it appears on the web. Opens the post epilogue screen to allow sharing / viewing of a post. Theme View action title Verb. The screen title shown when viewing a post inside the app. */ @@ -8639,6 +8447,10 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Message shown on screen while waiting for Google to finish its signup process. */ "Waiting for Google to complete…" = "Waiting for Google to complete…"; +/* Text while the webauthn signature is being verified + Text while waiting for a security key challenge */ +"Waiting for security key" = "Waiting for security key"; + /* View title during the Google auth process. */ "Waiting..." = "Waiting..."; @@ -8650,6 +8462,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ Title for Jetpack Restore Warning screen */ "Warning" = "Warning"; +/* No comment provided by engineer. */ +"Warning message" = "Warning message"; + /* Caption displayed in promotional screens shown during the login flow. */ "Watch your audience grow with in-depth analytics." = "Watch your audience grow with in-depth analytics."; @@ -9010,6 +8825,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* An error message shown when a wpcom user provides the wrong password. */ "Whoops, something went wrong and we couldn't log you in. Please try again!" = "Whoops, something went wrong and we couldn't log you in. Please try again!"; +/* Generic error on the 2FA screen */ +"Whoops, something went wrong. Please try again!" = "Whoops, something went wrong. Please try again!"; + +/* Error when the uses chooses an invalid security key on the 2FA screen. */ +"Whoops, that security key does not seem valid. Please try again with another one" = "Whoops, that security key does not seem valid. Please try again with another one"; + /* Error message shown when an incorrect two factor code is provided. */ "Whoops, that's not a valid two-factor verification code. Double-check your code and try again!" = "Whoops, that's not a valid two-factor verification code. Double-check your code and try again!"; @@ -9037,9 +8858,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Siri Suggestion to open Support */ "WordPress Help" = "WordPress Help"; -/* Name for the WordPress Media Library */ -"WordPress Media" = "WordPress Media"; - /* Accessibility label for selecting an image or video from the user's WordPress media library on formatting toolbar. */ "WordPress Media Library" = "WordPress Media Library"; @@ -9354,9 +9172,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Share extension error dialog text. */ "Your account does not have permission to upload media to this site. The Site Administrator can change these permissions." = "Your account does not have permission to upload media to this site. The Site Administrator can change these permissions."; -/* Explaining to the user why the app needs access to the device media library. */ -"Your app is not authorized to access media library due to active restrictions such as parental controls. Please check your parental control settings in this device." = "Your app is not authorised to access media library due to active restrictions such as parental controls. Please check your parental control settings in this device."; - /* Title for the Jetpack Backup Complete message. */ "Your backup is now available for download" = "Your backup is now available for download"; @@ -9375,9 +9190,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of the site address section in the Domains Dashboard. */ "Your free WordPress.com address is" = "Your free WordPress.com address is"; -/* Error message when picked media cannot be imported into stories. */ -"Your media could not be exported. If the problem persists you can contact us via the Me > Help & Support screen." = "Your media could not be exported. If the problem persists, you can contact us via the Me > Help and Support screen."; - /* Details about recently acquired domain on domain credit redemption success screen */ "Your new domain %@ is being set up. It may take up to 30 minutes for your domain to start working." = "Your new domain %@ is being set up. It may take up to 30 minutes for your domain to start working."; @@ -9501,24 +9313,84 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* This is the string we display when prompting the user to review the WordPress app */ "appRatings.wordpress.prompt" = "What do you think about WordPress?"; -/* Label displayed on audio media items. */ -"audio" = "audio"; +/* Option to enable the optimization of images when uploading. */ +"appSettings.media.imageOptimizationRow" = "Optimise Images"; + +/* Indicates an image will use high quality when uploaded. */ +"appSettings.media.imageQuality.high" = "High"; + +/* Indicates an image will use low quality when uploaded. */ +"appSettings.media.imageQuality.low" = "Low"; + +/* Indicates an image will use maximum quality when uploaded. */ +"appSettings.media.imageQuality.maximum" = "Maximum"; + +/* Indicates an image will use medium quality when uploaded. */ +"appSettings.media.imageQuality.medium" = "Medium"; + +/* The quality of image used when uploading */ +"appSettings.media.imageQuality.title" = "Quality"; + +/* Title for the image quality settings option. */ +"appSettings.media.imageQualityRow" = "Image Quality"; + +/* Message of an alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.message" = "Image optimisation shrinks images for faster uploading.\n\nThis option is enabled by default, but you can change it in the app settings at any time."; + +/* Title of an alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.title" = "Keep optimising images?"; + +/* Title of button for turning off image optimization, displayed in the alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.turnOff" = "No, turn off"; + +/* Title of button for leaving on image optimization, displayed in the alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.turnOn" = "Yes, leave on"; /* translators: displays audio file extension. e.g. MP3 audio file */ "audio file" = "audio file"; +/* Alert message when something goes wrong with the selected image. */ +"avatarMenu.failedToSetAvatarAlertMessage" = "Unable to load the image. Please choose a different one or try again later."; + +/* Title for menu that is shown when you tap your gravatar */ +"avatarMenu.title" = "Update Gravatar"; + /* The title of a button to close the classic editor deprecation notice alert dialog. */ "aztecPost.deprecationNotice.dismiss" = "Dismiss"; /* User action to dismiss media options. */ "aztecPost.mediaAttachmentActionSheet.dismiss" = "Dismiss"; +/* Button title for the button that shows the Blaze flow when tapped. */ +"blaze.campaigns.create.button.title" = "Create"; + +/* Text displayed when there are no Blaze campaigns to display. */ +"blaze.campaigns.empty.subtitle" = "You have not created any campaigns yet. Click create to get started."; + +/* Title displayed when there are no Blaze campaigns to display. */ +"blaze.campaigns.empty.title" = "You have no campaigns"; + +/* Text displayed when there is a failure loading Blaze campaigns. */ +"blaze.campaigns.errorMessage" = "There was an error loading campaigns."; + +/* Title for the view when there's an error loading Blaze campiagns. */ +"blaze.campaigns.errorTitle" = "Oops"; + +/* Displayed while Blaze campaigns are being loaded. */ +"blaze.campaigns.loading.title" = "Loading campaigns..."; + +/* Title for the screen that allows users to manage their Blaze campaigns. */ +"blaze.campaigns.title" = "Blaze Campaigns"; + /* Description for the Blaze dashboard card. */ "blaze.dashboard.card.description" = "Display your work across millions of sites."; /* Title for a menu action in the context menu on the Blaze card. */ "blaze.dashboard.card.menu.hide" = "Hide this"; +/* Title for a menu action in the context menu on the Blaze card. */ +"blaze.dashboard.card.menu.learnMore" = "Learn more"; + /* Title for the Blaze dashboard card. */ "blaze.dashboard.card.title" = "Promote your content with Blaze"; @@ -9543,21 +9415,139 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Button title for the Blaze overlay prompting users to blaze the selected post. */ "blaze.overlay.withPost.buttonTitle" = "Blaze this post"; +/* Short status description */ +"blazeCampaign.status.active" = "Active"; + +/* Short status description */ +"blazeCampaign.status.approved" = "Approved"; + +/* Short status description */ +"blazeCampaign.status.canceled" = "Cancelled"; + +/* Short status description */ +"blazeCampaign.status.completed" = "Completed"; + +/* Short status description */ +"blazeCampaign.status.inmoderation" = "In Moderation"; + +/* Short status description */ +"blazeCampaign.status.processing" = "Processing"; + +/* Short status description */ +"blazeCampaign.status.rejected" = "Rejected"; + +/* Short status description */ +"blazeCampaign.status.scheduled" = "Scheduled"; + +/* Title for budget stats view */ +"blazeCampaigns.budget" = "Budget"; + +/* Title for impressions stats view */ +"blazeCampaigns.clicks" = "Clicks"; + +/* Title for impressions stats view */ +"blazeCampaigns.impressions" = "Impressions"; + /* Title for the context menu action that hides the dashboard card. */ "blogDashboard.contextMenu.hideThis" = "Hide this"; /* Action shown in a bottom notice to dismiss it. */ "blogDashboard.dismiss" = "Dismiss"; +/* Context menu button title */ +"blogHeader.actionCopyURL" = "Copy URL"; + +/* Context menu button title */ +"blogHeader.actionVisitSite" = "Visit site"; + +/* Title for a button that, when tapped, shows more info about participating in Bloganuary. */ +"bloganuary.dashboard.card.button.learnMore" = "Learn more"; + +/* Short description for the Bloganuary event, shown right below the title. */ +"bloganuary.dashboard.card.description" = "For the month of January, blogging prompts will come from Bloganuary – our community challenge to build a blogging habit for the new year."; + +/* Title for the Bloganuary dashboard card while Bloganuary is running. */ +"bloganuary.dashboard.card.runningTitle" = "Bloganuary is here!"; + +/* Title for the Bloganuary dashboard card. */ +"bloganuary.dashboard.card.title" = "Bloganuary is coming!"; + +/* Title of a button that calls the user to enable the Blogging Prompts feature. */ +"bloganuary.learnMore.modal.button.promptsDisabled" = "Turn on blogging prompts"; + +/* Title of a button that will dismiss the Bloganuary modal when tapped. +Note that the word 'go' here should have a closer meaning to 'start' rather than 'move forward'. */ +"bloganuary.learnMore.modal.button.promptsEnabled" = "Let’s go!"; + +/* The second line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.description.second" = "Publish your response."; + +/* The third line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.description.third" = "Read other bloggers’ responses to get inspiration and make new connections."; + +/* The first line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.descriptions.first" = "Receive a new prompt to inspire you each day."; + +/* An additional piece of information shown in case the user has the Blogging Prompts feature disabled. */ +"bloganuary.learnMore.modal.footer.addition" = "To join Bloganuary you need to enable Blogging Prompts."; + +/* An informative excerpt shown in a subtler tone. */ +"bloganuary.learnMore.modal.footer.text" = "Bloganuary will use daily blogging prompts to send you topics for the month of January."; + +/* The headline text of the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.headline" = "Join our month-long writing challenge"; + /* Verb. Dismisses the blogging prompt notification. */ "bloggingPrompt.pushNotification.customActionDescription.dismiss" = "Dismiss"; /* Used when displaying author of a plugin. */ "by %@" = "by %@"; +/* Option for users to rate a chat bot answer as helpful. */ +"chat.rateHelpful" = "Rate as helpful"; + /* Displayed in the confirmation alert when marking comment notifications as read. */ "comment" = "comment"; +/* Sentence fragment. +The full phrase is 'Comments on' followed by the title of the post on a separate line. */ +"comment.header.subText.commentThread" = "Comments on"; + +/* Provides a hint that the current screen displays a comment on a post. +The title of the post will be displayed below this text. +Example: Comment on + My First Post */ +"comment.header.subText.post" = "Comment on"; + +/* Provides a hint that the current screen displays a reply to a comment. +%1$@ is a placeholder for the comment author's name that's been replied to. +Example: Reply to Pamela Nguyen */ +"comment.header.subText.reply" = "Reply to %1$@"; + +/* Error message informing a user about an invalid password. */ +"common.reEnterPasswordMessage" = "The username or password stored in the app may be out of date. Please re-enter your password in the settings and try again."; + +/* An error message. */ +"common.unableToConnect" = "Unable to Connect"; + +/* Footnote for the privacy compliance popover. */ +"compliance.analytics.popover.footnote" = "These cookies allow us to optimise performance by collecting information on how users interact with our websites."; + +/* Save Button Title for the privacy compliance popover. */ +"compliance.analytics.popover.save.button" = "Save"; + +/* Settings Button Title for the privacy compliance popover. */ +"compliance.analytics.popover.settings.button" = "Go to Settings"; + +/* Subtitle for the privacy compliance popover. */ +"compliance.analytics.popover.subtitle" = "We process your personal data to optimise our website and marketing activities based on your consent and our legitimate interest."; + +/* Title for the privacy compliance popover. */ +"compliance.analytics.popover.title" = "Manage privacy"; + +/* Toggle Title for the privacy compliance popover. */ +"compliance.analytics.popover.toggle" = "Analytics"; + /* The menu item to select during a guided tour. */ "connections" = "connections"; @@ -9588,6 +9578,33 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Personialize home tab button title */ "dasboard.personalizeHomeButtonTitle" = "Personalise your home tab"; +/* Title for a menu action in the context menu on the Jetpack Social dashboard card. */ +"dashboard.card.social.menu.hide" = "Hide this"; + +/* Title for the Jetpack Social dashboard card when the user has no social connections. */ +"dashboard.card.social.noconnections.title" = "Share across your social networks"; + +/* Title for the Jetpack Social dashboard card when the user has no social shares left. */ +"dashboard.card.social.noshares.title" = "You’re out of shares!"; + +/* Title for comments button on dashboard. */ +"dashboard.menu.comments" = "Comments"; + +/* Title for media button on dashboard. */ +"dashboard.menu.media" = "Media"; + +/* Title for more button on dashboard. */ +"dashboard.menu.more" = "More"; + +/* Title for pages button on dashboard. */ +"dashboard.menu.pages" = "Pages"; + +/* Title for posts button on dashboard. */ +"dashboard.menu.posts" = "Posts"; + +/* Title for stats button on dashboard. */ +"dashboard.menu.stats" = "Stats"; + /* Title for the Activity Log dashboard card context menu item that navigates the user to the full Activity Logs screen. */ "dashboardCard.ActivityLog.contextMenu.allActivity" = "All activity"; @@ -9600,6 +9617,24 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title for the Pages dashboard card. */ "dashboardCard.Pages.title" = "Pages"; +/* Title for impressions stats view */ +"dashboardCard.blazeCampaigns.clicks" = "Clicks"; + +/* Title of a button that starts the campaign creation flow. */ +"dashboardCard.blazeCampaigns.createCampaignButton" = "Create campaign"; + +/* Title for impressions stats view */ +"dashboardCard.blazeCampaigns.impressions" = "Impressions"; + +/* Title for the Learn more button in the More menu. */ +"dashboardCard.blazeCampaigns.learnMore" = "Learn more"; + +/* Title for the card displaying blaze campaigns. */ +"dashboardCard.blazeCampaigns.title" = "Blaze campaign"; + +/* Title for the View All Campaigns button in the More menu */ +"dashboardCard.blazeCampaigns.viewAllCampaigns" = "View all campaigns"; + /* Title of a button that starts the page creation flow. */ "dashboardCard.pages.add.button.title" = "Add pages to your site"; @@ -9618,6 +9653,15 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of a label that encourages the user to create a new page. */ "dashboardCard.pages.create.description" = "Start with bespoke, mobile friendly layouts."; +/* Title for the View stats button in the More menu */ +"dashboardCard.stats.viewStats" = "View stats"; + +/* Feature flags menu item */ +"debugMenu.featureFlags" = "Feature Flags"; + +/* General section title */ +"debugMenu.generalSectionTitle" = "General"; + /* Remote config params debug menu footer explaining the meaning of a cell with a checkmark. */ "debugMenu.remoteConfig.footer" = "Overridden parameters are denoted by a checkmark."; @@ -9630,22 +9674,100 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Remote Config debug menu title */ "debugMenu.remoteConfig.title" = "Remote Config"; +/* Remove current quick start tour menu item */ +"debugMenu.removeQuickStart" = "Remove Current Tour"; + /* Title for a menu action in the context menu on the Jetpack install card. */ "domain.dashboard.card.menu.hide" = "Hide this"; -/* Domain Purchase Completion footer */ -"domain.purchase.preview.footer" = "It may take up to 30 minutes for your custom domain to start working."; +/* Search domain - Title for the Suggested domains screen */ +"domain.management.addDomain.search.title" = "Search for a domain"; -/* Domain Purchase Completion description (only for FREE domains). */ -"domain.purchase.preview.free.description" = "Next, we'll help you get it ready to be browsed."; +/* Domain management buy domain card button title */ +"domain.management.buy.card.button.title" = "Get Domain"; -/* Domain Purchase Completion description (only for PAID domains). */ -"domain.purchase.preview.paid.description" = "We’ve emailed your receipt. Next, we'll help you get it ready for everyone."; +/* Domain management buy domain card subtitle */ +"domain.management.buy.card.subtitle" = "Add a site later."; -/* Reflects that site is live when domain purchase feature flag is ON. */ -"domain.purchase.preview.title" = "Kudos, your site is live!"; +/* Domain management buy domain card title */ +"domain.management.buy.card.title" = "Just buy a domain"; -/* The 'Best Alternative' label under the domain name in 'Choose a domain' screen */ +/* The expired label of the domain card in All Domains screen. */ +"domain.management.card.expired.label" = "Expired"; + +/* The renews label of the domain card in All Domains screen. */ +"domain.management.card.renews.label" = "Renews"; + +/* The empty state button title in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.button.title" = "Find a domain"; + +/* The empty state description in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.description" = "Tap below to find your perfect domain."; + +/* The empty state title in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.title" = "You don't have any domains"; + +/* The empty state description in All Domains screen when an error occurs */ +"domain.management.error.empty.state.description" = "We encountered an error while loading your domains. Please contact support if the issue persists."; + +/* The empty state title in All Domains screen when an error occurs */ +"domain.management.error.empty.state.title" = "Something went wrong"; + +/* The empty state button title in All Domains screen when an error occurs */ +"domain.management.error.state.button.title" = "Try again"; + +/* The empty state description in All Domains screen when the user is offline */ +"domain.management.offline.empty.state.description" = "Please check your network connection and try again."; + +/* The empty state title in All Domains screen when the user is offline */ +"domain.management.offline.empty.state.title" = "No Internet Connection"; + +/* Domain management choose site card button title */ +"domain.management.purchase.footer" = "*A free domain for one year is included with all paid annual plans"; + +/* Domain management purchase domain screen title */ +"domain.management.purchase.subtitle" = "Don't worry, you can easily add a site later."; + +/* Domain management purchase domain screen title. */ +"domain.management.purchase.title" = "Choose how to use your domain"; + +/* The search bar title in All Domains screen. */ +"domain.management.search-bar.title" = "Search domains"; + +/* The empty state description in All Domains screen when the are no domains matching the search criteria */ +"domain.management.search.empty.state.description" = "We couldn't find any domains that match your search for '%@'"; + +/* The empty state title in All Domains screen when the are no domains matching the search criteria */ +"domain.management.search.empty.state.title" = "No Matching Domains Found"; + +/* Domain management choose site card button title */ +"domain.management.site.card.button.title" = "Choose Site"; + +/* Domain management choose site card subtitle */ +"domain.management.site.card.footer" = "Free domain for the first year*"; + +/* Domain management choose site card subtitle */ +"domain.management.site.card.subtitle" = "Use with a site you already started."; + +/* Domain management choose site card title */ +"domain.management.site.card.title" = "Existing WordPress.com site"; + +/* Domain Management Screen Title */ +"domain.management.title" = "All Domains"; + +/* Domain Purchase Completion footer */ +"domain.purchase.preview.footer" = "It may take up to 30 minutes for your custom domain to start working."; + +/* Domain Purchase Completion description (only for FREE domains). */ +"domain.purchase.preview.free.description" = "Next, we'll help you get it ready to be browsed."; + +/* Domain Purchase Completion description (only for PAID domains). */ +"domain.purchase.preview.paid.description" = "We’ve emailed your receipt. Next, we'll help you get it ready for everyone."; + +/* Reflects that site is live when domain purchase feature flag is ON. */ +"domain.purchase.preview.title" = "Kudos, your site is live!"; + +/* The 'Best Alternative' label under the domain name in 'Choose a domain' screen */ "domain.suggestions.row.best-alternative" = "Best Alternative"; /* The text to display for paid domains on sale in 'Site Creation > Choose a domain' screen */ @@ -9654,6 +9776,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* The text to display for free domains in 'Site Creation > Choose a domain' screen */ "domain.suggestions.row.free" = "Free"; +/* The text to display for paid domains that are free for the first year with the paid plan in 'Site Creation > Choose a domain' screen */ +"domain.suggestions.row.free-with-plan" = "Free for the first year with annual paid plans"; + /* The 'Recommended' label under the domain name in 'Choose a domain' screen */ "domain.suggestions.row.recommended" = "Recommended"; @@ -9663,6 +9788,24 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* The text to display for paid domains in 'Site Creation > Choose a domain' screen */ "domain.suggestions.row.yearly" = "per year"; +/* Title for the checkout screen. */ +"domains.checkout.title" = "Checkout"; + +/* Action shown in a bottom notice to dismiss it. */ +"domains.failure.dismiss" = "Dismiss"; + +/* Content show when the domain selection action fails. */ +"domains.failure.title" = "Sorry, the domain you are trying to add cannot be bought on the Jetpack app at this time."; + +/* Title for the screen where the user can choose how to use the domain they're end up purchasing. */ +"domains.purchase.choice.title" = "Purchase Domain"; + +/* Back button title that navigates back to the search domains screen. */ +"domains.search.backButton.title" = "Search"; + +/* Title of screen where user chooses a site to connect to their selected domain */ +"domains.sitePicker.title" = "Choose Site"; + /* No comment provided by engineer. */ "double-tap to change unit" = "double tap to change unit"; @@ -9680,6 +9823,18 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ Site Address placeholder */ "example.com" = "example.com"; +/* Title for confirmation navigation bar button item */ +"externalMediaPicker.add" = "Add"; + +/* Bottom toolbar title in the selection mode */ +"externalMediaPicker.toolbarSelectItemsPrompt" = "Select Images"; + +/* Bottom toolbar title in the selection mode */ +"externalMediaPicker.toolbarViewSelected" = "View Selected (%@)"; + +/* Title of screen the displays the details of an advertisement campaign. */ +"feature.blaze.campaignDetails.title" = "Campaign Details"; + /* Name of a feature that allows the user to promote their posts. */ "feature.blaze.title" = "Blaze"; @@ -9695,6 +9850,24 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title for the Free to Paid plans dashboard card. */ "freeToPaidPlans.dashboard.card.shortTitle" = "Free domain with an annual plan"; +/* Done button title on the domain purchase result screen. Closes the screen. */ +"freeToPaidPlans.resultView.done" = "Done"; + +/* Notice on the domain purchase result screen. Tells user how long it might take for their domain to be ready. */ +"freeToPaidPlans.resultView.notice" = "It may take up to 30 minutes for your domain to start working properly"; + +/* Sub-title for the domain purchase result screen. Tells user their domain is being set up. */ +"freeToPaidPlans.resultView.subtitle" = "Your new domain %@ is being set up."; + +/* Title for the domain purchase result screen. Tells user their domain was obtained. */ +"freeToPaidPlans.resultView.title" = "All ready to go!"; + +/* A generic error message for a footer view in a list with pagination */ +"general.pagingFooterView.errorMessage" = "An error occurred"; + +/* A footer retry button */ +"general.pagingFooterView.retry" = "Retry"; + /* Title for button that will open up the blogging reminders screen. */ "growAudienceCell.bloggingReminders.actionButton" = "Set up blogging reminders"; @@ -9758,9 +9931,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* (placeholder) Help the user enter a URL into the field */ "http:\/\/my-site-address (URL)" = "http:\/\/my-site-address (URL)"; -/* Label displayed on image media items. */ -"image" = "image"; - /* Sentence to justify why the app is asking permission from the user to use their camera. */ "infoplist.NSCameraUsageDescription" = "To take photos or videos to use in your posts."; @@ -10061,15 +10231,159 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Indicating that referrer was marked as spam */ "marked as spam" = "marked as spam"; -/* Verb. User action to dismiss error alert when failing to load media item. */ -"mediaItemTable.errorAlert.dismissButton" = "Dismiss"; +/* Products header text in Me Screen. */ +"me.products.header" = "Products"; + +/* Title of error prompt shown when a sync fails. */ +"media.syncFailed" = "Unable to sync media"; + +/* An error message the app shows if media import fails */ +"mediaExporter.error.unknown" = "The item could not be added to the Media library"; + +/* An error message the app shows if media import fails */ +"mediaExporter.error.unsupportedContentType" = "Unsupported content type"; + +/* Message of an alert informing users that the video they are trying to select is not allowed. */ +"mediaExporter.videoLimitExceededError" = "Uploading videos longer than 5 minutes requires a paid plan."; + +/* Accessibility hint for add button to add items to the user's media library */ +"mediaLibrary.addButtonAccessibilityHint" = "Add new media"; + +/* Accessibility label for add button to add items to the user's media library */ +"mediaLibrary.addButtonAccessibilityLabel" = "Add"; + +/* Button name in the more menu */ +"mediaLibrary.aspectRatioGrid" = "Aspect Ratio Grid"; + +/* Context menu button */ +"mediaLibrary.buttonDelete" = "Delete"; + +/* Media screen navigation bar button Select title */ +"mediaLibrary.buttonSelect" = "Select"; + +/* Context menu button */ +"mediaLibrary.buttonShare" = "Share"; + +/* Verb. Button title. Tapping cancels an action. */ +"mediaLibrary.deleteConfirmationCancel" = "Cancel"; + +/* Title for button that permanently deletes one or more media items (photos / videos) */ +"mediaLibrary.deleteConfirmationConfirm" = "Delete"; + +/* Message prompting the user to confirm that they want to permanently delete a group of media items. */ +"mediaLibrary.deleteConfirmationMessageMany" = "Are you sure you want to permanently delete these items?"; + +/* Message prompting the user to confirm that they want to permanently delete a media item. Should match Calypso. */ +"mediaLibrary.deleteConfirmationMessageOne" = "Are you sure you want to permanently delete this item?"; + +/* Text displayed in HUD if there was an error attempting to delete a group of media items. */ +"mediaLibrary.deletionFailureMessage" = "Unable to delete all media items."; + +/* Text displayed in HUD while a media item is being deleted. */ +"mediaLibrary.deletionProgressViewTitle" = "Deleting..."; + +/* Text displayed in HUD after successfully deleting a media item */ +"mediaLibrary.deletionSuccessMessage" = "Deleted!"; + +/* The name of the media filter */ +"mediaLibrary.filterAll" = "All"; + +/* The name of the media filter */ +"mediaLibrary.filterAudio" = "Audio"; + +/* The name of the media filter */ +"mediaLibrary.filterDocuments" = "Documents"; + +/* The name of the media filter */ +"mediaLibrary.filterImages" = "Images"; + +/* The name of the media filter */ +"mediaLibrary.filterVideos" = "Videos"; + +/* User action to delete un-uploaded media. */ +"mediaLibrary.retryOptionsAlert.delete" = "Delete"; /* Verb. Button title. Tapping dismisses a prompt. */ "mediaLibrary.retryOptionsAlert.dismissButton" = "Dismiss"; +/* User action to retry media upload. */ +"mediaLibrary.retryOptionsAlert.retry" = "Retry Upload"; + +/* Message displayed when no results are returned from a media library search. Should match Calypso. */ +"mediaLibrary.searchResultsEmptyTitle" = "No media matching your search"; + +/* Text displayed in HUD if there was an error attempting to share a group of media items. */ +"mediaLibrary.sharingFailureMessage" = "Unable to share the selected items."; + +/* Button name in the more menu */ +"mediaLibrary.squareGrid" = "Square Grid"; + +/* Media screen navigation title */ +"mediaLibrary.title" = "Media"; + +/* Bottom toolbar title in the selection mode */ +"mediaLibrary.toolbarSelectImagesMany" = "%d Images Selected"; + +/* Bottom toolbar title in the selection mode */ +"mediaLibrary.toolbarSelectImagesOne" = "1 Image Selected"; + +/* Bottom toolbar title in the selection mode */ +"mediaLibrary.toolbarSelectItemsMany" = "%d Items Selected"; + +/* Bottom toolbar title in the selection mode */ +"mediaLibrary.toolbarSelectItemsOne" = "1 Item Selected"; + +/* Bottom toolbar title in the selection mode */ +"mediaLibrary.toolbarSelectItemsPrompt" = "Select Items"; + /* The title of the button to dismiss the alert shown when the picked media cannot be imported into stories. */ "mediaPicker.failedMediaExportAlert.dismissButton" = "Dismiss"; +/* Error message when picked media cannot be imported into stories. */ +"mediaPicker.failedMediaExportAlert.message" = "Your media could not be exported. If the problem persists, you can contact us via the Me > Help & Support screen."; + +/* Error title when picked media cannot be imported into stories. */ +"mediaPicker.failedMediaExportAlert.title" = "Failed Media Export"; + +/* Message for alert when access to camera is not granted */ +"mediaPicker.noCameraAccessMessage" = "This app needs permission to access the Camera to capture new media, please change the privacy settings if you wish to allow this."; + +/* Title for alert when access to camera is not granted */ +"mediaPicker.noCameraAccessTitle" = "Media Capture"; + +/* Button that opens the Settings app */ +"mediaPicker.openSettings" = "Open Settings"; + +/* The name of the action in the context menu for selecting photos from Tenor (free GIF library) */ +"mediaPicker.pickFromFreeGIFLibrary" = "Free GIF Library"; + +/* The name of the action in the context menu (user's WordPress Media Library */ +"mediaPicker.pickFromMediaLibrary" = "Choose from Media"; + +/* The name of the action in the context menu for selecting photos from other apps (Files app) */ +"mediaPicker.pickFromOtherApps" = "Other Files"; + +/* The name of the action in the context menu */ +"mediaPicker.pickFromPhotosLibrary" = "Choose from Device"; + +/* The name of the action in the context menu for selecting photos from free stock photos */ +"mediaPicker.pickFromStockPhotos" = "Free Photo Library"; + +/* The name of the action in the context menu */ +"mediaPicker.takePhoto" = "Take Photo"; + +/* The name of the action in the context menu */ +"mediaPicker.takePhotoOrVideo" = "Take Photo or Video"; + +/* The name of the action in the context menu */ +"mediaPicker.takeVideo" = "Take Video"; + +/* Navigation title for media preview. Example: 1 of 3 */ +"mediaPreview.NofM" = "%1$@ of %2$@"; + +/* Max image size in pixels (e.g. 300x300px) */ +"mediaSizeSlider.valueFormat" = "%1$d × %2$d px"; + /* The description in the Delete WordPress screen */ "migration.deleteWordpress.description" = "It looks like you still have the WordPress app installed."; @@ -10082,9 +10396,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* The title in the Delete WordPress screen */ "migration.deleteWordpress.title" = "You no longer need the WordPress app on your device"; -/* Primary button title in the migration done screen. */ -"migration.done.actions.primary.title" = "Finish"; - /* Footer for the migration done screen. */ "migration.done.footer" = "We recommend uninstalling the WordPress app on your device to avoid data conflicts."; @@ -10094,6 +10405,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Primary description in the migration done screen. */ "migration.done.primaryDescription" = "We’ve transferred all your data and settings. Everything is right where you left it."; +/* Secondary description (second paragraph) in the migration done screen. */ +"migration.done.secondaryDescription" = "It's time to continue your WordPress journey on the Jetpack app!"; + /* Title of the migration done screen. */ "migration.done.title" = "Thanks for switching to Jetpack!"; @@ -10142,6 +10456,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* The title in the migration welcome screen */ "migration.welcome.title" = "Welcome to Jetpack!"; +/* Primary button title in the migration done screen. */ +"migrationDone.actions.primaryTitle" = "Let's go"; + /* Description for the static screen displayed prompting users to switch the Jetpack app. */ "movedToJetpack.description" = "The Jetpack app has all the WordPress app’s functionality, and now exclusive access to Stats, Reader, Notifications and more."; @@ -10178,9 +10495,69 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title for the card displaying draft posts. */ "my-sites.drafts.card.title" = "Work on a draft post"; +/* Title for the View all drafts button in the More menu */ +"my-sites.drafts.card.viewAllDrafts" = "View all drafts"; + +/* Title for the View all scheduled drafts button in the More menu */ +"my-sites.scheduled.card.viewAllScheduledPosts" = "View all scheduled posts"; + /* Title for the card displaying today's stats. */ "my-sites.stats.card.title" = "Today's Stats"; +/* Title for the domain focus card on My Site */ +"mySite.domain.focus.cardCell.title" = "News"; + +/* Button title of the domain focus card on My Site */ +"mySite.domain.focus.cardView.button.title" = "Transfer your domains"; + +/* Description of the domain focus card on My Site */ +"mySite.domain.focus.cardView.description" = "As you may know, Google Domains has been sold to Squarespace. Transfer your domains to WordPress.com now, and we'll pay all transfer fees plus an extra year of your domain registration."; + +/* Title of the domain focus card on My Site */ +"mySite.domain.focus.cardView.title" = "Reclaim your Google Domains"; + +/* Action sheet button title. Launches the flow to a add self-hosted site. */ +"mySite.noSites.actionSheet.addSelfHostedSite" = "Add self-hosted site"; + +/* Action sheet button title. Launches the flow to create a WordPress.com site. */ +"mySite.noSites.actionSheet.createWPComSite" = "Create WordPress.com site"; + +/* Button title. Displays the account and setting screen. */ +"mySite.noSites.button.accountAndSettings" = "Account and settings"; + +/* Button title. Displays a screen to add a new site when tapped. */ +"mySite.noSites.button.addNewSite" = "Add new site"; + +/* Message description for when a user has no sites. */ +"mySite.noSites.description" = "Create a new site for your business, magazine, or personal blog; or connect an existing WordPress installation."; + +/* Message title for when a user has no sites. */ +"mySite.noSites.title" = "You don't have any sites"; + +/* Menu title for the add site option */ +"mySite.siteActions.addSite" = "Add site"; + +/* Button that reveals more site actions */ +"mySite.siteActions.button" = "Site Actions"; + +/* Accessibility hint for button used to show more site actions */ +"mySite.siteActions.hint" = "Tap to show more site actions"; + +/* Menu title for the personalize home option */ +"mySite.siteActions.personalizeHome" = "Personalise home"; + +/* Menu title for the change site icon option */ +"mySite.siteActions.siteIcon" = "Change site icon"; + +/* Menu title for the change site title option */ +"mySite.siteActions.siteTitle" = "Change site title"; + +/* Menu title for the switch site option */ +"mySite.siteActions.switchSite" = "Switch site"; + +/* Menu title for the visit site option */ +"mySite.siteActions.visitSite" = "Visit site"; + /* Dismiss button title. */ "noResultsViewController.dismissButton" = "Dismiss"; @@ -10196,14 +10573,20 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* This is one of the buttons we display when prompting the user for a review */ "notifications.appRatings.sendFeedback.yes.buttonTitle" = "Send feedback"; -/* Word separating the current index from the total amount. I.e.: 7 of 9 */ -"of" = "of"; +/* Badge for page cells */ +"pageList.badgeHomepage" = "Homepage"; + +/* Badge for page cells */ +"pageList.badgeLocalChanges" = "Local changes"; + +/* Badge for page cells */ +"pageList.badgePendingReview" = "Pending review"; -/* Label displayed on media items that are not video, image, or audio. */ -"other" = "other"; +/* Badge for page cells */ +"pageList.badgePosts" = "Posts page"; -/* Promote the page with Blaze. */ -"pages.blaze.actionTitle" = "Promote with Blaze"; +/* Badge for page cells */ +"pageList.badgePrivate" = "Private"; /* Subtitle of the theme template homepage cell */ "pages.template.subtitle" = "Your homepage is using a Theme template and will open in the web editor."; @@ -10211,12 +10594,45 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of the theme template homepage cell */ "pages.template.title" = "Homepage"; +/* Message informing the user that their static homepage page was set successfully */ +"pages.updatePage.successTitle" = "Page successfully updated"; + +/* Delete option in the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.actionTitle" = "Delete Permanently"; + +/* Message of the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.alertMessage" = "Are you sure you want to permanently delete this page?"; + +/* Title of the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.alertTitle" = "Delete Permanently?"; + +/* Menu option for filtering posts by everyone */ +"pagesList.pagesByEveryone" = "Pages by everyone"; + +/* Menu option for filtering posts by me */ +"pagesList.pagesByMe" = "Pages by me"; + +/* Trash option in the trash page confirmation alert. */ +"pagesList.trash.actionTitle" = "Move to Bin"; + +/* Message of the trash page confirmation alert. */ +"pagesList.trash.alertMessage" = "Are you sure you want to bin this page?"; + +/* Title of the trash page confirmation alert. */ +"pagesList.trash.alertTitle" = "Bin this page?"; + +/* Cancels an Action */ +"pagesList.trash.cancel" = "Cancel"; + /* No comment provided by engineer. */ "password" = "password"; /* Section footer displayed below the list of toggles */ "personalizeHome.cardsSectionFooter" = "Cards may show different content depending on what's happening on your site. We're working on more cards and controls."; +/* Section header */ +"personalizeHome.cardsSectionHeader" = "Show or hide cards"; + /* Card title for the pesonalization menu */ "personalizeHome.dashboardCard.activityLog" = "Recent activity"; @@ -10238,12 +10654,60 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Card title for the pesonalization menu */ "personalizeHome.dashboardCard.todaysStats" = "Today's stats"; +/* Section header for shortcuts */ +"personalizeHome.shortcutsSectionHeader" = "Show or hide shortcuts"; + /* Page title */ "personalizeHome.title" = "Personalise Home Tab"; /* Register Domain - Domain contact information field Phone */ "phone number" = "phone number"; +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.createdTimeAgo" = "Created %@"; + +/* Status mesasge for post cells */ +"post.deletingPostPermanentlyStatusMessage" = "Deleting post..."; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.editedTimeAgo" = "Edited %@"; + +/* Status mesasge for post cells */ +"post.movingToTrashStatusMessage" = "Moving post to bin..."; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.publishedTimeAgo" = "Published %@"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.scheduledForDate" = "Scheduled %@"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.trashedTimeAgo" = "Binned %@"; + +/* Accessibility label for the post author in the post list. The parameter is the author name. For example, \"By Elsa.\" */ +"postList.a11y.authorChunkFormat" = "By %@."; + +/* Accessibility label for a post's excerpt in the post list. The parameter is the post excerpt. For example, \"Excerpt. This is the first paragraph.\" */ +"postList.a11y.exerptChunkFormat" = "Excerpt. %@."; + +/* Accessibility label for a sticky post in the post list. */ +"postList.a11y.sticky" = "Sticky."; + +/* Accessibility label for a post in the post list. The first placeholder is the post title. The second placeholder is the date. */ +"postList.a11y.titleAndDateChunkFormat" = "%1$@, %2$@."; + +/* Title for the 'Trash' post list row swipe action */ +"postList.swipeActionDelete" = "Bin"; + +/* Title for the 'Delete' post list row swipe action */ +"postList.swipeActionDeletePermanently" = "Delete"; + +/* Title for the 'Share' post list row swipe action */ +"postList.swipeActionShare" = "Share"; + +/* Title for the 'View' post list row swipe action */ +"postList.swipeActionView" = "View"; + /* User action to dismiss featured media options. */ "postSettings.featuredImageUploadActionSheet.dismiss" = "Dismiss"; @@ -10259,9 +10723,151 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Section title for the disabled Twitter service in the Post Settings screen */ "postSettings.section.disabledTwitter.header" = "Twitter Auto-Sharing Is No Longer Available"; +/* Button in Post Settings */ +"postSettings.setFeaturedImageButton" = "Set Featured Image"; + +/* Error message on post/page settings screen */ +"postSettings.updateFailedMessage" = "Failed to update the post settings"; + /* Promote the post with Blaze. */ "posts.blaze.actionTitle" = "Promote with Blaze"; +/* Label for the Post List option that cancels automatic uploading of a post. */ +"posts.cancelUpload.actionTitle" = "Cancel upload"; + +/* Label for post comments option. Tapping displays comments for a post. */ +"posts.comments.actionTitle" = "Comments"; + +/* Label for the delete post option. Tapping permanently deletes a post. */ +"posts.delete.actionTitle" = "Delete permanently"; + +/* Label for an option that moves a post to the draft folder */ +"posts.draft.actionTitle" = "Move to draft"; + +/* Label for post duplicate option. Tapping creates a copy of the post. */ +"posts.duplicate.actionTitle" = "Duplicate"; + +/* Opens a submenu for page attributes. */ +"posts.pageAttributes.actionTitle" = "Page attributes"; + +/* Label for the preview post button. Tapping displays the post as it appears on the web. */ +"posts.preview.actionTitle" = "Preview"; + +/* Label for an option that moves a publishes a post immediately */ +"posts.publish.actionTitle" = "Publish now"; + +/* Retry uploading the post. */ +"posts.retry.actionTitle" = "Retry"; + +/* Set the selected page as the homepage. */ +"posts.setHomepage.actionTitle" = "Set as homepage"; + +/* Set the parent page for the selected page. */ +"posts.setParent.actionTitle" = "Set parent"; + +/* Set the selected page as a posts page. */ +"posts.setPostsPage.actionTitle" = "Set as posts page"; + +/* Set the selected page as a regular page. */ +"posts.setRegularPage.actionTitle" = "Set as regular page"; + +/* Label for post settings option. Tapping displays settings for a post. */ +"posts.settings.actionTitle" = "Settings"; + +/* Share the post. */ +"posts.share.actionTitle" = "Share"; + +/* Label for post stats option. Tapping displays statistics for a post. */ +"posts.stats.actionTitle" = "Stats"; + +/* Label for a option that moves a post to the trash folder */ +"posts.trash.actionTitle" = "Move to bin"; + +/* Label for the view post button. Tapping displays the post as it appears on the web. */ +"posts.view.actionTitle" = "View"; + +/* A short message explaining that a page was deleted permanently. */ +"postsList.deletePage.message" = "Page deleted permanently"; + +/* A short message explaining that a post was deleted permanently. */ +"postsList.deletePost.message" = "Post deleted permanently"; + +/* A short message explaining that a page was moved to the trash bin. */ +"postsList.movePageToTrash.message" = "Page moved to bin"; + +/* A short message explaining that a post was moved to the trash bin. */ +"postsList.movePostToTrash.message" = "Post moved to bin"; + +/* Menu option for filtering posts by everyone */ +"postsList.postsByEveryone" = "Posts by everyone"; + +/* Menu option for filtering posts by me */ +"postsList.postsByMe" = "Posts by me"; + +/* Title for the button to subscribe to Jetpack Social on the remaining shares view */ +"postsettings.social.remainingshares.subscribe" = "Subscribe now to share more"; + +/* The second half of the remaining social shares a user has. This is only displayed when there is no social limit warning. */ +"postsettings.social.remainingshares.text.part" = " in the next 30 days"; + +/* Beginning text of the remaining social shares a user has left. %1$d is their current remaining shares. This text is combined with ' in the next 30 days' if there is no warning displayed. */ +"postsettings.social.shares.text.format" = "%1$d social shares remaining"; + +/* The primary label for the auto-sharing row on the pre-publishing sheet. +Indicates the number of social accounts that will be sharing the blog post. +%1$d is a placeholder for the number of social network accounts that will be auto-shared. +Example: Sharing to 3 accounts */ +"prepublishing.social.label.multipleConnections" = "Sharing to %1$d accounts"; + +/* The primary label for the auto-sharing row on the pre-publishing sheet. +Indicates the blog post will not be shared to any social accounts. */ +"prepublishing.social.label.notSharing" = "Not sharing to social"; + +/* The primary label for the auto-sharing row on the pre-publishing sheet. +Indicates the number of social accounts that will be sharing the blog post. +This string is displayed when some of the social accounts are turned off for auto-sharing. +%1$d is a placeholder for the number of social media accounts that will be sharing the blog post. +%2$d is a placeholder for the total number of social media accounts connected to the user's blog. +Example: Sharing to 2 of 3 accounts */ +"prepublishing.social.label.partialConnections" = "Sharing to %1$d of %2$d accounts"; + +/* The primary label for the auto-sharing row on the pre-publishing sheet. +Indicates the blog post will be shared to a social media account. +%1$@ is a placeholder for the account name. +Example: Sharing to @wordpress */ +"prepublishing.social.label.singleConnection" = "Sharing to %1$@"; + +/* A subtext that's shown below the primary label in the auto-sharing row on the pre-publishing sheet. +Informs the remaining limit for post auto-sharing. +%1$d is a placeholder for the remaining shares. +Example: 27 social shares remaining */ +"prepublishing.social.remainingShares.format" = "%1$d social shares remaining"; + +/* a VoiceOver description for the warning icon to hint that the remaining shares are low. */ +"prepublishing.social.warningIcon.accessibilityHint" = "Warning"; + +/* The navigation title for a screen that edits the sharing message for the post. */ +"prepublishing.socialAccounts.editMessage.navigationTitle" = "Customise message"; + +/* The label for a call-to-action button in the social accounts' footer section. */ +"prepublishing.socialAccounts.footer.button.text" = "Subscribe to share more"; + +/* Text shown below the list of social accounts to indicate how many social shares available for the site. +Note that the '30 days' part is intended to be a static value. +%1$d is a placeholder for the amount of remaining shares. +Example: 27 social shares remaining in the next 30 days */ +"prepublishing.socialAccounts.footer.remainingShares.text" = "%1$d social shares remaining in the next 30 days"; + +/* a VoiceOver description for the warning icon to hint that the remaining shares are low. */ +"prepublishing.socialAccounts.footer.warningIcon.accessibilityHint" = "Warning"; + +/* The label displayed for a table row that displays the sharing message for the post. +Tapping on this row allows the user to edit the sharing message. */ +"prepublishing.socialAccounts.message.label" = "Message"; + +/* The navigation title for the pre-publishing social accounts screen. */ +"prepublishing.socialAccounts.navigationTitle" = "Social"; + /* Title for a tappable string that opens the reader with a prompts tag */ "prompts.card.viewprompts.title" = "View all responses"; @@ -10280,6 +10886,46 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title for the success view when the user has successfully logged in */ "qrLoginVerifyAuthorization.completedInstructions.title" = "You're logged in!"; +/* The quick tour actions item to select during a guided tour. */ +"quickStart.moreMenu" = "More"; + +/* Accessibility hint to inform that the author section can be tapped to see posts from the site. */ +"reader.detail.header.authorInfo.a11y.hint" = "Views posts from the site"; + +/* Title for the Comment button on the Reader Detail toolbar. +Note: Since the display space is limited, a short or concise translation is preferred. */ +"reader.detail.toolbar.comment.button" = "Comment"; + +/* Title for the Like button in the Reader Detail toolbar. +This is shown when the user has not liked the post yet. +Note: Since the display space is limited, a short or concise translation is preferred. */ +"reader.detail.toolbar.like.button" = "Like"; + +/* Accessibility hint for the Like button state. The button shows that the user has not liked the post, +but tapping on this button will add a Like to the post. */ +"reader.detail.toolbar.like.button.a11y.hint" = "Likes this post."; + +/* Title for the Like button in the Reader Detail toolbar. +This is shown when the user has already liked the post. +Note: Since the display space is limited, a short or concise translation is preferred. */ +"reader.detail.toolbar.liked.button" = "Liked"; + +/* Accessibility hint for the Liked button state. The button shows that the user has liked the post, +but tapping on this button will remove their like from the post. */ +"reader.detail.toolbar.liked.button.a11y.hint" = "Unlikes this post."; + +/* Accessibility hint for the 'Save Post' button. */ +"reader.detail.toolbar.save.button.a11y.hint" = "Saves this post for later."; + +/* Accessibility label for the 'Save Post' button. */ +"reader.detail.toolbar.save.button.a11y.label" = "Save post"; + +/* Accessibility hint for the 'Save Post' button when a post is already saved. */ +"reader.detail.toolbar.saved.button.a11y.hint" = "Unsaves this post."; + +/* Accessibility label for the 'Save Post' button when a post has been saved. */ +"reader.detail.toolbar.saved.button.a11y.label" = "Saved Post"; + /* Reader search button accessibility label. */ "reader.navigation.search.button.label" = "Search"; @@ -10293,12 +10939,54 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Notice title when blocking a user fails. */ "reader.notice.user.blocked" = "reader.notice.user.block.failed"; +/* Text for the 'Comment' button on the reader post card cell. */ +"reader.post.button.comment" = "Comment"; + +/* Accessibility hint for the comment button on the reader post card cell */ +"reader.post.button.comment.accessibility.hint" = "Opens the comments for the post."; + +/* Text for the 'Like' button on the reader post card cell. */ +"reader.post.button.like" = "Like"; + +/* Accessibility hint for the like button on the reader post card cell */ +"reader.post.button.like.accessibility.hint" = "Likes the post."; + +/* Text for the 'Liked' button on the reader post card cell. */ +"reader.post.button.liked" = "Liked"; + +/* Accessibility hint for the liked button on the reader post card cell */ +"reader.post.button.liked.accessibility.hint" = "Unlikes the post."; + +/* Accessibility hint for the site header on the reader post card cell */ +"reader.post.button.menu.accessibility.hint" = "Opens a menu with more actions."; + +/* Accessibility label for the more menu button on the reader post card cell */ +"reader.post.button.menu.accessibility.label" = "More"; + +/* Text for the 'Reblog' button on the reader post card cell. */ +"reader.post.button.reblog" = "Reblog"; + +/* Accessibility hint for the reblog button on the reader post card cell */ +"reader.post.button.reblog.accessibility.hint" = "Reblogs the post."; + +/* Accessibility hint for the site header on the reader post card cell */ +"reader.post.header.accessibility.hint" = "Opens the site details for the post."; + /* The title of a button that triggers blocking a user from the user's reader. */ "reader.post.menu.block.user" = "Block this user"; +/* The title of a button that removes a saved post. */ +"reader.post.menu.remove.post" = "Remove Saved Post"; + /* The title of a button that triggers the reporting of a post's author. */ "reader.post.menu.report.user" = "Report this user"; +/* The title of a button that saves a post. */ +"reader.post.menu.save.post" = "Save"; + +/* The formatted number of posts and followers for a site. '%1$@' is a placeholder for the site post count. '%2$@' is a placeholder for the site follower count. Example: `5,000 posts • 10M followers` */ +"reader.site.header.counts" = "%1$@ posts • %2$@ followers"; + /* Spoken accessibility label */ "readerDetail.backButton.accessibilityLabel" = "Back"; @@ -10332,6 +11020,54 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Title for the tooltip anchor. */ "readerDetail.tooltipAnchorTitle.accessibilityLabel" = "New"; +/* The button title for the transfer footer view in Register Domain screen */ +"register.domain.transfer.button.title" = "Transfer domain"; + +/* The title for the transfer footer view in Register Domain screen */ +"register.domain.transfer.title" = "Looking to transfer a domain you already own?"; + +/* Information of what related post are and how they are presented */ +"relatedPostsSettings.optionsFooter" = "Related Posts displays relevant content from your site below your posts."; + +/* Text for related post cell preview */ +"relatedPostsSettings.preview1.details" = "in \"Mobile\""; + +/* Text for related post cell preview */ +"relatedPostsSettings.preview1.title" = "Big iPhone\/iPad Update Now Available"; + +/* Text for related post cell preview */ +"relatedPostsSettings.preview2.details" = "in \"Apps\""; + +/* Text for related post cell preview */ +"relatedPostsSettings.preview2.title" = "The WordPress for Android App Gets a Big Facelift"; + +/* Text for related post cell preview */ +"relatedPostsSettings.preview3.details" = "in \"Upgrade\""; + +/* Text for related post cell preview */ +"relatedPostsSettings.preview3.title" = "Upgrade Focus: VideoPress For Weddings"; + +/* Section title for related posts section preview */ +"relatedPostsSettings.previewsHeaders" = "Preview"; + +/* Label for Related Post header preview */ +"relatedPostsSettings.relatedPostsHeader" = "Related Posts"; + +/* Message to show when setting save failed */ +"relatedPostsSettings.settingsUpdateFailed" = "Settings update failed"; + +/* Label for configuration switch to show/hide the header for the related posts section */ +"relatedPostsSettings.showHeader" = "Show Header"; + +/* Label for configuration switch to enable/disable related posts */ +"relatedPostsSettings.showRelatedPosts" = "Show Related Posts"; + +/* Label for configuration switch to show/hide images thumbnail for the related posts */ +"relatedPostsSettings.showThumbnail" = "Show Images"; + +/* Title for screen that allows configuration of your blog/site related posts settings. */ +"relatedPostsSettings.title" = "Related Posts"; + /* User action to dismiss media options. */ "shareExtension.editor.attachmentActions.dismiss" = "Dismiss"; @@ -10365,9 +11101,102 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Site name that is placed in the tooltip view. */ "site.creation.domain.tooltip.site.name" = "YourSiteName.com"; +/* Back button title shown in Site Creation flow to come back from Plan selection to Domain selection */ +"siteCreation.domain.backButton.title" = "Domains"; + +/* Button to progress to the next step after selecting domain in Site Creation */ +"siteCreation.domains.buttons.selectDomain" = "Select domain"; + +/* Accessibility label for audio items in the media collection view. The parameter is the creation date of the audio. */ +"siteMedia.accessibilityLabelAudio" = "Audio, %@"; + +/* Accessibility label for other media items in the media collection view. The parameter is the filename file. */ +"siteMedia.accessibilityLabelDocument" = "Document, %@"; + +/* Accessibility label for image thumbnails in the media collection view. The parameter is the creation date of the image. */ +"siteMedia.accessibilityLabelImage" = "Image, %@"; + +/* Accessibility label for video thumbnails in the media collection view. The parameter is the creation date of the video. */ +"siteMedia.accessibilityLabelVideo" = "Video, %@"; + +/* Accessibility label to use when creation date from media asset is not know. */ +"siteMedia.accessibilityUnknownCreationDate" = "Unknown creation date"; + +/* Accessibility hint for actions when displaying media items. */ +"siteMedia.cellAccessibilityHint" = "Select media."; + +/* Accessibility hint for media item preview for user's viewing an item in their media library */ +"siteMediaItem.contentViewAccessibilityHint" = "Tap to view media in full screen"; + +/* Accessibility label for media item preview for user's viewing an item in their media library */ +"siteMediaItem.contentViewAccessibilityLabel" = "Preview media"; + +/* Title for confirmation navigation bar button item */ +"siteMediaPicker.add" = "Add"; + +/* Button selection media in media picker */ +"siteMediaPicker.deselect" = "Deselect"; + +/* Button selection media in media picker */ +"siteMediaPicker.select" = "Select"; + +/* Media screen navigation title */ +"siteMediaPicker.title" = "Media"; + +/* Title for screen to select the privacy options for a blog */ +"siteSettings.privacy.title" = "Privacy"; + +/* Hint for users when hidden privacy setting is set */ +"siteVisibility.hidden.hint" = "Your site is hidden from visitors behind a \"Coming Soon\" notice until it is ready for viewing."; + +/* Text for privacy settings: Hidden */ +"siteVisibility.hidden.title" = "Hidden"; + +/* Hint for users when private privacy setting is set */ +"siteVisibility.private.hint" = "Your site is only visible to you and users you approve."; + +/* Text for privacy settings: Private */ +"siteVisibility.private.title" = "Private"; + +/* Hint for users when public privacy setting is set */ +"siteVisibility.public.hint" = "Your site is visible to everyone, and it may be indexed by search engines."; + +/* Text for privacy settings: Public */ +"siteVisibility.public.title" = "Public"; + +/* Text for unknown privacy setting */ +"siteVisibility.unknown.hint" = "Unknown"; + +/* Text for unknown privacy setting */ +"siteVisibility.unknown.title" = "Unknown"; + /* Label for the blogging reminders setting */ "sitesettings.reminders.title" = "Reminders"; +/* Body text for the Jetpack Social no connection view */ +"social.noconnection.body" = "Increase your traffic by auto-sharing your posts with your friends on social media."; + +/* Title for the connect button to add social sharing for the Jetpack Social no connection view */ +"social.noconnection.connectAccounts" = "Connect accounts"; + +/* Accessibility label for the social media icons in the Jetpack Social no connection view */ +"social.noconnection.icons.accessibility.label" = "Social media icons"; + +/* Title for the not now button to hide the Jetpack Social no connection view */ +"social.noconnection.notnow" = "Not now"; + +/* Plural body text for the Jetpack Social no shares dashboard card. %1$d is the number of social accounts the user has. */ +"social.noshares.body.plural" = "Your posts won’t be shared to your %1$d social accounts."; + +/* Singular body text for the Jetpack Social no shares dashboard card. */ +"social.noshares.body.singular" = "Your posts won’t be shared to your social account."; + +/* Accessibility label for the social media icons in the Jetpack Social no shares dashboard card */ +"social.noshares.icons.accessibility.label" = "Social media icons"; + +/* Title for the button to subscribe to Jetpack Social on the no shares dashboard card */ +"social.noshares.subscribe" = "Subscribe to share more"; + /* Section title for the disabled Twitter service in the Social screen */ "social.section.disabledTwitter.header" = "Twitter Auto-Sharing Is No Longer Available"; @@ -10470,9 +11299,21 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Hint displayed on the 'Most Popular Time' stats card when a user's site hasn't yet received enough traffic. */ "stats.insights.mostPopularTime.noData" = "Not enough activity. Check back later when your site's had more visitors!"; +/* A hint shown to the user in stats informing the user how many likes one of their posts has received. The %1$@ placeholder will be replaced with the title of a post, the %2$@ with the number of likes. */ +"stats.insights.totalLikes.guideText.plural" = "Your latest post %1$@ has received %2$@ likes."; + +/* A hint shown to the user in stats informing the user that one of their posts has received a like. The %1$@ placeholder will be replaced with the title of a post, and the %2$@ will be replaced by the numeral one. */ +"stats.insights.totalLikes.guideText.singular" = "Your latest post %1$@ has received %2$@ like."; + /* Dismiss the AlertView */ "stockPhotos.strings.dismiss" = "Dismiss"; +/* Subtitle for placeholder in Free Photos. The company name 'Pexels' should always be written as it is. */ +"stockPhotos.subtitle" = "Photos provided by Pexels"; + +/* Title for placeholder in Free Photos */ +"stockPhotos.title" = "Search to find free photos to add to your Media Library!"; + /* Section title for prominent suggestions */ "suggestions.section.prominent" = "In this conversation"; @@ -10497,6 +11338,69 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Option in Support view to visit the WordPress.org support forums. */ "support.button.visitForum.title" = "Visit WordPress.org"; +/* Indicator that the chat bot is processing user's input. */ +"support.chatBot.botThinkingIndicator" = "Thinking..."; + +/* Dismiss the current view */ +"support.chatBot.close.title" = "Close"; + +/* Button for users to contact the support team directly. */ +"support.chatBot.contactSupport" = "Contact support"; + +/* Initial message shown to the user when the chat starts. */ +"support.chatBot.firstMessage" = "Hi there, I'm the Jetpack AI Assistant.\\n\\nWhat can we help you with?\\n\\nIf I can't answer your question, I'll help you open a support ticket with our team!"; + +/* Placeholder text for the chat input field. */ +"support.chatBot.inputPlaceholder" = "Send a message..."; + +/* An example question shown to a user seeking support */ +"support.chatBot.questionFive" = "I forgot my login information"; + +/* An example question shown to a user seeking support */ +"support.chatBot.questionFour" = "Why can't I log in?"; + +/* An example question shown to a user seeking support */ +"support.chatBot.questionOne" = "What is my site address?"; + +/* An example question shown to a user seeking support */ +"support.chatBot.questionSix" = "How can I use my custom domain in the app?"; + +/* An example question shown to a user seeking support */ +"support.chatBot.questionThree" = "I can't upload photos\/videos"; + +/* An example question shown to a user seeking support */ +"support.chatBot.questionTwo" = "Help, my site is down!"; + +/* Option for users to report a chat bot answer as inaccurate. */ +"support.chatBot.reportInaccuracy" = "Report as inaccurate"; + +/* Button title referring to the sources of information. */ +"support.chatBot.sources" = "Sources"; + +/* Prompt for users suggesting to select a default question from the list to start a support chat. */ +"support.chatBot.suggestionsPrompt" = "Not sure what to ask?"; + +/* Notice informing user that there was an error submitting their support ticket. */ +"support.chatBot.ticketCreationFailure" = "Error submitting support ticket"; + +/* Notice informing user that their support ticket is being created. */ +"support.chatBot.ticketCreationLoading" = "Creating support ticket..."; + +/* Notice informing user that their support ticket has been created. */ +"support.chatBot.ticketCreationSuccess" = "Ticket created"; + +/* Title of the view that shows support chat bot. */ +"support.chatBot.title" = "Contact Support"; + +/* A title for a text that displays a transcript of an answer in a support chat */ +"support.chatBot.zendesk.answer" = "Answer"; + +/* A title for a text that displays a transcript of user's question in a support chat */ +"support.chatBot.zendesk.question" = "Question"; + +/* A title for a text that displays a transcript from a conversation between Jetpack Mobile Bot (chat bot) and a user */ +"support.chatBot.zendesk.transcript" = "Jetpack Mobile Bot transcript"; + /* Suggestion in Support view to visit the Forums. */ "support.row.communityForum.title" = "Ask a question in the community forum and get help from our group of volunteers."; @@ -10557,6 +11461,9 @@ Example: given a notice format "Following %@" and empty site name, this will be /* View title for Help & Support page. */ "support.title" = "Help"; +/* Title for placeholder in Tenor picker */ +"tenor.welcomeMessage" = "Search to find GIFs to add to your Media Library!"; + /* Header of delete screen section listing things that will be deleted. */ "these items will be deleted:" = "these items will be deleted:"; @@ -10572,18 +11479,27 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Displayed in the confirmation alert when marking unread notifications as read. */ "unread" = "unread"; -/* Label displayed on video media items. */ -"video" = "video"; - /* Portion of a message for Jetpack users that have multisite WP installation, thus Restore is not available. This part is a link, colored with a different color. */ "visit our documentation page" = "visit our documentation page"; /* Verb. Dismiss the web view screen. */ "webKit.button.dismiss" = "Dismiss"; +/* Preview title of all-time posts and most views widget */ +"widget.allTimePostViews.previewTitle" = "All-Time Posts & Most Views"; + +/* Preview title of all-time views widget */ +"widget.allTimeViews.previewTitle" = "All-Time Views"; + +/* Preview title of all-time views and visitors widget */ +"widget.allTimeViewsVisitors.previewTitle" = "All-Time Views & Visitors"; + /* Title of best views ever label in all time widget */ "widget.alltime.bestviews.label" = "Best views ever"; +/* Title of the label which displays the number of the most daily views the site has ever had. Keep the translation as short as possible. */ +"widget.alltime.bestviewsshort.label" = "Most Views"; + /* Title of the no data view in all time widget */ "widget.alltime.nodata.view.title" = "Unable to load all time stats."; @@ -10611,6 +11527,12 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Title of the unconfigured view in today widget */ "widget.jetpack.today.unconfigured.view.title" = "Log in to Jetpack to see today's stats."; +/* Title of the one-liner information consist of views field and all time date range in lock screen all time views widget */ +"widget.lockscreen.alltimeview.label" = "All-Time Views"; + +/* Title of the one-liner information consist of views field and today date range in lock screen today views widget */ +"widget.lockscreen.todayview.label" = "Views Today"; + /* Title of the no data view in this week widget */ "widget.thisweek.nodata.view.title" = "Unable to load this week's stats."; @@ -10662,6 +11584,15 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Title of visitors label in today widget */ "widget.today.visitors.label" = "Visitors"; +/* Preview title of today's likes and commnets widget */ +"widget.todayLikesComments.previewTitle" = "Today's Likes & Comments"; + +/* Preview title of today's views widget */ +"widget.todayViews.previewTitle" = "Today's Views"; + +/* Preview title of today's views and visitors widget */ +"widget.todayViewsVisitors.previewTitle" = "Today's Views & Visitors"; + /* Second part of delete screen title stating [the site] will be unavailable in the future. */ "will be unavailable in the future." = "will be unavailable in the future."; diff --git a/WordPress/Resources/en.lproj/Localizable.strings b/WordPress/Resources/en.lproj/Localizable.strings index 1ef4a79bdae1..0108161cb77e 100644 --- a/WordPress/Resources/en.lproj/Localizable.strings +++ b/WordPress/Resources/en.lproj/Localizable.strings @@ -2,9 +2,6 @@ /* MARK: - Localizable.strings */ -/* Per-year postfix shown after a domain's cost. */ -" / year" = " / year"; - /* Description for the restore action. $1$@ is a placeholder for the selected date. */ "%1$@ is the selected point for your restore." = "%1$@ is the selected point for your restore."; @@ -133,9 +130,6 @@ /* Difference label for Period Overview stat, indicating change from previous period. Ex: +99.9K (5%) */ "%@%@ (%@%%)" = "%1$@%2$@ (%3$@%%)"; -/* Accessibility label for a post in the post list. The parameters are the title, and date respectively. For example, \"Let it Go, 1 hour ago.\" */ -"%@, %@." = "%1$@, %2$@."; - /* Accessibility value for a Stats' Posting Activity Month if the user selected a day with posts. The first parameter is day (e.g. November 2019). The second parameter is the number of posts. */ "%@. %d posts." = "%1$@. %2$d posts."; @@ -191,9 +185,6 @@ /* Age between dates over one year. */ "%d years" = "%d years"; -/* Max image size in pixels (e.g. 300x300px) */ -"%dx%dpx" = "%1$dx%2$dpx"; - /* One menu area available in the theme */ "%i menu area in this theme" = "%i menu area in this theme"; @@ -215,9 +206,6 @@ /* Displays the number of words and characters in text */ "%li words, %li characters" = "%1$li words, %2$li characters"; -/* translators: %s: Block name e.g. \"Image block\"\ntranslators: Block name. %s: The localized block name */ -"%s block" = "%s block"; - /* translators: %s: block title e.g: \"Paragraph\". */ "%s block options" = "%s block options"; @@ -251,6 +239,9 @@ /* translators: %s: social link name e.g: \"Instagram\". */ "%s social icon" = "%s social icon"; +/* translators: displayed right after the classic block is converted to blocks. %s: The localized classic block name */ +"'%s' block converted to blocks" = "'%s' block converted to blocks"; + /* translators: Missing block alert title. %s: The localized block name */ "'%s' is not fully-supported" = "'%s' is not fully-supported"; @@ -500,19 +491,9 @@ /* Dismiss button title */ "activityList.dismiss.title" = "Dismiss"; -/* Accessibility label for add button to add items to the user's media library - Remove asset from media picker list */ -"Add" = "Add"; - -/* Action for Media Picker to indicate selection of media. The argument in the string represents the number of elements (as numeric digits) selected */ -"Add %@" = "Add %@"; - /* The title on the add category screen */ "Add a Category" = "Add a Category"; -/* Hint for the reader CSS URL field */ -"Add a custom CSS URL here to be loaded in Reader. If you're running Calypso locally this can be something like: http://192.168.15.23:3000/calypso/reader-mobile.css" = "Add a custom CSS URL here to be loaded in Reader. If you're running Calypso locally this can be something like: http://192.168.15.23:3000/calypso/reader-mobile.css"; - /* Label of the button that starts the purchase of an additional redirected domain in the Domains Dashboard. */ "Add a domain" = "Add a domain"; @@ -597,9 +578,6 @@ /* No comment provided by engineer. */ "Add menu item to children" = "Add menu item to children"; -/* Accessibility hint for add button to add items to the user's media library */ -"Add new media" = "Add new media"; - /* Screen reader text for Menus button that adds a new menu to a site. */ "Add new menu" = "Add new menu"; @@ -680,9 +658,6 @@ /* Option to select the Airmail app when logging in with magic links */ "Airmail" = "Airmail"; -/* Description of albums in the photo libraries */ -"Albums" = "Albums"; - /* Image alignment option title. Title of the screen for choosing an image's alignment. */ "Alignment" = "Alignment"; @@ -711,9 +686,8 @@ /* Message shown when all Quick Start tasks are complete. */ "All tasks complete!" = "All tasks complete!"; -/* Footer of the free domain registration section for a paid plan. - Information about redeeming domain credit on site dashboard. */ -"All WordPress.com plans include a custom domain name. Register your free premium domain now." = "All WordPress.com plans include a custom domain name. Register your free premium domain now."; +/* Information about redeeming domain credit on site dashboard. */ +"All WordPress.com annual plans include a custom domain name. Register your free domain now." = "All WordPress.com annual plans include a custom domain name. Register your free domain now."; /* Insights 'All-Time' header */ "All-Time" = "All-Time"; @@ -761,10 +735,13 @@ "Alt Text" = "Alt Text"; /* No comment provided by engineer. */ -"Alternatively, you can detach and edit these blocks separately by tapping “Detach patterns”." = "Alternatively, you can detach and edit these blocks separately by tapping “Detach patterns”."; +"Alternatively, you can convert the content to blocks." = "Alternatively, you can convert the content to blocks."; /* No comment provided by engineer. */ -"Alternatively, you can detach and edit this block separately by tapping “Detach pattern”." = "Alternatively, you can detach and edit this block separately by tapping “Detach pattern”."; +"Alternatively, you can detach and edit this block separately by tapping “Detach”." = "Alternatively, you can detach and edit this block separately by tapping “Detach”."; + +/* translators: Alternative option included in a warning related to having blocks deeply nested. */ +"Alternatively, you can flatten the content by ungrouping the block." = "Alternatively, you can flatten the content by ungrouping the block."; /* Instruction text to explain to help users type their password instead of using magic link login option. */ "Alternatively, you may enter the password for this account." = "Alternatively, you may enter the password for this account."; @@ -904,6 +881,39 @@ /* VoiceOver accessibility hint, informing the user the button can be used to approve a comment */ "Approves the Comment." = "Approves the Comment."; +/* Option to enable the optimization of images when uploading. */ +"appSettings.media.imageOptimizationRow" = "Optimize Images"; + +/* Indicates an image will use high quality when uploaded. */ +"appSettings.media.imageQuality.high" = "High"; + +/* Indicates an image will use low quality when uploaded. */ +"appSettings.media.imageQuality.low" = "Low"; + +/* Indicates an image will use maximum quality when uploaded. */ +"appSettings.media.imageQuality.maximum" = "Maximum"; + +/* Indicates an image will use medium quality when uploaded. */ +"appSettings.media.imageQuality.medium" = "Medium"; + +/* The quality of image used when uploading */ +"appSettings.media.imageQuality.title" = "Quality"; + +/* Title for the image quality settings option. */ +"appSettings.media.imageQualityRow" = "Image Quality"; + +/* Message of an alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.message" = "Image optimization shrinks images for faster uploading.\n\nThis option is enabled by default, but you can change it in the app settings at any time."; + +/* Title of an alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.title" = "Keep optimizing images?"; + +/* Title of button for turning off image optimization, displayed in the alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.turnOff" = "No, turn off"; + +/* Title of button for leaving on image optimization, displayed in the alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.turnOn" = "Yes, leave on"; + /* Menus alert message for alerting the user to unsaved changes while trying back out of Menus. */ "Are you sure you want to cancel and discard changes?" = "Are you sure you want to cancel and discard changes?"; @@ -922,15 +932,9 @@ /* Message prompting the user to confirm that they want to disconnect Jetpack from the site. */ "Are you sure you want to disconnect Jetpack from the site?" = "Are you sure you want to disconnect Jetpack from the site?"; -/* Message prompting the user to confirm that they want to permanently delete a group of media items. */ -"Are you sure you want to permanently delete these items?" = "Are you sure you want to permanently delete these items?"; - /* Message prompting the user to confirm that they want to permanently delete a media item. Should match Calypso. */ "Are you sure you want to permanently delete this item?" = "Are you sure you want to permanently delete this item?"; -/* Message of the confirmation alert when deleting a page from the trash. */ -"Are you sure you want to permanently delete this page?" = "Are you sure you want to permanently delete this page?"; - /* Message of the confirmation alert when deleting a post from the trash. */ "Are you sure you want to permanently delete this post?" = "Are you sure you want to permanently delete this post?"; @@ -962,9 +966,6 @@ /* Title of message shown when user taps submit for review. */ "Are you sure you want to submit for review?" = "Are you sure you want to submit for review?"; -/* Message of the trash page confirmation alert. */ -"Are you sure you want to trash this page?" = "Are you sure you want to trash this page?"; - /* Message of the trash confirmation alert. */ "Are you sure you want to trash this post?" = "Are you sure you want to trash this post?"; @@ -996,9 +997,6 @@ /* Alert option to embed a doc link into a blog post. */ "Attach File as Link" = "Attach File as Link"; -/* Label displayed on audio media items. */ -"audio" = "audio"; - /* translators: accessibility text. %s: Audio caption. */ "Audio caption. %s" = "Audio caption. %s"; @@ -1011,9 +1009,6 @@ /* No comment provided by engineer. */ "Audio Player" = "Audio Player"; -/* Accessibility label for audio items in the media collection view. The parameter is the creation date of the audio. */ -"Audio, %@" = "Audio, %@"; - /* No comment provided by engineer. */ "Authenticating" = "Authenticating"; @@ -1046,9 +1041,6 @@ /* The plugin can not be manually updated or deactivated */ "Auto-managed on this site" = "Auto-managed on this site"; -/* Label indicating that a domain name registration will automatically renew */ -"Auto-renew enabled" = "Auto-renew enabled"; - /* Discussion Settings Title Settings: Comments Approval settings */ "Automatically Approve" = "Automatically Approve"; @@ -1275,9 +1267,6 @@ /* translators: displayed right after the block is duplicated. */ "Block duplicated" = "Block duplicated"; -/* Popup title about why this post is being opened in block editor */ -"Block editor enabled" = "Block editor enabled"; - /* translators: displayed right after the block is grouped */ "Block grouped" = "Block grouped"; @@ -1323,10 +1312,7 @@ "Blocks menu" = "Blocks menu"; /* translators: Warning related to having blocks deeply nested. %d: The deepest nesting level. */ -"Blocks nested deeper than %d levels may not render properly in the mobile editor. For this reason, we recommend flattening the content by ungrouping the block or editing the block using the web editor." = "Blocks nested deeper than %d levels may not render properly in the mobile editor. For this reason, we recommend flattening the content by ungrouping the block or editing the block using the web editor."; - -/* translators: Warning related to having blocks deeply nested. %d: The deepest nesting level. */ -"Blocks nested deeper than %d levels may not render properly in the mobile editor. For this reason, we recommend flattening the content by ungrouping the block or editing the block using your web browser." = "Blocks nested deeper than %d levels may not render properly in the mobile editor. For this reason, we recommend flattening the content by ungrouping the block or editing the block using your web browser."; +"Blocks nested deeper than %d levels may not render properly in the mobile editor." = "Blocks nested deeper than %d levels may not render properly in the mobile editor."; /* Title of a button that displays the WordPress.com blog */ "Blog" = "Blog"; @@ -1346,6 +1332,43 @@ /* Blog's Viewer Profile. Displayed when the name is empty! */ "Blog's Viewer" = "Blog's Viewer"; +/* Title for a button that, when tapped, shows more info about participating in Bloganuary. */ +"bloganuary.dashboard.card.button.learnMore" = "Learn more"; + +/* Short description for the Bloganuary event, shown right below the title. */ +"bloganuary.dashboard.card.description" = "For the month of January, blogging prompts will come from Bloganuary — our community challenge to build a blogging habit for the new year."; + +/* Title for the Bloganuary dashboard card while Bloganuary is running. */ +"bloganuary.dashboard.card.runningTitle" = "Bloganuary is here!"; + +/* Title for the Bloganuary dashboard card. */ +"bloganuary.dashboard.card.title" = "Bloganuary is coming!"; + +/* Title of a button that calls the user to enable the Blogging Prompts feature. */ +"bloganuary.learnMore.modal.button.promptsDisabled" = "Turn on blogging prompts"; + +/* Title of a button that will dismiss the Bloganuary modal when tapped. +Note that the word 'go' here should have a closer meaning to 'start' rather than 'move forward'. */ +"bloganuary.learnMore.modal.button.promptsEnabled" = "Let’s go!"; + +/* The second line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.description.second" = "Publish your response."; + +/* The third line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.description.third" = "Read other bloggers’ responses to get inspiration and make new connections."; + +/* The first line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.descriptions.first" = "Receive a new prompt to inspire you each day."; + +/* An additional piece of information shown in case the user has the Blogging Prompts feature disabled. */ +"bloganuary.learnMore.modal.footer.addition" = "To join Bloganuary you need to enable Blogging Prompts."; + +/* An informative excerpt shown in a subtler tone. */ +"bloganuary.learnMore.modal.footer.text" = "Bloganuary will use Daily Blogging Prompts to send you topics for the month of January."; + +/* The headline text of the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.headline" = "Join our month-long writing challenge"; + /* Title for the context menu action that hides the dashboard card. */ "blogDashboard.contextMenu.hideThis" = "Hide this"; @@ -1365,7 +1388,7 @@ "blogHeader.actionCopyURL" = "Copy URL"; /* Context menu button title */ -"blogHeader.actionOpenInBrowser" = "Open in Browser"; +"blogHeader.actionVisitSite" = "Visit site"; /* Accessibility label for bold button on formatting toolbar. Discoverability title for bold formatting keyboard shortcut. */ @@ -1389,9 +1412,6 @@ /* Description of a Quick Start Tour */ "Bring media straight from your device or camera to your site." = "Bring media straight from your device or camera to your site."; -/* Description of a Quick Start Tour */ -"Browse all our themes to find your perfect fit." = "Browse all our themes to find your perfect fit."; - /* Jetpack Settings: Brute Force Attack Protection Section */ "Brute Force Attack Protection" = "Brute Force Attack Protection"; @@ -1422,9 +1442,6 @@ /* Used when displaying author of a plugin. */ "by %@" = "by %@"; -/* Accessibility label for the post author in the post list. The parameter is the author name. For example, \"By Elsa.\" */ -"By %@." = "By %@."; - /* Legal disclaimer for logging in. The underscores _..._ denote underline. */ "By continuing, you agree to our _Terms of Service_." = "By continuing, you agree to our _Terms of Service_."; @@ -1440,8 +1457,7 @@ /* Label for size of media while it's being calculated. */ "Calculating..." = "Calculating..."; -/* Accessibility label for taking an image or video with the camera on formatting toolbar. - Accessibility label for the camera tile in the collection view */ +/* Accessibility label for taking an image or video with the camera on formatting toolbar. */ "Camera" = "Camera"; /* Title of an alert informing the user the camera permission for the app is disabled and its needed to proceed */ @@ -1492,10 +1508,7 @@ Cancels an Action Cancels an alert. Cancels the mark all as read action. - Dismiss the post action sheet Editing GIF alert cancel action button. - Label for a cancel button - Label for the auto-upload cancelation button in the post list. Tapping will prevent the app from auto-uploading the post. Menus cancel button for deleting a menu. Menus cancel button within text bar while editing items. Menus: Cancel button title for canceling an edited menu item. @@ -1526,10 +1539,6 @@ /* Share extension dialog dismiss button label - displayed when user is missing a login token. */ "Cancel sharing" = "Cancel sharing"; -/* Label for the Post List option that cancels automatic uploading of a post. - Media Library option to cancel an in-progress or failed upload. */ -"Cancel Upload" = "Cancel Upload"; - /* The action was canceled */ "Canceled" = "Canceled"; @@ -1580,9 +1589,6 @@ Main title */ "Change Password" = "Change Password"; -/* Title of button that takes user to the system Settings section for the app */ -"Change Settings" = "Change Settings"; - /* Accessibility hint for web preview device switching button */ "Change the device type used for preview" = "Change the device type used for preview"; @@ -1666,6 +1672,9 @@ /* Title of button that asks the users if they'd like to focus on checking their sites stats */ "Checking stats" = "Checking stats"; +/* Title for the checkout view */ +"checkout.title" = "Checkout"; + /* Screen reader text expressing the menu item is a child of another menu item. Argument is a name for another menu item. */ "Child of %@" = "Child of %@"; @@ -1691,8 +1700,7 @@ /* A text for title label on Login epilogue screen */ "Choose a site to open." = "Choose a site to open."; -/* Title for the screen to pick a theme and homepage for a site. - Title of a Quick Start Tour */ +/* Title for the screen to pick a theme and homepage for a site. */ "Choose a theme" = "Choose a theme"; /* Select the site's intent. Subtitle */ @@ -1716,9 +1724,6 @@ /* No comment provided by engineer. */ "Choose from device" = "Choose from device"; -/* Menu option for selecting media from the device's photo library. */ -"Choose from My Device" = "Choose from My Device"; - /* Title for selecting a new homepage */ "Choose Homepage" = "Choose Homepage"; @@ -1793,7 +1798,6 @@ Action button to close edior and cancel changes or insertion of post Action button to close the editor Dismiss the current view - Dismiss the media picker for Stock Photos Dismisses the current screen Voiceover accessibility label informing the user that this button dismiss the current view */ "Close" = "Close"; @@ -1932,15 +1936,18 @@ Example: Reply to Pamela Nguyen */ /* A label title. */ "Comments per page" = "Comments per page"; +/* Error message informing a user about an invalid password. */ +"common.reEnterPasswordMessage" = "The username or password stored in the app may be out of date. Please re-enter your password in the settings and try again."; + +/* An error message. */ +"common.unableToConnect" = "Unable to Connect"; + /* Setting: WordPress.com Community */ "Community" = "Community"; /* Community & Non-Profit site intent topic */ "Community & Non-Profit" = "Community & Non-Profit"; -/* Accessibility indication that the current Post List style is currently Compact. */ -"Compact" = "Compact"; - /* The action is completed */ "Completed" = "Completed"; @@ -1953,24 +1960,15 @@ Example: Reply to Pamela Nguyen */ /* The Quick Start Tour title after the user finished the step. */ "Completed: Check your site title" = "Completed: Check your site title"; -/* The Quick Start Tour title after the user finished the step. */ -"Completed: Choose a theme" = "Completed: Choose a theme"; - /* The Quick Start Tour title after the user finished the step. */ "Completed: Choose a unique site icon" = "Completed: Choose a unique site icon"; /* The Quick Start Tour title after the user finished the step. */ "Completed: Connect with other sites" = "Completed: Connect with other sites"; -/* The Quick Start Tour title after the user finished the step. */ -"Completed: Continue with site setup" = "Completed: Continue with site setup"; - /* The Quick Start Tour title after the user finished the step. */ "Completed: Create your site" = "Completed: Create your site"; -/* The Quick Start Tour title after the user finished the step. */ -"Completed: Explore plans" = "Completed: Explore plans"; - /* The Quick Start Tour title after the user finished the step. */ "Completed: Publish a post" = "Completed: Publish a post"; @@ -2123,9 +2121,6 @@ Example: Reply to Pamela Nguyen */ /* Button title. Tapping begins log in using Google. */ "Continue with Google" = "Continue with Google"; -/* Title of a Quick Start Tour */ -"Continue with site setup" = "Continue with site setup"; - /* Button title. Takes the user to the login with WordPress.com flow. */ "Continue With WordPress.com" = "Continue With WordPress.com"; @@ -2150,10 +2145,6 @@ Example: Reply to Pamela Nguyen */ /* No comment provided by engineer. */ "Copy file URL" = "Copy file URL"; -/* Copy the post url and paste anywhere in phone - Label for page copy link. Tapping copy the url of page */ -"Copy Link" = "Copy Link"; - /* Copy link to oomment button title */ "Copy Link to Comment" = "Copy Link to Comment"; @@ -2230,13 +2221,13 @@ Example: Reply to Pamela Nguyen */ "Couldn't connect to the WordPress site. There is no valid WordPress site at this address. Check the site address (URL) you entered." = "Couldn't connect to the WordPress site. There is no valid WordPress site at this address. Check the site address (URL) you entered."; /* Message to show to user when he tries to add a self-hosted site with RSD link present, but xmlrpc is missing. */ -"Couldn't connect. Required XML-RPC methods are missing on the server." = "Couldn't connect. Required XML-RPC methods are missing on the server."; +"Couldn't connect. Required XML-RPC methods are missing on the server. Please contact your hosting provider to solve this problem." = "Couldn't connect. Required XML-RPC methods are missing on the server. Please contact your hosting provider to solve this problem."; /* Message to show to user when he tries to add a self-hosted site but the host returned a 403 error, meaning that the access to the /xmlrpc.php file is forbidden. */ -"Couldn't connect. We received a 403 error when trying to access your site's XMLRPC endpoint. The app needs that in order to communicate with your site. Contact your host to solve this problem." = "Couldn't connect. We received a 403 error when trying to access your site's XMLRPC endpoint. The app needs that in order to communicate with your site. Contact your host to solve this problem."; +"Couldn't connect. We received a 403 error when trying to access your site's XMLRPC endpoint. The app needs that in order to communicate with your site. Please contact your hosting provider to solve this problem." = "Couldn't connect. We received a 403 error when trying to access your site's XMLRPC endpoint. The app needs that in order to communicate with your site. Please contact your hosting provider to solve this problem."; /* Message to show to user when he tries to add a self-hosted site but the host returned a 405 error, meaning that the host is blocking POST requests on /xmlrpc.php file. */ -"Couldn't connect. Your host is blocking POST requests, and the app needs that in order to communicate with your site. Contact your host to solve this problem." = "Couldn't connect. Your host is blocking POST requests, and the app needs that in order to communicate with your site. Contact your host to solve this problem."; +"Couldn't connect. Your host is blocking POST requests, and the app needs that in order to communicate with your site. Please contact your hosting provider to solve this problem." = "Couldn't connect. Your host is blocking POST requests, and the app needs that in order to communicate with your site. Please contact your hosting provider to solve this problem."; /* Error message when tag loading failed */ "Couldn't load tags." = "Couldn't load tags."; @@ -2259,9 +2250,6 @@ Example: Reply to Pamela Nguyen */ /* Error title displayed when unable to close user account. */ "Couldn’t close account automatically" = "Couldn’t close account automatically"; -/* Message to show while media data source is finding the number of items available. */ -"Counting media items..." = "Counting media items..."; - /* Period Stats 'Countries' header */ "Countries" = "Countries"; @@ -2272,9 +2260,6 @@ Example: Reply to Pamela Nguyen */ /* Register Domain - Address information field Country Code */ "Country Code" = "Country Code"; -/* Title of a section on the debug screen that shows a list of actions related to crash logging */ -"Crash Logging" = "Crash Logging"; - /* Label for switch to turn on/off sending crashes info */ "Crash reports" = "Crash reports"; @@ -2327,9 +2312,6 @@ Example: Reply to Pamela Nguyen */ /* Create New header text */ "Create New" = "Create New"; -/* Title for the site creation flow. */ -"Create New Site" = "Create New Site"; - /* Button for selecting the current page template. Button title, encourages users to create their first page on their blog. Title for button to make a page with the contents of the selected layout */ @@ -2584,30 +2566,58 @@ Example: Reply to Pamela Nguyen */ /* Navigates to debug menu only available in development builds */ "Debug" = "Debug"; -/* Debug settings title */ -"Debug Settings" = "Debug Settings"; +/* Debug menu item title */ +"debugMenu.analytics" = "Analytics"; /* Feature flags menu item */ "debugMenu.featureFlags" = "Feature Flags"; -/* General section title */ -"debugMenu.generalSectionTitle" = "General"; +/* Title of the screen that allows the user to change the Reader CSS URL for debug builds */ +"debugMenu.readerCellTitle" = "Reader CSS URL"; + +/* Placeholder for the reader CSS URL */ +"debugMenu.readerDefaultURL" = "Default URL"; + +/* Hint for the reader CSS URL field */ +"debugMenu.readerHit" = "Add a custom CSS URL here to be loaded in Reader. If you're running Calypso locally this can be something like: http://192.168.15.23:3000/calypso/reader-mobile.css"; + +/* Remote Config Debug Menu section title */ +"debugMenu.remoteConfig.currentValue" = "Current Value"; + +/* Remote Config Debug Menu section title */ +"debugMenu.remoteConfig.defaultValue" = "Default Value"; -/* Remote config params debug menu footer explaining the meaning of a cell with a checkmark. */ -"debugMenu.remoteConfig.footer" = "Overridden parameters are denoted by a checkmark."; +/* Remote Config Debug Menu section title */ +"debugMenu.remoteConfig.overridenValue" = "Remote Config"; -/* Hint for overriding remote config params */ -"debugMenu.remoteConfig.hint" = "Override the chosen param by defining a new value here."; +/* Remote Config Debug Menu section title */ +"debugMenu.remoteConfig.remoteConfigValue" = "Remote Config Value"; -/* Placeholder for overriding remote config params */ -"debugMenu.remoteConfig.placeholder" = "No remote or default value"; +/* Remote Config Debug Menu reset button title */ +"debugMenu.remoteConfig.reset" = "Reset"; -/* Remote Config debug menu title */ +/* Remote Config Debug Menu screen title + Remote Config debug menu title */ "debugMenu.remoteConfig.title" = "Remote Config"; /* Remove current quick start tour menu item */ "debugMenu.removeQuickStart" = "Remove Current Tour"; +/* Debug Menu section title */ +"debugMenu.section.logging" = "Logging"; + +/* Debug Menu section title */ +"debugMenu.section.quickStart" = "Quick Start"; + +/* Debug Menu section title */ +"debugMenu.section.settings" = "Settings"; + +/* Title for debug menu screen */ +"debugMenu.title" = "Developer"; + +/* Weekly Roundup debug menu item */ +"debugMenu.weeklyRoundup" = "Weekly Roundup"; + /* Only December needs to be translated */ "December 17, 2017" = "December 17, 2017"; @@ -2629,16 +2639,12 @@ Example: Reply to Pamela Nguyen */ Title for screen to select a default post format for a blog */ "Default Post Format" = "Default Post Format"; -/* Placeholder for the reader CSS URL */ -"Default URL" = "Default URL"; - /* Discussion Settings: Posts Section */ "Defaults for New Posts" = "Defaults for New Posts"; /* Delete Delete button title for the warning shown to the user when he refuses to re-login when the authToken is missing. Title for button that permanently deletes a media item (photo / video) - Title for button that permanently deletes one or more media items (photos / videos) Title of a delete button Title of the trash confirmation alert. */ "Delete" = "Delete"; @@ -2649,15 +2655,11 @@ Example: Reply to Pamela Nguyen */ /* Menus confirmation button for deleting a menu. */ "Delete Menu" = "Delete Menu"; -/* Delete option in the confirmation alert when deleting a page from the trash. - Delete option in the confirmation alert when deleting a post from the trash. - Label for a button permanently deletes a page. - Label for the delete post option. Tapping permanently deletes a post. +/* Delete option in the confirmation alert when deleting a post from the trash. Title for button on the comment details page that deletes the comment when tapped. */ "Delete Permanently" = "Delete Permanently"; -/* Title of the confirmation alert when deleting a page from the trash. - Title of the confirmation alert when deleting a post from the trash. */ +/* Title of the confirmation alert when deleting a post from the trash. */ "Delete Permanently?" = "Delete Permanently?"; /* Button label for deleting the current site @@ -2773,7 +2775,6 @@ Example: Reply to Pamela Nguyen */ /* Accessibility label for button to dismiss a bottom sheet Accessibility label for the transparent space above the login dialog which acts as a button to dismiss the dialog. Accessibility label for the transparent space above the signup dialog which acts as a button to dismiss the dialog. - Action to show on alert when view asset fails. Dismiss a view. Verb */ "Dismiss" = "Dismiss"; @@ -2794,12 +2795,6 @@ Example: Reply to Pamela Nguyen */ /* DIY site intent topic */ "DIY" = "DIY"; -/* Accessibility label for other media items in the media collection view. The parameter is the creation date of the document. */ -"Document, %@" = "Document, %@"; - -/* Accessibility label for other media items in the media collection view. The parameter is the filename file. */ -"Document: %@" = "Document: %@"; - /* Message shown when all tours have been completed */ "Doesn't it feel good to cross things off a list?" = "Doesn't it feel good to cross things off a list?"; @@ -2812,50 +2807,95 @@ Example: Reply to Pamela Nguyen */ /* Title for a menu action in the context menu on the Jetpack install card. */ "domain.dashboard.card.menu.hide" = "Hide this"; -/* Domain Purchase Completion footer */ -"domain.purchase.preview.footer" = "It may take up to 30 minutes for your custom domain to start working."; +/* Search domain - Title for the Suggested domains screen */ +"domain.management.addDomain.search.title" = "Search for a domain"; -/* Domain Purchase Completion description (only for FREE domains). */ -"domain.purchase.preview.free.description" = "Next, we'll help you get it ready to be browsed."; +/* Domain management buy domain card button title */ +"domain.management.buy.card.button.title" = "Get Domain"; -/* Domain Purchase Completion description (only for PAID domains). */ -"domain.purchase.preview.paid.description" = "We’ve emailed your receipt. Next, we'll help you get it ready for everyone."; +/* Domain management buy domain card subtitle */ +"domain.management.buy.card.subtitle" = "Add a site later."; -/* Reflects that site is live when domain purchase feature flag is ON. */ -"domain.purchase.preview.title" = "Kudos, your site is live!"; +/* Domain management buy domain card title */ +"domain.management.buy.card.title" = "Just buy a domain"; + +/* The expired label of the domain card in All Domains screen. */ +"domain.management.card.expired.label" = "Expired"; + +/* Label indicating that a domain name registration has no expiry date. */ +"domain.management.card.neverExpires.label" = "Never expires"; + +/* The renews label of the domain card in All Domains screen. */ +"domain.management.card.renews.label" = "Renews"; + +/* The empty state button title in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.button.title" = "Find a domain"; + +/* The empty state description in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.description" = "Tap below to find your perfect domain."; + +/* The empty state title in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.title" = "You don't have any domains"; + +/* The empty state description in All Domains screen when an error occurs */ +"domain.management.error.empty.state.description" = "We encountered an error while loading your domains. Please contact support if the issue persists."; -/* Status of a domain in `Action Required` state */ -"domain.status.action.required" = "Action Required"; +/* The empty state title in All Domains screen when an error occurs */ +"domain.management.error.empty.state.title" = "Something went wrong"; -/* Status of a domain in `Active` state */ -"domain.status.active" = "Active"; +/* The empty state button title in All Domains screen when an error occurs */ +"domain.management.error.state.button.title" = "Try again"; -/* Status of a domain in `Complete Setup` state */ -"domain.status.complete.setup" = "Complete Setup"; +/* The empty state description in All Domains screen when the user is offline */ +"domain.management.offline.empty.state.description" = "Please check your network connection and try again."; -/* Status of a domain in `Error` state */ -"domain.status.error" = "Error"; +/* The empty state title in All Domains screen when the user is offline */ +"domain.management.offline.empty.state.title" = "No Internet Connection"; -/* Status of a domain in `Expired` state */ -"domain.status.expired" = "Expired"; +/* Domain management choose site card button title */ +"domain.management.purchase.footer" = "*A free domain for one year is included with all paid annual plans"; -/* Status of a domain in `Expiring Soon` state */ -"domain.status.expiring.soon" = "Expiring Soon"; +/* Domain management purchase domain screen title */ +"domain.management.purchase.subtitle" = "Don't worry, you can easily add a site later."; -/* Status of a domain in `Failed` state */ -"domain.status.failed" = "Failed"; +/* Domain management purchase domain screen title. */ +"domain.management.purchase.title" = "Choose how to use your domain"; -/* Status of a domain in `In Progress` state */ -"domain.status.in.progress" = "In Progress"; +/* The search bar title in All Domains screen. */ +"domain.management.search-bar.title" = "Search domains"; -/* Status of a domain in `Renew` state */ -"domain.status.renew" = "Renew"; +/* The empty state description in All Domains screen when the are no domains matching the search criteria */ +"domain.management.search.empty.state.description" = "We couldn't find any domains that match your search for '%@'"; -/* Status of a domain in `Verify Email` state */ -"domain.status.verify.email" = "Verify Email"; +/* The empty state title in All Domains screen when the are no domains matching the search criteria */ +"domain.management.search.empty.state.title" = "No Matching Domains Found"; -/* Status of a domain in `Verifying` state */ -"domain.status.verifying" = "Verifying"; +/* Domain management choose site card button title */ +"domain.management.site.card.button.title" = "Choose Site"; + +/* Domain management choose site card subtitle */ +"domain.management.site.card.footer" = "Free domain for the first year*"; + +/* Domain management choose site card subtitle */ +"domain.management.site.card.subtitle" = "Use with a site you already started."; + +/* Domain management choose site card title */ +"domain.management.site.card.title" = "Existing WordPress.com site"; + +/* Domain Management Screen Title */ +"domain.management.title" = "All Domains"; + +/* Domain Purchase Completion footer */ +"domain.purchase.preview.footer" = "It may take up to 30 minutes for your custom domain to start working."; + +/* Domain Purchase Completion description (only for FREE domains). */ +"domain.purchase.preview.free.description" = "Next, we'll help you get it ready to be browsed."; + +/* Domain Purchase Completion description (only for PAID domains). */ +"domain.purchase.preview.paid.description" = "We’ve emailed your receipt. Next, we'll help you get it ready for everyone."; + +/* Reflects that site is live when domain purchase feature flag is ON. */ +"domain.purchase.preview.title" = "Kudos, your site is live!"; /* The 'Best Alternative' label under the domain name in 'Choose a domain' screen */ "domain.suggestions.row.best-alternative" = "Best Alternative"; @@ -2881,11 +2921,8 @@ Example: Reply to Pamela Nguyen */ /* Noun. Title. Links to the Domains screen. */ "Domains" = "Domains"; -/* Description for the first domain purchased with a free plan. */ -"Domains purchased on this site will redirect to %@" = "Domains purchased on this site will redirect to %@"; - -/* Description for the first domain purchased with a free plan. */ -"Domains purchased on this site will redirect users to " = "Domains purchased on this site will redirect users to "; +/* Title for the checkout screen. */ +"domains.checkout.title" = "Checkout"; /* Action shown in a bottom notice to dismiss it. */ "domains.failure.dismiss" = "Dismiss"; @@ -2893,6 +2930,24 @@ Example: Reply to Pamela Nguyen */ /* Content show when the domain selection action fails. */ "domains.failure.title" = "Sorry, the domain you are trying to add cannot be bought on the Jetpack app at this time."; +/* Title for the screen where the user can choose how to use the domain they're end up purchasing. */ +"domains.purchase.choice.title" = "Purchase Domain"; + +/* Back button title that navigates back to the search domains screen. */ +"domains.search.backButton.title" = "Search"; + +/* Title of screen where user chooses a site to connect to their selected domain */ +"domains.sitePicker.title" = "Choose Site"; + +/* Help button */ +"domainSelection.helpButton.title" = "Help"; + +/* Description for the first domain purchased with a free plan. */ +"domainSelection.redirectPrompt.title" = "Domains purchased on this site will redirect to %1$@"; + +/* Search domain - Title for the Suggested domains screen */ +"domainSelection.search.title" = "Search domains"; + /* Label for button to log in using your site address. The underscores _..._ denote underline */ "Don't have an account? _Sign up_" = "Don't have an account? _Sign up_"; @@ -3038,8 +3093,7 @@ Example: Reply to Pamela Nguyen */ /* Description of a Quick Start Tour */ "Draft and publish a post." = "Draft and publish a post."; -/* Title of the drafts filter. This filter shows a list of draft posts. - Title of the drafts header in search list. */ +/* Title of the drafts filter. This filter shows a list of draft posts. */ "Drafts" = "Drafts"; /* No comment provided by engineer. */ @@ -3051,10 +3105,6 @@ Example: Reply to Pamela Nguyen */ /* No comment provided by engineer. */ "Drag to adjust focal point" = "Drag to adjust focal point"; -/* Label for page duplicate option. Tapping creates a copy of the page. - Label for post duplicate option. Tapping creates a copy of the post. */ -"Duplicate" = "Duplicate"; - /* No comment provided by engineer. */ "Duplicate block" = "Duplicate block"; @@ -3067,20 +3117,12 @@ Example: Reply to Pamela Nguyen */ /* No comment provided by engineer. */ "Each block has its own settings. To find them, tap on a block. Its settings will appear on the toolbar at the bottom of the screen." = "Each block has its own settings. To find them, tap on a block. Its settings will appear on the toolbar at the bottom of the screen."; -/* Accessibility label for edit button to enable multi selection mode in the user's media library - Edit the post. - Editing GIF alert default action button. +/* Editing GIF alert default action button. Edits a Comment Edits the comment - Label for a button that opens the Edit Page view controller - Label for the edit post button. Tapping displays the editor. - User action to edit media details. - Verb, edit a comment */ + User action to edit media details. */ "Edit" = "Edit"; -/* Button that displays the media editor to the user */ -"Edit %@" = "Edit %@"; - /* Title for the edit more button section */ "Edit \"More\" button" = "Edit \"More\" button"; @@ -3143,9 +3185,6 @@ Example: Reply to Pamela Nguyen */ /* Title for the editor settings section */ "Editor" = "Editor"; -/* Edit Action Spoken hint. */ -"Edits a comment" = "Edits a comment"; - /* VoiceOver accessibility hint, informing the user the button can be used to Edit the Comment. */ "Edits the comment." = "Edits the comment."; @@ -3279,12 +3318,6 @@ Example: Reply to Pamela Nguyen */ /* Message explaining why the user might enter a password. */ "Enter a password to protect this post" = "Enter a password to protect this post"; -/* Secondary message shown when there are no domains that match the user entered text. */ -"Enter different words above and we'll look for an address that matches it." = "Enter different words above and we'll look for an address that matches it."; - -/* Accessibility hint for edit button to enable multi selection mode in the user's media library */ -"Enter edit mode to enable multi select to delete" = "Enter edit mode to enable multi select to delete"; - /* Accessibility Label for the enter full screen button on the comment reply text view */ "Enter Full Screen" = "Enter Full Screen"; @@ -3443,14 +3476,10 @@ Example: Reply to Pamela Nguyen */ /* Short title telling the user they will receive a blogging reminder every day of the week. */ "Every day at %@" = "Every day at %@"; -/* Label for the post author filter. This filter shows posts for all users on the blog. */ -"Everyone" = "Everyone"; - /* Example story title description */ "Example story title" = "Example story title"; /* Placeholder for the site url textfield. - Provides a sample of what a domain name looks like. Site Address placeholder */ "example.com" = "example.com"; @@ -3460,9 +3489,6 @@ Example: Reply to Pamela Nguyen */ /* No comment provided by engineer. */ "Excerpt length (words)" = "Excerpt length (words)"; -/* Accessibility label for a post's excerpt in the post list. The parameter is the post excerpt. For example, \"Excerpt. This is the first paragraph.\" */ -"Excerpt. %@." = "Excerpt. %@."; - /* Should be the same as the text displayed if the user clicks the (i) in Calypso. */ "Excerpts are optional hand-crafted summaries of your content." = "Excerpts are optional hand-crafted summaries of your content."; @@ -3472,8 +3498,7 @@ Example: Reply to Pamela Nguyen */ /* Accessibility Label for the exit full screen button on the full screen comment reply mode */ "Exit Full Screen" = "Exit Full Screen"; -/* Accessibility indication that the current Post List style is currently Expanded. - Screen reader text to represent the expanded state of a UI control */ +/* Screen reader text to represent the expanded state of a UI control */ "Expanded" = "Expanded"; /* Accessibility hint */ @@ -3485,24 +3510,15 @@ Example: Reply to Pamela Nguyen */ /* Screen reader hint (non-imperative) about what does the site menu area selector button do. */ "Expands to select a different menu area" = "Expands to select a different menu area"; -/* Label indicating that a domain name registration has expired. */ -"Expired" = "Expired"; - /* Title for the error view when the user scanned an expired log in code */ "Expired log in code" = "Expired log in code"; /* Title. Indicates an expiration date. */ "Expires on" = "Expires on"; -/* Label indicating the date on which a domain name registration will expire. The %@ placeholder will be replaced with a date at runtime. */ -"Expires on %@" = "Expires on %@"; - /* Placeholder text for the tagline of a site */ "Explain what this site is about." = "Explain what this site is about."; -/* Title of a Quick Start Tour */ -"Explore plans" = "Explore plans"; - /* Export Content confirmation action title Label for selecting the Export Content Settings item */ "Export Content" = "Export Content"; @@ -3519,6 +3535,15 @@ Example: Reply to Pamela Nguyen */ /* Section title for the external table section in the blog details screen */ "External" = "External"; +/* Title for confirmation navigation bar button item */ +"externalMediaPicker.add" = "Add"; + +/* Bottom toolbar title in the selection mode */ +"externalMediaPicker.toolbarSelectItemsPrompt" = "Select Images"; + +/* Bottom toolbar title in the selection mode */ +"externalMediaPicker.toolbarViewSelected" = "View Selected (%@)"; + /* Status for Media object that is failed upload or export. The action failed */ "Failed" = "Failed"; @@ -3526,9 +3551,6 @@ Example: Reply to Pamela Nguyen */ /* Message for mark all as read success notice */ "Failed marking Notifications as read" = "Failed marking Notifications as read"; -/* Error title when picked media cannot be imported into stories. */ -"Failed Media Export" = "Failed Media Export"; - /* No comment provided by engineer. */ "Failed to insert audio file. Please tap for options." = "Failed to insert audio file. Please tap for options."; @@ -3610,6 +3632,9 @@ Example: Reply to Pamela Nguyen */ /* Label for the file type (.JPG, .PNG, etc) for a media asset (image / video) */ "File type" = "File type"; +/* No comment provided by engineer. */ +"File type not supported as a media file." = "File type not supported as a media file."; + /* Film & Television site intent topic */ "Film & Television" = "Film & Television"; @@ -3700,8 +3725,7 @@ Example: Reply to Pamela Nguyen */ Label for number of followers. */ "Followers" = "Followers"; -/* Accessibility label for following buttons. - Title of the Following Reader tab +/* Title of the Following Reader tab User is following the blog. Verb. Button title. The user is following a blog. */ "Following" = "Following"; @@ -3718,9 +3742,6 @@ Example: Reply to Pamela Nguyen */ /* Filters Follows Notifications */ "Follows" = "Follows"; -/* Spoken hint describing action for unselected following buttons. */ -"Follows blog" = "Follows blog"; - /* VoiceOver accessibility hint, informing the user the button can be used to follow a blog. */ "Follows the blog." = "Follows the blog."; @@ -3730,12 +3751,21 @@ Example: Reply to Pamela Nguyen */ /* No comment provided by engineer. */ "Font Size" = "Font Size"; +/* translators: %1$s: Font size name e.g. Small */ +"Font Size, %1$s" = "Font Size, %1$s"; + /* Food site intent topic */ "Food" = "Food"; /* An example tag used in the login prologue screens. */ "Football" = "Football"; +/* translators: Recommendation included in a warning related to having blocks deeply nested. */ +"For this reason, we recommend editing the block using the web editor." = "For this reason, we recommend editing the block using the web editor."; + +/* translators: Recommendation included in a warning related to having blocks deeply nested. */ +"For this reason, we recommend editing the block using your web browser." = "For this reason, we recommend editing the block using your web browser."; + /* Register Domain - Domain contact information section header description */ "For your convenience, we have pre-filled your WordPress.com contact information. Please review to be sure it’s the correct information you want to use for this domain." = "For your convenience, we have pre-filled your WordPress.com contact information. Please review to be sure it’s the correct information you want to use for this domain."; @@ -3748,9 +3778,6 @@ Example: Reply to Pamela Nguyen */ /* Browse free themes selection title */ "Free" = "Free"; -/* Label shown for domains that will be free for the first year due to the user having a premium plan with available domain credit. */ -"Free for the first year " = "Free for the first year "; - /* One of the options when selecting More in the Post Editor's format bar */ "Free GIF Library" = "Free GIF Library"; @@ -3869,9 +3896,6 @@ Example: Reply to Pamela Nguyen */ /* Name of the Quick Start list that guides users through a few tasks to explore the WordPress app. */ "Get to know the WordPress app" = "Get to know the WordPress app"; -/* Title of the card that starts the purchase of the first redirected domain in the Domains Dashboard. */ -"Get your domain" = "Get your domain"; - /* Title of the second alert preparing users to grant permission for us to send them push notifications. */ "Get your notifications faster" = "Get your notifications faster"; @@ -3899,9 +3923,6 @@ Example: Reply to Pamela Nguyen */ /* Option to select the Gmail app when logging in with magic links */ "Gmail" = "Gmail"; -/* No comment provided by engineer. */ -"Go back" = "Go back"; - /* Button title. Tapping lets the user view the sites they follow. */ "Go to Following" = "Go to Following"; @@ -3997,18 +4018,12 @@ Example: Reply to Pamela Nguyen */ /* This value is used to set the accessibility hint text for viewing the user's notifications. */ "Guides you through the process of checking your notifications." = "Guides you through the process of checking your notifications."; -/* This value is used to set the accessibility hint text for choosing a theme for the user's site. */ -"Guides you through the process of choosing a theme for your site." = "Guides you through the process of choosing a theme for your site."; - /* This value is used to set the accessibility hint text for creating a new page for the user's site. */ "Guides you through the process of creating a new page for your site." = "Guides you through the process of creating a new page for your site."; /* This value is used to set the accessibility hint text for creating the user's site. */ "Guides you through the process of creating your site." = "Guides you through the process of creating your site."; -/* This value is used to set the accessibility hint text for exploring plans on the user's site. */ -"Guides you through the process of exploring plans for your site." = "Guides you through the process of exploring plans for your site."; - /* This value is used to set the accessibility hint text for following the sites of other users. */ "Guides you through the process of following other sites." = "Guides you through the process of following other sites."; @@ -4024,9 +4039,6 @@ Example: Reply to Pamela Nguyen */ /* This value is used to set the accessibility hint text for setting the site title. */ "Guides you through the process of setting a title for your site." = "Guides you through the process of setting a title for your site."; -/* This value is used to set the accessibility hint text for setting up the user's site. */ -"Guides you through the process of setting up your site." = "Guides you through the process of setting up your site."; - /* This value is used to set the accessibility hint text for uploading a site icon. */ "Guides you through the process of uploading an icon for your site." = "Guides you through the process of uploading an icon for your site."; @@ -4133,8 +4145,7 @@ Example: Reply to Pamela Nguyen */ "Home" = "Home"; /* Title for setting which shows the current page assigned as a site's homepage - Title for the homepage section in site settings screen - Title of the Homepage Badge */ + Title for the homepage section in site settings screen */ "Homepage" = "Homepage"; /* Label for Homepage Settings site settings section @@ -4184,9 +4195,6 @@ Example: Reply to Pamela Nguyen */ /* Message to show when site icon update failed */ "Icon update failed" = "Icon update failed"; -/* Message explaining that they will need to install Jetpack on one of their sites. */ -"If you already have a site, you’ll need to install the free Jetpack plugin and connect it to your WordPress.com account." = "If you already have a site, you’ll need to install the free Jetpack plugin and connect it to your WordPress.com account."; - /* The instructions text about not being able to find the magic link email. */ "If you can’t find the email, please check your junk or spam email folder" = "If you can’t find the email, please check your junk or spam email folder"; @@ -4214,9 +4222,6 @@ Example: Reply to Pamela Nguyen */ /* Displays the ignored threats */ "Ignored" = "Ignored"; -/* Label displayed on image media items. */ -"image" = "image"; - /* Hint for image alt on image settings. */ "Image Alt" = "Image Alt"; @@ -4241,9 +4246,6 @@ Example: Reply to Pamela Nguyen */ /* Hint for image title on image settings. */ "Image title" = "Image title"; -/* Accessibility label for image thumbnails in the media collection view. The parameter is the creation date of the image. */ -"Image, %@" = "Image, %@"; - /* Undated post time label */ "Immediately" = "Immediately"; @@ -4864,9 +4866,6 @@ Please install the %3$@ to use the app with this site."; /* Body text of the first alert preparing users to grant permission for us to send them push notifications. */ "Learn about new comments, likes, and follows in seconds." = "Learn about new comments, likes, and follows in seconds."; -/* Description of a Quick Start Tour */ -"Learn about the marketing and SEO tools in our paid plans." = "Learn about the marketing and SEO tools in our paid plans."; - /* A button title. Link to cookie policy Menu title to show the prompts feature introduction modal. @@ -4997,9 +4996,6 @@ Please install the %3$@ to use the app with this site."; Settings: Comments Approval settings */ "Links in comments" = "Links in comments"; -/* The accessibility label for the list style button in the Post List. */ -"List style" = "List style"; - /* Title of the screen that load selected the revisions. */ "Load" = "Load"; @@ -5018,12 +5014,6 @@ Please install the %3$@ to use the app with this site."; /* Displayed while a comment is being loaded. */ "Loading comment..." = "Loading comment..."; -/* Shown while the app waits for the domain suggestions web service to return during the site creation process. */ -"Loading domains" = "Loading domains"; - -/* Phrase to show when the user has searched for GIFs and they are being loaded. */ -"Loading GIFs..." = "Loading GIFs..."; - /* Displayed while a call is loading the history. */ "Loading history..." = "Loading history..."; @@ -5036,9 +5026,6 @@ Please install the %3$@ to use the app with this site."; /* Text displayed while loading site People. */ "Loading People..." = "Loading People..."; -/* Phrase to show when the user has searched for images and they are being loaded. */ -"Loading Photos..." = "Loading Photos..."; - /* Text displayed while loading plans details */ "Loading Plan..." = "Loading Plan..."; @@ -5084,8 +5071,7 @@ Please install the %3$@ to use the app with this site."; /* Status for Media object that is only exists locally. */ "Local" = "Local"; -/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. - Title of the Local Changes Badge */ +/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. */ "Local changes" = "Local changes"; /* Local Services site intent topic */ @@ -5255,7 +5241,6 @@ Please install the %3$@ to use the app with this site."; "Max Video Upload Size" = "Max Video Upload Size"; /* Accessibility label for the Me button in My Site. - Label for the post author filter. This filter shows posts only authored by the current user. Me page title Noun. Title. Links to the Me screen. The accessibility value of the me tab. @@ -5263,9 +5248,7 @@ Please install the %3$@ to use the app with this site."; "Me" = "Me"; /* Noun. Title. Links to the blog's Media library. - Tab bar title for the Media tab in Media Picker The menu item to select during a guided tour. - Title for Media Library section of the app. Title for the media section in site settings screen Title label for the media settings section in the app settings */ "Media" = "Media"; @@ -5277,28 +5260,18 @@ Please install the %3$@ to use the app with this site."; /* Label for size of media cache in the app. */ "Media Cache Size" = "Media Cache Size"; -/* Title for alert when access to media capture is not granted */ -"Media Capture" = "Media Capture"; - /* Error message to show to users when trying to upload a media object with no local file associated */ "Media doesn't have an associated file to upload." = "Media doesn't have an associated file to upload."; /* Error message to show to users when trying to upload a media object with file size is larger than the max file size allowed in the site */ "Media filesize (%@) is too large to upload. Maximum allowed is %@" = "Media filesize (%1$@) is too large to upload. Maximum allowed is %2$@"; -/* Title for alert when a generic error happened when loading media - Title for alert when access to the media library is not granted by the user */ -"Media Library" = "Media Library"; - /* translators: %s: block title e.g: \"Paragraph\". */ "Media options" = "Media options"; /* Title for action sheet with media options. */ "Media Options" = "Media Options"; -/* Alert title when there is issues loading an asset to preview. */ -"Media preview failed." = "Media preview failed."; - /* Media Settings Title User action to edit media settings. */ "Media Settings" = "Media Settings"; @@ -5327,18 +5300,24 @@ Please install the %3$@ to use the app with this site."; /* Message of an alert informing users that the video they are trying to select is not allowed. */ "mediaExporter.videoLimitExceededError" = "Uploading videos longer than 5 minutes requires a paid plan."; -/* Verb. User action to dismiss error alert when failing to load media item. */ -"mediaItemTable.errorAlert.dismissButton" = "Dismiss"; - /* Accessibility hint for add button to add items to the user's media library */ "mediaLibrary.addButtonAccessibilityHint" = "Add new media"; /* Accessibility label for add button to add items to the user's media library */ "mediaLibrary.addButtonAccessibilityLabel" = "Add"; +/* Button name in the more menu */ +"mediaLibrary.aspectRatioGrid" = "Aspect Ratio Grid"; + +/* Context menu button */ +"mediaLibrary.buttonDelete" = "Delete"; + /* Media screen navigation bar button Select title */ "mediaLibrary.buttonSelect" = "Select"; +/* Context menu button */ +"mediaLibrary.buttonShare" = "Share"; + /* Verb. Button title. Tapping cancels an action. */ "mediaLibrary.deleteConfirmationCancel" = "Cancel"; @@ -5360,6 +5339,21 @@ Please install the %3$@ to use the app with this site."; /* Text displayed in HUD after successfully deleting a media item */ "mediaLibrary.deletionSuccessMessage" = "Deleted!"; +/* The name of the media filter */ +"mediaLibrary.filterAll" = "All"; + +/* The name of the media filter */ +"mediaLibrary.filterAudio" = "Audio"; + +/* The name of the media filter */ +"mediaLibrary.filterDocuments" = "Documents"; + +/* The name of the media filter */ +"mediaLibrary.filterImages" = "Images"; + +/* The name of the media filter */ +"mediaLibrary.filterVideos" = "Videos"; + /* User action to delete un-uploaded media. */ "mediaLibrary.retryOptionsAlert.delete" = "Delete"; @@ -5372,6 +5366,12 @@ Please install the %3$@ to use the app with this site."; /* Message displayed when no results are returned from a media library search. Should match Calypso. */ "mediaLibrary.searchResultsEmptyTitle" = "No media matching your search"; +/* Text displayed in HUD if there was an error attempting to share a group of media items. */ +"mediaLibrary.sharingFailureMessage" = "Unable to share the selected items."; + +/* Button name in the more menu */ +"mediaLibrary.squareGrid" = "Square Grid"; + /* Media screen navigation title */ "mediaLibrary.title" = "Media"; @@ -5393,6 +5393,12 @@ Please install the %3$@ to use the app with this site."; /* The title of the button to dismiss the alert shown when the picked media cannot be imported into stories. */ "mediaPicker.failedMediaExportAlert.dismissButton" = "Dismiss"; +/* Error message when picked media cannot be imported into stories. */ +"mediaPicker.failedMediaExportAlert.message" = "Your media could not be exported. If the problem persists you can contact us via the Me > Help & Support screen."; + +/* Error title when picked media cannot be imported into stories. */ +"mediaPicker.failedMediaExportAlert.title" = "Failed Media Export"; + /* Message for alert when access to camera is not granted */ "mediaPicker.noCameraAccessMessage" = "This app needs permission to access the Camera to capture new media, please change the privacy settings if you wish to allow this."; @@ -5426,6 +5432,12 @@ Please install the %3$@ to use the app with this site."; /* The name of the action in the context menu */ "mediaPicker.takeVideo" = "Take Video"; +/* Navigation title for media preview. Example: 1 of 3 */ +"mediaPreview.NofM" = "%1$@ of %2$@"; + +/* Max image size in pixels (e.g. 300x300px) */ +"mediaSizeSlider.valueFormat" = "%1$d × %2$d px"; + /* Medium image size. Should be the same as in core WP. */ "Medium" = "Medium"; @@ -5457,9 +5469,6 @@ Please install the %3$@ to use the app with this site."; Label for the share message field on the post settings. */ "Message" = "Message"; -/* Title of section containing image / video metadata such as size and file type */ -"Metadata" = "Metadata"; - /* Option to select the Microsft Outlook app when logging in with magic links */ "Microsoft Outlook" = "Microsoft Outlook"; @@ -5475,9 +5484,6 @@ Please install the %3$@ to use the app with this site."; /* The title in the Delete WordPress screen */ "migration.deleteWordpress.title" = "You no longer need the WordPress app on your device"; -/* Primary button title in the migration done screen. */ -"migration.done.actions.primary.title" = "Finish"; - /* Footer for the migration done screen. */ "migration.done.footer" = "We recommend uninstalling the WordPress app on your device to avoid data conflicts."; @@ -5487,6 +5493,9 @@ Please install the %3$@ to use the app with this site."; /* Primary description in the migration done screen. */ "migration.done.primaryDescription" = "We’ve transferred all your data and settings. Everything is right where you left it."; +/* Secondary description (second paragraph) in the migration done screen. */ +"migration.done.secondaryDescription" = "It's time to continue your WordPress journey on the Jetpack app!"; + /* Title of the migration done screen. */ "migration.done.title" = "Thanks for switching to Jetpack!"; @@ -5535,6 +5544,9 @@ Please install the %3$@ to use the app with this site."; /* The title in the migration welcome screen */ "migration.welcome.title" = "Welcome to Jetpack!"; +/* Primary button title in the migration done screen. */ +"migrationDone.actions.primaryTitle" = "Let's go"; + /* Summary description for a threat */ "Miscellaneous vulnerability" = "Miscellaneous vulnerability"; @@ -5551,13 +5563,11 @@ Please install the %3$@ to use the app with this site."; "Months and Years" = "Months and Years"; /* Accessibility label for more button in dashboard quick start card. - Accessibility label for the More button in Page List. Accessibility label for the More button in Post List (compact view). Accessibility label for the More button on formatting toolbar. Accessibility label for the More button on Reader Cell Accessibility label for the More button on Reader's post details - Action button to display more available options - Label for the more post button. Tapping displays an action sheet with post options. */ + Action button to display more available options */ "More" = "More"; /* Action button to display more available options @@ -5618,18 +5628,11 @@ Please install the %3$@ to use the app with this site."; /* No comment provided by engineer. */ "Move to bottom" = "Move to bottom"; -/* Label for a button that moves a page to the draft folder - Label for an option that moves a post to the draft folder */ -"Move to Draft" = "Move to Draft"; - /* No comment provided by engineer. */ "Move to top" = "Move to top"; -/* Label for a button that moves a page to the trash folder - Label for a option that moves a post to the trash folder - Title for button on the comment details page that moves the comment to trash when tapped. +/* Title for button on the comment details page that moves the comment to trash when tapped. Trash option in the trash confirmation alert. - Trash option in the trash page confirmation alert. Trashes the comment */ "Move to Trash" = "Move to Trash"; @@ -5676,7 +5679,8 @@ Please install the %3$@ to use the app with this site."; Title of My Site tab */ "My Site" = "My Site"; -/* Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ +/* Title for site picker screen. + Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ "My Sites" = "My Sites"; /* Siri Suggestion to open My Sites */ @@ -5739,6 +5743,30 @@ Please install the %3$@ to use the app with this site."; /* Message title for when a user has no sites. */ "mySite.noSites.title" = "You don't have any sites"; +/* Menu title for the add site option */ +"mySite.siteActions.addSite" = "Add site"; + +/* Button that reveals more site actions */ +"mySite.siteActions.button" = "Site Actions"; + +/* Accessibility hint for button used to show more site actions */ +"mySite.siteActions.hint" = "Tap to show more site actions"; + +/* Menu title for the personalize home option */ +"mySite.siteActions.personalizeHome" = "Personalize home"; + +/* Menu title for the change site icon option */ +"mySite.siteActions.siteIcon" = "Change site icon"; + +/* Menu title for the change site title option */ +"mySite.siteActions.siteTitle" = "Change site title"; + +/* Menu title for the switch site option */ +"mySite.siteActions.switchSite" = "Switch site"; + +/* Menu title for the visit site option */ +"mySite.siteActions.visitSite" = "Visit site"; + /* Accessibility label for the Email text field. Header for a comment author's name, shown when editing a comment. Name text field placeholder */ @@ -5781,8 +5809,11 @@ Please install the %3$@ to use the app with this site."; /* Describes a status of a plugin */ "Needs Update" = "Needs Update"; -/* Label indicating that a domain name registration has no expiry date. */ -"Never expires" = "Never expires"; +/* No comment provided by engineer. */ +"Network connection lost, working offline" = "Network connection lost, working offline"; + +/* No comment provided by engineer. */ +"Network connection re-established" = "Network connection re-established"; /* No comment provided by engineer. */ "NEW" = "NEW"; @@ -5955,9 +5986,6 @@ Please install the %3$@ to use the app with this site."; /* List Editor Empty State Message */ "No Items" = "No Items"; -/* Title when users have no Jetpack sites. */ -"No Jetpack sites found" = "No Jetpack sites found"; - /* Displayed in the Notifications Tab as a title, when the Likes Filter shows no notifications */ "No likes yet" = "No likes yet"; @@ -5967,9 +5995,7 @@ Please install the %3$@ to use the app with this site."; /* Title for the view when there aren't any Activities to display in the Activity Log for a given filter. */ "No matching events found." = "No matching events found."; -/* Message displayed when no results are returned from a media library search. Should match Calypso. - Phrase to show when the user search for images but there are no result to show. - Phrase to show when the user searches for GIFs but there are no result to show. */ +/* Message displayed when no results are returned from a media library search. Should match Calypso. */ "No media matching your search" = "No media matching your search"; /* Menus selection title for setting a location to not use a menu. */ @@ -5993,8 +6019,7 @@ Please install the %3$@ to use the app with this site."; /* Displayed in the Notifications Tab as a title, when there are no notifications */ "No notifications yet" = "No notifications yet"; -/* Displayed when the user is searching the pages list and there are no matching pages - Text displayed when there's no matching with the text search */ +/* Text displayed when there's no matching with the text search */ "No pages matching your search" = "No pages matching your search"; /* Text displayed when search for plugins returns no results */ @@ -6015,9 +6040,6 @@ Please install the %3$@ to use the app with this site."; /* Message shown whent the reader finds no posts for the chosen tag */ "No posts have been made recently with this tag." = "No posts have been made recently with this tag."; -/* Displayed when the user is searching the posts list and there are no matching posts */ -"No posts matching your search" = "No posts matching your search"; - /* Accessibility value for a Stats' Posting Activity Month if there are no posts. */ "No posts." = "No posts."; @@ -6088,9 +6110,6 @@ Please install the %3$@ to use the app with this site."; /* Error message to show to users when trying to upload a media object with file size is larger than the available site disk quota */ "Not enough space to upload" = "Not enough space to upload"; -/* Accessibility label for unselected following buttons. */ -"Not following" = "Not following"; - /* Button label for denying our request to allow push notifications Not now button title shown in alert preparing users to grant permission for us to send them push notifications. Phrase displayed to dismiss a quick start tour suggestion. */ @@ -6127,9 +6146,6 @@ Please install the %3$@ to use the app with this site."; /* A message title */ "Nothing liked yet" = "Nothing liked yet"; -/* Default message for empty media picker */ -"Nothing to show" = "Nothing to show"; - /* Notifications Details Accessibility Identifier */ "Notification Details Table" = "Notification Details Table"; @@ -6195,9 +6211,6 @@ Please install the %3$@ to use the app with this site."; /* Discoverability title for numbered list keyboard shortcut. */ "Numbered List" = "Numbered List"; -/* Word separating the current index from the total amount. I.e.: 7 of 9 */ -"of" = "of"; - /* Disabled */ "Off" = "Off"; @@ -6210,7 +6223,6 @@ Please install the %3$@ to use the app with this site."; Alert dismissal title Button title. Acknowledges a prompt. Button title. An acknowledgement of the message displayed in a prompt. - Confirmation of action Default action Dismisses the alert Marks all notifications as read. @@ -6220,7 +6232,6 @@ Please install the %3$@ to use the app with this site."; Ok button for dismissing alert helping users understand their site address OK button title for the warning shown to the user when the app realizes there should be an auth token but there isn't one. OK Button title shown in alert informing users about the Reader Save for Later feature. - OK button to close the informative dialog on Gutenberg editor Submit button on prompt for user information. Title of a button that dismisses a prompt Title of an OK button. Pressing the button acknowledges and dismisses a prompt. @@ -6263,9 +6274,6 @@ Please install the %3$@ to use the app with this site."; /* No comment provided by engineer. */ "Only show excerpt" = "Only show excerpt"; -/* Message telling the user that they've only enabled limited photo library permissions for the app. */ -"Only the selected photos you've given access to are available." = "Only the selected photos you've given access to are available."; - /* An informal exclaimation that means `something went wrong`. Title for the view when there's an error loading a comment. Title for the view when there's an error loading Activity Log @@ -6292,9 +6300,6 @@ Please install the %3$@ to use the app with this site."; /* Opens iOS's Device Settings for WordPress App */ "Open Device Settings" = "Open Device Settings"; -/* Editor button to swich the media picker from quick mode to full picker */ -"Open full media picker" = "Open full media picker"; - /* Label for the description of openening a link using a new window */ "Open in a new Window/Tab" = "Open in a new Window/Tab"; @@ -6381,9 +6386,6 @@ Please install the %3$@ to use the app with this site."; Title of Stats section that shows referrer traffic from other sources. */ "Other" = "Other"; -/* Label displayed on media items that are not video, image, or audio. */ -"other" = "other"; - /* Accessibility label for selecting an image or video from other applications on formatting toolbar. Menu option used for adding media from other applications. */ "Other Apps" = "Other Apps"; @@ -6415,24 +6417,12 @@ Please install the %3$@ to use the app with this site."; /* Title of notification displayed when a page has failed to upload. */ "Page failed to upload" = "Page failed to upload"; -/* A short message explaining that a page was moved to the trash bin. */ -"Page moved to trash." = "Page moved to trash."; - /* Title of notification displayed when a page has been successfully saved as a draft. */ "Page pending review" = "Page pending review"; /* Title of notification displayed when a page has been successfully published. */ "Page published" = "Page published"; -/* Prompts the user that a restored page was moved to the drafts list. */ -"Page Restored to Drafts" = "Page Restored to Drafts"; - -/* Prompts the user that a restored page was moved to the published list. */ -"Page Restored to Published" = "Page Restored to Published"; - -/* Prompts the user that a restored page was moved to the scheduled list. */ -"Page Restored to Scheduled" = "Page Restored to Scheduled"; - /* Title of notification displayed when a page has been successfully scheduled. */ "Page scheduled" = "Page scheduled"; @@ -6449,21 +6439,63 @@ Please install the %3$@ to use the app with this site."; /* Title of notification displayed when a page has been successfully updated. */ "Page updated" = "Page updated"; +/* Badge for page cells */ +"pageList.badgeHomepage" = "Homepage"; + +/* Badge for page cells */ +"pageList.badgeLocalChanges" = "Local changes"; + +/* Badge for page cells */ +"pageList.badgePendingReview" = "Pending review"; + +/* Badge for page cells */ +"pageList.badgePosts" = "Posts page"; + +/* Badge for page cells */ +"pageList.badgePrivate" = "Private"; + /* Noun. Title. Links to the blog's Pages screen. The item to select during a guided tour. This is the section title Title of the screen showing the list of pages for a blog. */ "Pages" = "Pages"; -/* Promote the page with Blaze. */ -"pages.blaze.actionTitle" = "Promote with Blaze"; - /* Subtitle of the theme template homepage cell */ "pages.template.subtitle" = "Your homepage is using a Theme template and will open in the web editor."; /* Title of the theme template homepage cell */ "pages.template.title" = "Homepage"; +/* Message informing the user that their static homepage page was set successfully */ +"pages.updatePage.successTitle" = "Page successfully updated"; + +/* Delete option in the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.actionTitle" = "Delete Permanently"; + +/* Message of the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.alertMessage" = "Are you sure you want to permanently delete this page?"; + +/* Title of the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.alertTitle" = "Delete Permanently?"; + +/* Menu option for filtering posts by everyone */ +"pagesList.pagesByEveryone" = "Pages by everyone"; + +/* Menu option for filtering posts by me */ +"pagesList.pagesByMe" = "Pages by me"; + +/* Trash option in the trash page confirmation alert. */ +"pagesList.trash.actionTitle" = "Move to Trash"; + +/* Message of the trash page confirmation alert. */ +"pagesList.trash.alertMessage" = "Are you sure you want to trash this page?"; + +/* Title of the trash page confirmation alert. */ +"pagesList.trash.alertTitle" = "Trash this page?"; + +/* Cancels an Action */ +"pagesList.trash.cancel" = "Cancel"; + /* Comments Paging Discussion Settings Settings: Comments Paging preferences */ @@ -6514,8 +6546,7 @@ Please install the %3$@ to use the app with this site."; Title of pending Comments filter. */ "Pending" = "Pending"; -/* Name for the status of a post pending review. - Title of the Pending Review Badge */ +/* Name for the status of a post pending review. */ "Pending review" = "Pending review"; /* Noun. Title of the people management feature. @@ -6583,25 +6614,19 @@ Please install the %3$@ to use the app with this site."; /* Photography site intent topic */ "Photography" = "Photography"; -/* Tab bar title for the Photos tab in Media Picker */ -"Photos" = "Photos"; - -/* Subtitle for placeholder in Free Photos. The company name 'Pexels' should always be written as it is. */ -"Photos provided by Pexels" = "Photos provided by Pexels"; - /* Title for selecting a new username in the site creation flow. */ "Pick username" = "Pick username"; /* Caption for the recommended sections in site designs. */ "PICKED FOR YOU" = "PICKED FOR YOU"; -/* The item to select during a guided tour. */ -"Plan" = "Plan"; - /* Action title. Noun. Links to a blog's Plans screen. Title for the plan selector */ "Plans" = "Plans"; +/* Title for the plan selection view */ +"planSelection.title" = "Plans"; + /* User action to play a video on the editor. */ "Play video" = "Play video"; @@ -6791,9 +6816,6 @@ Please install the %3$@ to use the app with this site."; The post formats available for the post. Should be the same as in core WP. */ "Post Format" = "Post Format"; -/* A short message explaining that a post was moved to the trash bin. */ -"Post moved to trash." = "Post moved to trash."; - /* Title of notification displayed when a post has been successfully saved as a draft. */ "Post pending review" = "Post pending review"; @@ -6803,15 +6825,6 @@ Please install the %3$@ to use the app with this site."; /* Title of the notification presented in Reader when a post is removed from save for later */ "Post removed." = "Post removed."; -/* Prompts the user that a restored post was moved to the drafts list. */ -"Post Restored to Drafts" = "Post Restored to Drafts"; - -/* Prompts the user that a restored post was moved to the published list. */ -"Post Restored to Published" = "Post Restored to Published"; - -/* Prompts the user that a restored post was moved to the scheduled list. */ -"Post Restored to Scheduled" = "Post Restored to Scheduled"; - /* Title of the notification presented in Reader when a post is saved for later */ "Post saved." = "Post saved."; @@ -6840,6 +6853,27 @@ Please install the %3$@ to use the app with this site."; /* Title of notification displayed when a post has been successfully updated. */ "Post updated" = "Post updated"; +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.createdTimeAgo" = "Created %@"; + +/* Status mesasge for post cells */ +"post.deletingPostPermanentlyStatusMessage" = "Deleting post..."; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.editedTimeAgo" = "Edited %@"; + +/* Status mesasge for post cells */ +"post.movingToTrashStatusMessage" = "Moving post to trash..."; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.publishedTimeAgo" = "Published %@"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.scheduledForDate" = "Scheduled %@"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.trashedTimeAgo" = "Trashed %@"; + /* Register Domain - Address information field Postal Code */ "Postal Code" = "Postal Code"; @@ -6853,6 +6887,30 @@ Please install the %3$@ to use the app with this site."; Title for stats Posting Activity view. */ "Posting Activity" = "Posting Activity"; +/* Accessibility label for the post author in the post list. The parameter is the author name. For example, \"By Elsa.\" */ +"postList.a11y.authorChunkFormat" = "By %@."; + +/* Accessibility label for a post's excerpt in the post list. The parameter is the post excerpt. For example, \"Excerpt. This is the first paragraph.\" */ +"postList.a11y.exerptChunkFormat" = "Excerpt. %@."; + +/* Accessibility label for a sticky post in the post list. */ +"postList.a11y.sticky" = "Sticky."; + +/* Accessibility label for a post in the post list. The first placeholder is the post title. The second placeholder is the date. */ +"postList.a11y.titleAndDateChunkFormat" = "%1$@, %2$@."; + +/* Title for the 'Trash' post list row swipe action */ +"postList.swipeActionDelete" = "Trash"; + +/* Title for the 'Delete' post list row swipe action */ +"postList.swipeActionDeletePermanently" = "Delete"; + +/* Title for the 'Share' post list row swipe action */ +"postList.swipeActionShare" = "Share"; + +/* Title for the 'View' post list row swipe action */ +"postList.swipeActionView" = "View"; + /* All Time Stats 'Posts' label Insights 'Posts' header Noun. Title. Links to the blog's Posts screen. @@ -6868,9 +6926,6 @@ Please install the %3$@ to use the app with this site."; /* Title for setting which shows the current page assigned as a site's posts page */ "Posts Page" = "Posts Page"; -/* Title of the Posts Page Badge */ -"Posts page" = "Posts page"; - /* Message informing the user that their static homepage for posts was set successfully */ "Posts page successfully updated" = "Posts page successfully updated"; @@ -6886,6 +6941,60 @@ Please install the %3$@ to use the app with this site."; /* Promote the post with Blaze. */ "posts.blaze.actionTitle" = "Promote with Blaze"; +/* Label for the Post List option that cancels automatic uploading of a post. */ +"posts.cancelUpload.actionTitle" = "Cancel upload"; + +/* Label for post comments option. Tapping displays comments for a post. */ +"posts.comments.actionTitle" = "Comments"; + +/* Label for the delete post option. Tapping permanently deletes a post. */ +"posts.delete.actionTitle" = "Delete permanently"; + +/* Label for an option that moves a post to the draft folder */ +"posts.draft.actionTitle" = "Move to draft"; + +/* Label for post duplicate option. Tapping creates a copy of the post. */ +"posts.duplicate.actionTitle" = "Duplicate"; + +/* Opens a submenu for page attributes. */ +"posts.pageAttributes.actionTitle" = "Page attributes"; + +/* Label for the preview post button. Tapping displays the post as it appears on the web. */ +"posts.preview.actionTitle" = "Preview"; + +/* Label for an option that moves a publishes a post immediately */ +"posts.publish.actionTitle" = "Publish now"; + +/* Retry uploading the post. */ +"posts.retry.actionTitle" = "Retry"; + +/* Set the selected page as the homepage. */ +"posts.setHomepage.actionTitle" = "Set as homepage"; + +/* Set the parent page for the selected page. */ +"posts.setParent.actionTitle" = "Set parent"; + +/* Set the selected page as a posts page. */ +"posts.setPostsPage.actionTitle" = "Set as posts page"; + +/* Set the selected page as a regular page. */ +"posts.setRegularPage.actionTitle" = "Set as regular page"; + +/* Label for post settings option. Tapping displays settings for a post. */ +"posts.settings.actionTitle" = "Settings"; + +/* Share the post. */ +"posts.share.actionTitle" = "Share"; + +/* Label for post stats option. Tapping displays statistics for a post. */ +"posts.stats.actionTitle" = "Stats"; + +/* Label for a option that moves a post to the trash folder */ +"posts.trash.actionTitle" = "Move to trash"; + +/* Label for the view post button. Tapping displays the post as it appears on the web. */ +"posts.view.actionTitle" = "View"; + /* User action to dismiss featured media options. */ "postSettings.featuredImageUploadActionSheet.dismiss" = "Dismiss"; @@ -6913,8 +7022,26 @@ Please install the %3$@ to use the app with this site."; /* Beginning text of the remaining social shares a user has left. %1$d is their current remaining shares. This text is combined with ' in the next 30 days' if there is no warning displayed. */ "postsettings.social.shares.text.format" = "%1$d social shares remaining"; -/* Subtitle for placeholder in Tenor picker. `The company name 'Tenor' should always be written as it is. */ -"Powered by Tenor" = "Powered by Tenor"; +/* Error message on post/page settings screen */ +"postSettings.updateFailedMessage" = "Failed to update the post settings"; + +/* A short message explaining that a page was deleted permanently. */ +"postsList.deletePage.message" = "Page deleted permanently"; + +/* A short message explaining that a post was deleted permanently. */ +"postsList.deletePost.message" = "Post deleted permanently"; + +/* A short message explaining that a page was moved to the trash bin. */ +"postsList.movePageToTrash.message" = "Page moved to trash"; + +/* A short message explaining that a post was moved to the trash bin. */ +"postsList.movePostToTrash.message" = "Post moved to trash"; + +/* Menu option for filtering posts by everyone */ +"postsList.postsByEveryone" = "Posts by everyone"; + +/* Menu option for filtering posts by me */ +"postsList.postsByMe" = "Posts by me"; /* Browse premium themes selection title */ "Premium" = "Premium"; @@ -6993,15 +7120,9 @@ Tapping on this row allows the user to edit the sharing message. */ Title for screen to preview a static content. */ "Preview" = "Preview"; -/* Action for Media Picker to preview the selected media items. The argument in the string represents the number of elements (as numeric digits) selected */ -"Preview %@" = "Preview %@"; - /* Title for web preview device switching button */ "Preview Device" = "Preview Device"; -/* Accessibility label for media item preview for user's viewing an item in their media library */ -"Preview media" = "Preview media"; - /* No comment provided by engineer. */ "Preview page" = "Preview page"; @@ -7027,9 +7148,6 @@ Tapping on this row allows the user to edit the sharing message. */ Primary Web Site */ "Primary Site" = "Primary Site"; -/* Primary site address label, used in the site address section of the Domains Dashboard. */ -"Primary site address" = "Primary site address"; - /* Label for the privacy setting Privacy settings section header */ "Privacy" = "Privacy"; @@ -7051,8 +7169,7 @@ Tapping on this row allows the user to edit the sharing message. */ "Privacy Settings" = "Privacy Settings"; /* Name for the status of a post that is marked private. - Privacy setting for posts set to 'Private'. Should be the same as in core WP. - Title of the Private Badge */ + Privacy setting for posts set to 'Private'. Should be the same as in core WP. */ "Private" = "Private"; /* No comment provided by engineer. */ @@ -7090,12 +7207,6 @@ Tapping on this row allows the user to edit the sharing message. */ /* Title for a tappable string that opens the reader with a prompts tag */ "prompts.card.viewprompts.title" = "View all responses"; -/* Subtitle of the notification when prompts are hidden from the dashboard card */ -"prompts.notification.removed.subtitle" = "Visit Site Settings to turn back on"; - -/* Title of the notification when prompts are hidden from the dashboard card */ -"prompts.notification.removed.title" = "Blogging Prompts hidden"; - /* Privacy setting for posts set to 'Public' (default). Should be the same as in core WP. */ "Public" = "Public"; @@ -7117,12 +7228,10 @@ Tapping on this row allows the user to edit the sharing message. */ /* Immediately publish button title */ "Publish immediately" = "Publish immediately"; -/* A short phrase indicating a post is due to be immedately published. - Label for a button that moves a page to the published folder, publishing with the current date/time. */ +/* A short phrase indicating a post is due to be immedately published. */ "Publish Immediately" = "Publish Immediately"; /* Label for a button that publishes the post - Label for an option that moves a publishes a post immediately Title of button allowing the user to immediately publish the post they are editing. */ "Publish Now" = "Publish Now"; @@ -7134,8 +7243,7 @@ Tapping on this row allows the user to edit the sharing message. */ /* Name for the status of a published post. Period Stats 'Published' header - Title of the published filter. This filter shows a list of posts that the user has published. - Title of the published header in search list. */ + Title of the published filter. This filter shows a list of posts that the user has published. */ "Published" = "Published"; /* Precedes the name of the blog just posted on */ @@ -7150,9 +7258,6 @@ Tapping on this row allows the user to edit the sharing message. */ /* A short message that informs the user a post is being published to the server from the share extension. */ "Publishing post..." = "Publishing post..."; -/* Label that describes in which blog the user is publishing to */ -"Publishing To" = "Publishing To"; - /* Text displayed in HUD while a post is being published. */ "Publishing..." = "Publishing..."; @@ -7177,9 +7282,6 @@ Tapping on this row allows the user to edit the sharing message. */ /* Title for the success view when the user has successfully logged in */ "qrLoginVerifyAuthorization.completedInstructions.title" = "You're logged in!"; -/* The menu item to select during a guided tour. */ -"Quick Start" = "Quick Start"; - /* The quick tour actions item to select during a guided tour. */ "quickStart.moreMenu" = "More"; @@ -7201,13 +7303,9 @@ Tapping on this row allows the user to edit the sharing message. */ The accessibility value of the reader tab. The default title of the Reader The menu item to select during a guided tour. - Title of the 'Reader' tab - used for spotlight indexing on iOS. - Title of the Reader section of the debug screen used in debug builds of the app */ + Title of the 'Reader' tab - used for spotlight indexing on iOS. */ "Reader" = "Reader"; -/* Title of the screen that allows the user to change the Reader CSS URL for debug builds */ -"Reader CSS URL" = "Reader CSS URL"; - /* Accessibility hint to inform that the author section can be tapped to see posts from the site. */ "reader.detail.header.authorInfo.a11y.hint" = "Views posts from the site"; @@ -7267,13 +7365,15 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Text for the 'Like' button on the reader post card cell. */ "reader.post.button.like" = "Like"; -/* Accessibility hint for the like button on the reader post card cell - Accessibility hint for the liked button on the reader post card cell */ +/* Accessibility hint for the like button on the reader post card cell */ "reader.post.button.like.accessibility.hint" = "Likes the post."; /* Text for the 'Liked' button on the reader post card cell. */ "reader.post.button.liked" = "Liked"; +/* Accessibility hint for the liked button on the reader post card cell */ +"reader.post.button.liked.accessibility.hint" = "Unlikes the post."; + /* Accessibility hint for the site header on the reader post card cell */ "reader.post.button.menu.accessibility.hint" = "Opens a menu with more actions."; @@ -7403,6 +7503,12 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Register Domain - Register publicly option title */ "Register publicly" = "Register publicly"; +/* The button title for the transfer footer view in Register Domain screen */ +"register.domain.transfer.button.title" = "Transfer domain"; + +/* The title for the transfer footer view in Register Domain screen */ +"register.domain.transfer.title" = "Looking to transfer a domain you already own?"; + /* Describes a domain that was registered with WordPress.com */ "Registered Domain" = "Registered Domain"; @@ -7460,8 +7566,7 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Title of the completion screen of the Blogging Reminders Settings screen when the reminders are removed. */ "Reminders removed" = "Reminders removed"; -/* Add asset to media picker list - Alert button to confirm a plugin to be removed +/* Alert button to confirm a plugin to be removed Button label when removing a blog Label for the Remove Feature Image icon. Tapping will show a confirmation screen for removing the feature image from the post. Remove Action @@ -7540,9 +7645,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Explanation of what will happen if the user confirms this alert. */ "Removing Next Steps will hide all tours on this site. This action cannot be undone." = "Removing Next Steps will hide all tours on this site. This action cannot be undone."; -/* Label indicating the date on which a domain name registration will be renewed. The %@ placeholder will be replaced with a date at runtime. */ -"Renews on %@" = "Renews on %@"; - /* No comment provided by engineer. */ "Replace audio" = "Replace audio"; @@ -7614,9 +7716,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Title of secondary button on alert prompting verify their accounts while attempting to publish */ "Resend" = "Resend"; -/* Title of the reset button */ -"Reset" = "Reset"; - /* Accessibility label for the reset activity type button */ "Reset Activity Type filter" = "Reset Activity Type filter"; @@ -7671,17 +7770,13 @@ Example: given a notice format "Following %@" and empty site name, this will be Button title, displayed when media has failed to upload. Allows the user to try the upload again. Button title. Retries uploading a post. If a user taps the button with this label, the action that evinced this error view will be retried. - Label for a button that attempts to re-upload a page that previously failed to upload. - Label for the retry post upload button. Tapping attempts to upload the post again. Opens the media library . Retries the upload of a user's post. Retry updating User's Role - Retry uploading the post. Retry. Action Retry. Verb – retry a failed media upload. The Jetpack view button title used when an error occurred Title for accessory view in the empty state table view cell in the Verticals step of Enhanced Site Creation - title for action that tries to connect to the reader after a loading error. User action to retry media upload. */ "Retry" = "Retry"; @@ -7691,9 +7786,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Button title that triggers a scan */ "Retry Scan" = "Retry Scan"; -/* User action to retry media upload. */ -"Retry Upload" = "Retry Upload"; - /* No comment provided by engineer. */ "Retry?" = "Retry?"; @@ -7732,6 +7824,9 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Button label to open web page in Safari */ "Safari" = "Safari"; +/* Title of a row displayed on the debug screen used to configure the sandbox store use in the App. */ +"Sandbox Store" = "Sandbox Store"; + /* Menus save button title Save Action Save button label (saving content, ex: Post, Page, Comment, Category). @@ -7782,9 +7877,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Accessibility label for the 'Save Post' button when a post has been saved. */ "Saved Post" = "Saved Post"; -/* Text displayed in HUD when a media item's metadata (title, etc) is saved successfully. */ -"Saved!" = "Saved!"; - /* Accessibility hint for the 'Save Post' button. */ "Saves this post for later." = "Saves this post for later."; @@ -7795,7 +7887,6 @@ Example: given a notice format "Following %@" and empty site name, this will be "Saving post…" = "Saving post…"; /* Menus save button title while it is saving a Menu. - Text displayed in HUD while a media item's metadata (title, etc) is being saved. Text displayed in HUD while a post is being saved as a draft. */ "Saving..." = "Saving..."; @@ -7877,12 +7968,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* No comment provided by engineer. */ "Search or type URL" = "Search or type URL"; -/* Text displayed when the search controller will be presented */ -"Search pages" = "Search pages"; - -/* Text displayed when the search controller will be presented */ -"Search posts" = "Search posts"; - /* No comment provided by engineer. */ "Search settings" = "Search settings"; @@ -7892,12 +7977,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Period Stats 'Search Terms' header */ "Search Terms" = "Search Terms"; -/* Title for placeholder in Free Photos */ -"Search to find free photos to add to your Media Library!" = "Search to find free photos to add to your Media Library!"; - -/* Title for placeholder in Tenor picker */ -"Search to find GIFs to add to your Media Library!" = "Search to find GIFs to add to your Media Library!"; - /* Placeholder text for the Reader search feature. */ "Search WordPress" = "Search WordPress"; @@ -7919,9 +7998,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Caption displayed in promotional screens shown during the login flow. */ "See comments and notifications in real time." = "See comments and notifications in real time."; -/* Action button linking to instructions for installing Jetpack.Presented when logging in with a site address that does not have a valid Jetpack installation */ -"See Instructions" = "See Instructions"; - /* Select action on the app extension category picker screen. Saves the selected categories for the post. Select action on the app extension post type picker screen. Saves the selected post type for the post. */ "Select" = "Select"; @@ -7935,24 +8011,15 @@ Example: given a notice format "Following %@" and empty site name, this will be /* A step in a guided tour for quick start. %@ will be the name of the item to select. */ "Select %@ to create a new post" = "Select %@ to create a new post"; -/* A step in a guided tour for quick start. %@ will be the name of the item to select. */ -"Select %@ to discover new themes" = "Select %@ to discover new themes"; - /* A step in a guided tour for quick start. %@ will be the name of the item to select. */ "Select %@ to find other sites." = "Select %@ to find other sites."; /* A step in a guided tour for quick start. %@ will be the name of the item to select. */ "Select %@ to see how your site is performing." = "Select %@ to see how your site is performing."; -/* A step in a guided tour for quick start. %@ will be the name of the item to select. */ -"Select %@ to see your checklist" = "Select %@ to see your checklist"; - /* A step in a guided tour for quick start. %@ will be the name of the item to select. */ "Select %@ to see your current library." = "Select %@ to see your current library."; -/* A step in a guided tour for quick start. %@ will be the name of the item to select. */ -"Select %@ to see your current plan and other available plans." = "Select %@ to see your current plan and other available plans."; - /* A step in a guided tour for quick start. %@ will be the name of the item to select. */ "Select %@ to see your page list." = "Select %@ to see your page list."; @@ -7986,12 +8053,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Register domain - Title for the Choose domain button of Suggested domains screen */ "Select domain" = "Select domain"; -/* Accessibility hint for actions when displaying media items. */ -"Select media." = "Select media."; - -/* Title of button that allows the user to select more photos to access within the app */ -"Select More" = "Select More"; - /* Accessibility label for selecting paragraph style button on formatting toolbar. */ "Select paragraph style" = "Select paragraph style"; @@ -8106,14 +8167,7 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Notice confirming that an image has been set as the post's featured image. */ "Set as featured image" = "Set as featured image"; -/* Label for a button that sets the selected page as the site's Homepage */ -"Set as Homepage" = "Set as Homepage"; - -/* Label for a button that sets the selected page as the site's Posts page */ -"Set as Posts Page" = "Set as Posts Page"; - -/* Label for a button that opens the Set Parent options view controller - Navigation title displayed on the navigation bar */ +/* Navigation title displayed on the navigation bar */ "Set Parent" = "Set Parent"; /* Title of the set goals button in the Blogging Reminders Settings flow. */ @@ -8156,7 +8210,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Accessibility label for share buttons in nav bars Button label to share a post Button label to share a web page - Share the post. Shares the comment URL Spoken accessibility label Title for a button that allows the user to share their answer to the prompt. @@ -8363,9 +8416,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Describes a site redirect domain */ "Site Redirect" = "Site Redirect"; -/* Prologue title label, the \n force splits it into 2 lines. */ -"Site security and performance\nfrom your pocket" = "Site security and performance\nfrom your pocket"; - /* Noun. Title. Links to the blog's Settings screen. */ "Site Settings" = "Site Settings"; @@ -8400,6 +8450,30 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Site name that is placed in the tooltip view. */ "site.creation.domain.tooltip.site.name" = "YourSiteName.com"; +/* Header of the secondary domains list section in the Domains Dashboard. %1$@ is the name of the site. */ +"site.domains.domainSection.title" = "Other domains for %1$@"; + +/* A section title which displays a row with a free WP.com domain */ +"site.domains.freeDomainSection.title" = "Your Free WordPress.com domain"; + +/* Description for the first domain purchased with a paid plan. */ +"site.domains.freeDomainWithPaidPlan.description" = "Get a free one-year domain registration or transfer with any annual paid plan."; + +/* Title of the card that starts the purchase of the first domain with a paid plan. */ +"site.domains.freeDomainWithPaidPlan.title" = "Get your domain"; + +/* Footer of the primary site section in the Domains Dashboard. */ +"site.domains.primaryDomain" = "Your primary site address is what visitors will see in their address bar when visiting your website."; + +/* Primary domain label, used in the site address section of the Domains Dashboard. */ +"site.domains.primaryDomain.title" = "Primary domain"; + +/* Title for a button that opens domain purchasing flow. */ +"site.domains.purchaseDirectly.buttons.title" = "Just search for a domain"; + +/* Title for a button that opens plan and domain purchasing flow. */ +"site.domains.purchaseWithPlan.buttons.title" = "Upgrade to a plan"; + /* Back button title shown in Site Creation flow to come back from Plan selection to Domain selection */ "siteCreation.domain.backButton.title" = "Domains"; @@ -8424,6 +8498,21 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Accessibility hint for actions when displaying media items. */ "siteMedia.cellAccessibilityHint" = "Select media."; +/* Accessibility hint for media item preview for user's viewing an item in their media library */ +"siteMediaItem.contentViewAccessibilityHint" = "Tap to view media in full screen"; + +/* Accessibility label for media item preview for user's viewing an item in their media library */ +"siteMediaItem.contentViewAccessibilityLabel" = "Preview media"; + +/* Title for confirmation navigation bar button item */ +"siteMediaPicker.add" = "Add"; + +/* Button selection media in media picker */ +"siteMediaPicker.deselect" = "Deselect"; + +/* Button selection media in media picker */ +"siteMediaPicker.select" = "Select"; + /* Media screen navigation title */ "siteMediaPicker.title" = "Media"; @@ -8444,7 +8533,7 @@ Example: given a notice format "Following %@" and empty site name, this will be "sitesettings.reminders.title" = "Reminders"; /* Hint for users when hidden privacy setting is set */ -"siteVisibility.hidden.hint" = "Your site is visible to everyone, but asks search engines not to index your site."; +"siteVisibility.hidden.hint" = "Your site is hidden from visitors behind a \"Coming Soon\" notice until it is ready for viewing."; /* Text for privacy settings: Hidden */ "siteVisibility.hidden.title" = "Hidden"; @@ -8541,9 +8630,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Title shown on the dashboard when it fails to load */ "Some data wasn't loaded" = "Some data wasn't loaded"; -/* Confirms with the user if they save the post all media that failed to upload will be removed from it. */ -"Some media uploads failed. This action will remove all failed media from the post.\nSave anyway?" = "Some media uploads failed. This action will remove all failed media from the post.\nSave anyway?"; - /* Title for a label that appears when the scan failed Title for the error view when the scan start has failed */ "Something went wrong" = "Something went wrong"; @@ -8667,8 +8753,7 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Name of setting configured when a site uses a static page as its homepage */ "Static Homepage" = "Static Homepage"; -/* Label for post stats option. Tapping displays statistics for a post. - Noun. Abbreviation of Statistics. Name of the Stats feature +/* Noun. Abbreviation of Statistics. Name of the Stats feature Noun. Abbv. of Statistics. Links to a blog's Stats screen. Stats 3D Touch Shortcut Stats window title @@ -8801,12 +8886,15 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Label text that defines a post marked as sticky */ "Sticky" = "Sticky"; -/* Accessibility label for a sticky post in the post list. */ -"Sticky." = "Sticky."; - /* Dismiss the AlertView */ "stockPhotos.strings.dismiss" = "Dismiss"; +/* Subtitle for placeholder in Free Photos. The company name 'Pexels' should always be written as it is. */ +"stockPhotos.subtitle" = "Photos provided by Pexels"; + +/* Title for placeholder in Free Photos */ +"stockPhotos.title" = "Search to find free photos to add to your Media Library!"; + /* User action to stop upload. */ "Stop upload" = "Stop upload"; @@ -8971,9 +9059,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Support email label. */ "support.row.email.title" = "Email"; -/* Option in Support view to view the Forums. */ -"support.row.forums.title" = "WordPress Forums"; - /* Option in Support view to launch the Help Center. */ "support.row.helpCenter.title" = "WordPress Help Center"; @@ -9010,7 +9095,7 @@ Example: given a notice format "Following %@" and empty site name, this will be /* View title for Help & Support page. */ "support.title" = "Help"; -/* Button used to switch site */ +/* Title for back button that leads to the site picker screen. */ "Switch Site" = "Switch Site"; /* Switches from the classic editor to block editor. */ @@ -9104,9 +9189,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* No comment provided by engineer. */ "Take a Video" = "Take a Video"; -/* Menu option for taking an image or video with the device's camera. */ -"Take Photo or Video" = "Take Photo or Video"; - /* A hint displayed in the Saved Posts section of the Reader. The '[bookmark-outline]' placeholder will be replaced by an icon at runtime – please leave that string intact. */ "Tap [bookmark-outline] to save a post to your list." = "Tap [bookmark-outline] to save a post to your list."; @@ -9171,12 +9253,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Accessibility hint */ "Tap to select the previous period" = "Tap to select the previous period"; -/* Accessibility hint for button used to switch site */ -"Tap to switch to another site, or add a new site" = "Tap to switch to another site, or add a new site"; - -/* Accessibility hint for media item preview for user's viewing an item in their media library */ -"Tap to view media in full screen" = "Tap to view media in full screen"; - /* Accessibility hint for a button that opens a new view with more details. */ "Tap to view more details." = "Tap to view more details."; @@ -9204,6 +9280,9 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Create site, step 1. Select type of site. Title */ "Tell us what kind of site you'd like to make" = "Tell us what kind of site you'd like to make"; +/* Title for placeholder in Tenor picker */ +"tenor.welcomeMessage" = "Search to find GIFs to add to your Media Library!"; + /* The underlined title sentence */ "Terms and Conditions" = "Terms and Conditions"; @@ -9273,9 +9352,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Title for a threat that includes the file name of the file */ "The file %1$@ contains a malicious code pattern" = "The file %1$@ contains a malicious code pattern"; -/* Message shown when a GIF failed to load while trying to add it to the Media library. */ -"The GIF could not be added to the Media Library." = "The GIF could not be added to the Media Library."; - /* Description shown when a user logs in with Google but no matching WordPress.com account is found */ "The Google account \"%@\" doesn't match any account on WordPress.com" = "The Google account \"%@\" doesn't match any account on WordPress.com"; @@ -9340,7 +9416,8 @@ Example: given a notice format "Following %@" and empty site name, this will be /* No comment provided by engineer. */ "The site at %@ uses WordPress %@. We recommend to update to the latest version, or at least %@" = "The site at %1$@ uses WordPress %2$@. We recommend to update to the latest version, or at least %3$@"; -/* Error message shown a URL does not point to an existing site. */ +/* Error message shown a URL does not point to an existing site. + Error message shown when a URL does not point to an existing site. */ "The site at this address is not a WordPress site. For us to connect to it, the site must use WordPress." = "The site at this address is not a WordPress site. For us to connect to it, the site must use WordPress."; /* Message shown when site deletion API failed */ @@ -9382,7 +9459,7 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Error message shown when user attempts to remove the site owner. */ "The user you are trying to remove is the owner of this site. Please contact support for assistance." = "The user you are trying to remove is the owner of this site. Please contact support for assistance."; -/* Error message informing a user about an invalid password. */ +/* No comment provided by engineer. */ "The username or password stored in the app may be out of date. Please re-enter your password in the settings and try again." = "The username or password stored in the app may be out of date. Please re-enter your password in the settings and try again."; /* Message shown when a video failed to load while trying to add it to the Media library. */ @@ -9395,7 +9472,6 @@ Example: given a notice format "Following %@" and empty site name, this will be "Theme Activated" = "Theme Activated"; /* Noun. Name of the Themes feature - The menu item to select during a guided tour. Themes option in the blog details Title of Themes browser page */ "Themes" = "Themes"; @@ -9453,9 +9529,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* A short error message letting the user know about a problem displaying a post. */ "There was a problem displaying this post." = "There was a problem displaying this post."; -/* Error message displayed when the Media Library is unable to load a full sized preview of an item. */ -"There was a problem loading the media item." = "There was a problem loading the media item."; - /* The loading view subtitle displayed when an error occurred */ "There was a problem loading your data, refresh your page to try again." = "There was a problem loading your data, refresh your page to try again."; @@ -9468,9 +9541,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Explaining to the user there was an error trying to obtain the current location of the user. */ "There was a problem when trying to access your location. Please try again later." = "There was a problem when trying to access your location. Please try again later."; -/* Explaining to the user there was an generic error accesing media. */ -"There was a problem when trying to access your media. Please try again later." = "There was a problem when trying to access your media. Please try again later."; - /* Message for stories unknown error. */ "There was a problem with the Stories editor. If the problem persists you can contact us via the Me > Help & Support screen." = "There was a problem with the Stories editor. If the problem persists you can contact us via the Me > Help & Support screen."; @@ -9541,9 +9611,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* A description informing the user in order to proceed with this feature we will need camera permissions, and how to enable it. */ "This app needs permission to access the Camera to scan login codes, tap on the Open Settings button to enable it." = "This app needs permission to access the Camera to scan login codes, tap on the Open Settings button to enable it."; -/* Explaining to the user why the app needs access to the device media library. */ -"This app needs permission to access your device media library in order to add photos and/or video to your posts. Please change the privacy settings if you wish to allow this." = "This app needs permission to access your device media library in order to add photos and/or video to your posts. Please change the privacy settings if you wish to allow this."; - /* No comment provided by engineer. */ "This color combination may be hard for people to read. Try using a brighter background color and/or a darker text color." = "This color combination may be hard for people to read. Try using a brighter background color and/or a darker text color."; @@ -9649,9 +9716,6 @@ Example: given a notice format "Following %@" and empty site name, this will be Writing Time Format Settings Title */ "Time Format" = "Time Format"; -/* Description of a Quick Start Tour */ -"Time to finish setting up your site! Our checklist walks you through the next steps." = "Time to finish setting up your site! Our checklist walks you through the next steps."; - /* Label for the timezone setting Title for the time zone selector */ "Time Zone" = "Time Zone"; @@ -9707,9 +9771,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Message asking the user if they want to set up Jetpack from stats */ "To use stats on your site, you'll need to install the Jetpack plugin." = "To use stats on your site, you'll need to install the Jetpack plugin."; -/* Message explaining that Jetpack needs to be installed for a particular site. Reads like 'To use this app for example.com you'll need to have... */ -"To use this app for %@ you'll need to have the Jetpack plugin installed and activated." = "To use this app for %@ you'll need to have the Jetpack plugin installed and activated."; - /* Comments Today Section Header Insights 'Today' header Notifications Today Section Header */ @@ -9730,9 +9791,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Accessibility Identifier for the Aztec Unordered List Style */ "Toggles the unordered list style" = "Toggles the unordered list style"; -/* Title of the Tools section of the debug screen used in debug builds of the app */ -"Tools" = "Tools"; - /* Insights 'Top Commenters' header */ "Top Commenters" = "Top Commenters"; @@ -9743,8 +9801,7 @@ Example: given a notice format "Following %@" and empty site name, this will be /* The part of the nudge title that should be emphasized, this content needs to match a string in 'If you want to try get more...' */ "top tips" = "top tips"; -/* Shortened version of the main title to be used in back navigation - Topic page title */ +/* Shortened version of the main title to be used in back navigation */ "Topic" = "Topic"; /* Used when a Reader Topic is not found for a specific id */ @@ -9785,24 +9842,20 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Title for the traffic section in site settings screen */ "Traffic" = "Traffic"; +/* Describes a domain that was transferred from elsewhere to wordpress.com */ +"Transferred Domain" = "Transferred Domain"; + /* translators: %s: block title e.g: \"Paragraph\". */ "Transform %s to" = "Transform %s to"; /* No comment provided by engineer. */ "Transform block…" = "Transform block…"; -/* Accessibility label for trash button to delete items from the user's media library - Accessibility label for trash buttons in nav bars +/* Accessibility label for trash buttons in nav bars Trashes a comment Trashes the comment */ "Trash" = "Trash"; -/* Accessibility hint for trash button to delete items from the user's media library */ -"Trash selected media" = "Trash selected media"; - -/* Title of the trash page confirmation alert. */ -"Trash this page?" = "Trash this page?"; - /* Title of the trash confirmation alert. */ "Trash this post?" = "Trash this post?"; @@ -9851,9 +9904,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* The title of a notice telling users that the classic editor is deprecated and will be removed in a future version of the app. */ "Try the new Block Editor" = "Try the new Block Editor"; -/* Action button that will restart the login flow.Presented when logging in with a site address that does not have a valid Jetpack installation */ -"Try With Another Account" = "Try With Another Account"; - /* When social login fails, this button offers to let the user try again with a differen email address */ "Try with another email" = "Try with another email"; @@ -9908,15 +9958,9 @@ Example: given a notice format "Following %@" and empty site name, this will be /* No comment provided by engineer. */ "Type a URL" = "Type a URL"; -/* Register domain - Search field placeholder for the Suggested Domain screen */ -"Type to get more suggestions" = "Type to get more suggestions"; - /* Notice title when blocking a site fails. */ "Unable to block site" = "Unable to block site"; -/* An error message. */ -"Unable to Connect" = "Unable to Connect"; - /* Shown when a user logs in with Google but it subsequently fails to work as login to WordPress.com */ "Unable To Connect" = "Unable To Connect"; @@ -9926,9 +9970,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Title for stories unknown error. */ "Unable to Create Stories Editor" = "Unable to Create Stories Editor"; -/* Text displayed in HUD if there was an error attempting to delete a group of media items. */ -"Unable to delete all media items." = "Unable to delete all media items."; - /* Text displayed in HUD if there was an error attempting to delete a media item. */ "Unable to delete media item." = "Unable to delete media item."; @@ -9998,12 +10039,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Title of error prompt shown when a sync the user initiated fails. */ "Unable to Sync" = "Unable to Sync"; -/* Message that appears when a user tries to trash a page while their device is offline. */ -"Unable to trash pages while offline. Please try again later." = "Unable to trash pages while offline. Please try again later."; - -/* Message that appears when a user tries to trash a post while their device is offline. */ -"Unable to trash posts while offline. Please try again later." = "Unable to trash posts while offline. Please try again later."; - /* Notice title when turning site notifications off fails. */ "Unable to turn off site notifications" = "Unable to turn off site notifications"; @@ -10022,12 +10057,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Alert displayed to the user when a single post has failed to upload. */ "Unable to upload 1 draft post" = "Unable to upload 1 draft post"; -/* Alert displayed to the user when a single post and multiple files have failed to upload. */ -"Unable to upload 1 draft post, %ld files" = "Unable to upload 1 draft post, %ld files"; - -/* Alert displayed to the user when a single post and 1 file has failed to upload. */ -"Unable to upload 1 draft post, 1 file" = "Unable to upload 1 draft post, 1 file"; - /* Alert displayed to the user when a single post has failed to upload. */ "Unable to upload 1 post" = "Unable to upload 1 post"; @@ -10076,16 +10105,13 @@ Example: given a notice format "Following %@" and empty site name, this will be Button title. Reverts the previous notification operation Revert an operation Revert enabling notification after successfully subcribing to the comments for the post. - The title of an 'undo' button. Tapping the button moves a trashed page out of the trash folder. - The title of an 'undo' button. Tapping the button moves a trashed post out of the trash folder. Undo action */ "Undo" = "Undo"; /* Label of the table view cell's delete button, when unfollowing a site. */ "Unfollow" = "Unfollow"; -/* Accessibility label for unfollowing a site - Accessibility label for unfollowing a tag */ +/* Accessibility label for unfollowing a tag */ "Unfollow %@" = "Unfollow %@"; /* Title for a button that unsubscribes the user from the post. */ @@ -10101,9 +10127,6 @@ Example: given a notice format "Following %@" and empty site name, this will be User unfollowed a site. */ "Unfollowed site" = "Unfollowed site"; -/* Spoken hint describing action for selected following buttons. */ -"Unfollows blog" = "Unfollows blog"; - /* VoiceOver accessibility hint, informing the user the button can be used to unfollow a blog. */ "Unfollows the blog." = "Unfollows the blog."; @@ -10117,9 +10140,6 @@ Example: given a notice format "Following %@" and empty site name, this will be Unknown Tag Name */ "Unknown" = "Unknown"; -/* Label to use when creation date from media asset is not know. */ -"Unknown creation date" = "Unknown creation date"; - /* No comment provided by engineer. */ "Unknown error" = "Unknown error"; @@ -10285,9 +10305,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* No comment provided by engineer. */ "Uploading…" = "Uploading…"; -/* Title for alert when trying to save post with failed media items */ -"Uploads failed" = "Uploads failed"; - /* URL text field placeholder */ "URL" = "URL"; @@ -10309,9 +10326,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* The button title text for logging in with WP.com password instead of magic link. */ "Use password to sign in" = "Use password to sign in"; -/* Title of a row displayed on the debug screen used to configure the sandbox store use in the App. */ -"Use Sandbox Store" = "Use Sandbox Store"; - /* Description of a Quick Start Tour */ "Used across the web: in browser tabs, social media previews, and the WordPress.com Reader." = "Used across the web: in browser tabs, social media previews, and the WordPress.com Reader."; @@ -10350,9 +10364,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Push Authentication Alert Title */ "Verify Log In" = "Verify Log In"; -/* Notice displayed after domain credit redemption success. */ -"Verify your email address - instructions sent to %@" = "Verify your email address - instructions sent to %@"; - /* Description for the version label in the What's new page. */ "Version " = "Version "; @@ -10366,9 +10377,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Message to show when a new plugin version is available */ "Version %@ is available" = "Version %@ is available"; -/* Label displayed on video media items. */ -"video" = "video"; - /* translators: accessibility text. %s: video caption. */ "Video caption. %s" = "Video caption. %s"; @@ -10384,15 +10392,10 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Message shown if a video preview image is unavailable while the video is being uploaded. */ "Video Preview Unavailable" = "Video Preview Unavailable"; -/* Accessibility label for video thumbnails in the media collection view. The parameter is the creation date of the video. */ -"Video, %@" = "Video, %@"; - /* Period Stats 'Videos' header */ "Videos" = "Videos"; /* Button title. Displays a summary / sharing screen for a specific post. - Label for a button that opens the page when tapped. - Label for the view post button. Tapping displays the post as it appears on the web. Opens the post epilogue screen to allow sharing / viewing of a post. Theme View action title Verb. The screen title shown when viewing a post inside the app. */ @@ -10566,9 +10569,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Message for error displayed when preparing a backup fails. */ "We couldn't create your backup. Please try again later." = "We couldn't create your backup. Please try again later."; -/* Primary message shown when there are no domains that match the user entered text. */ -"We couldn't find any available address with the words you entered - let's try again." = "We couldn't find any available address with the words you entered - let's try again."; - /* Text displayed in notice after the app fails to upload a page, it will attempt to upload it later. */ "We couldn't publish this page, but we'll try again later." = "We couldn't publish this page, but we'll try again later."; @@ -10644,9 +10644,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* The subtitle text on the magic link requested screen followed by the email address. */ "We just sent a magic link to" = "We just sent a magic link to"; -/* Popup content about why this post is being opened in block editor */ -"We made big improvements to the block editor and think it's worth a try!\n\nWe enabled it for new posts and pages but if you'd like to change to the classic editor, go to 'My Site' > 'Site Settings'." = "We made big improvements to the block editor and think it's worth a try!\n\nWe enabled it for new posts and pages but if you'd like to change to the classic editor, go to 'My Site' > 'Site Settings'."; - /* Message displayed when a backup has finished */ "We successfully created a backup of your site as of %@" = "We successfully created a backup of your site as of %@"; @@ -10656,9 +10653,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Informational text about link to other tracking tools */ "We use other tracking tools, including some from third parties. Read about these and how to control them." = "We use other tracking tools, including some from third parties. Read about these and how to control them."; -/* Message explaining that WordPress was not detected. */ -"We were not able to detect a WordPress site at the address you entered. Please make sure WordPress is installed and that you are running the latest available version." = "We were not able to detect a WordPress site at the address you entered. Please make sure WordPress is installed and that you are running the latest available version."; - /* Error message displayed when an error occurred sending the magic link email. */ "We were unable to send you an email at this time. Please try again later." = "We were unable to send you an email at this time. Please try again later."; @@ -10747,9 +10741,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Instruction text after a signup Magic Link was requested. */ "We've emailed you a signup link to create your new WordPress.com account. Check your email on this device, and tap the link in the email you receive from WordPress.com." = "We've emailed you a signup link to create your new WordPress.com account. Check your email on this device, and tap the link in the email you receive from WordPress.com."; -/* Register Domain - error displayed when a domain was purchased succesfully, but there was a problem setting it to a primary domain for the site */ -"We've had problems changing the primary domain on your site — but don't worry, your domain was successfully purchased." = "We've had problems changing the primary domain on your site — but don't worry, your domain was successfully purchased."; - /* Account Settings Web Address label Header for a comment author's web address, shown when editing a comment. */ "Web Address" = "Web Address"; @@ -11032,9 +11023,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Siri Suggestion to open Support */ "WordPress Help" = "WordPress Help"; -/* Name for the WordPress Media Library */ -"WordPress Media" = "WordPress Media"; - /* Accessibility label for selecting an image or video from the user's WordPress media library on formatting toolbar. */ "WordPress Media Library" = "WordPress Media Library"; @@ -11095,6 +11083,9 @@ Example: given a notice format "Following %@" and empty site name, this will be /* This is a comma separated list of keywords used for spotlight indexing of the 'My Sites' tab. */ "wordpress, sites, site, blogs, blog" = "wordpress, sites, site, blogs, blog"; +/* Error message that describes an unknown error had occured */ +"wordpress-api.error.unknown" = "Something went wrong, please try again later."; + /* Label for WordPress.com followers */ "WordPress.com" = "WordPress.com"; @@ -11141,6 +11132,9 @@ from anywhere."; /* Title of button that displays the Automattic Work With Us web page */ "Work With Us" = "Work With Us"; +/* No comment provided by engineer. */ +"Working Offline" = "Working Offline"; + /* Accessibility label for the Stats' world map. */ "World map showing views by country." = "World map showing views by country."; @@ -11199,8 +11193,7 @@ from anywhere."; /* Title of Years stats filter. */ "Years" = "Years"; -/* Accept Action - Button title. Confirms that the user wants to proceed with a pending action. +/* Button title. Confirms that the user wants to proceed with a pending action. Label for a button that clears all old activity logs Yes */ "Yes" = "Yes"; @@ -11300,7 +11293,7 @@ from anywhere."; "You have 1 hidden WordPress site." = "You have 1 hidden WordPress site."; /* Description for the first domain purchased with a paid plan. */ -"You have a free one-year domain registration with your plan" = "You have a free one-year domain registration with your plan"; +"You have a free one-year domain registration with your plan." = "You have a free one-year domain registration with your plan."; /* Message alert when attempting to delete site with purchases */ "You have active premium upgrades on your site. Please cancel your upgrades prior to deleting your site." = "You have active premium upgrades on your site. Please cancel your upgrades prior to deleting your site."; @@ -11388,9 +11381,6 @@ from anywhere."; /* Share extension error dialog text. */ "Your account does not have permission to upload media to this site. The Site Administrator can change these permissions." = "Your account does not have permission to upload media to this site. The Site Administrator can change these permissions."; -/* Explaining to the user why the app needs access to the device media library. */ -"Your app is not authorized to access media library due to active restrictions such as parental controls. Please check your parental control settings in this device." = "Your app is not authorized to access media library due to active restrictions such as parental controls. Please check your parental control settings in this device."; - /* Title for the Jetpack Backup Complete message. */ "Your backup is now available for download" = "Your backup is now available for download"; @@ -11406,12 +11396,6 @@ from anywhere."; /* Title for the view when there aren't any Backups to display */ "Your first backup will be ready soon" = "Your first backup will be ready soon"; -/* Title of the site address section in the Domains Dashboard. */ -"Your free WordPress.com address is" = "Your free WordPress.com address is"; - -/* Error message when picked media cannot be imported into stories. */ -"Your media could not be exported. If the problem persists you can contact us via the Me > Help & Support screen." = "Your media could not be exported. If the problem persists you can contact us via the Me > Help & Support screen."; - /* Details about recently acquired domain on domain credit redemption success screen */ "Your new domain %@ is being set up. It may take up to 30 minutes for your domain to start working." = "Your new domain %@ is being set up. It may take up to 30 minutes for your domain to start working."; @@ -11427,18 +11411,12 @@ from anywhere."; /* Message of Export Content confirmation alert; substitution is user's email address */ "Your posts, pages, and settings will be mailed to you at %@." = "Your posts, pages, and settings will be mailed to you at %@."; -/* Footer of the primary site section in the Domains Dashboard. */ -"Your primary site address is what visitors will see in their address bar when visiting your website." = "Your primary site address is what visitors will see in their address bar when visiting your website."; - /* Text displayed when a site restore takes too long. */ "Your restore is taking longer than usual, please check again in a few minutes." = "Your restore is taking longer than usual, please check again in a few minutes."; /* This is shown to the user when their domain search query contains invalid characters. */ "Your search includes characters not supported in WordPress.com domains. The following characters are allowed: A–Z, a–z, 0–9." = "Your search includes characters not supported in WordPress.com domains. The following characters are allowed: A–Z, a–z, 0–9."; -/* Placeholder for site url, if the url is unknown.Presented when logging in with a site address that does not have a valid Jetpack installation.The error would read: to use this app for your site you'll need... */ -"your site" = "your site"; - /* Body text of alert helping users understand their site address */ "Your site address appears in the bar at the top of the screen when you visit your site in Safari." = "Your site address appears in the bar at the top of the screen when you visit your site in Safari."; @@ -11448,9 +11426,6 @@ from anywhere."; /* Example notification content displayed on the Enable Notifications prompt that is personalized based on a users selection. Words marked between * characters will be displayed as bold text. */ "Your site appears to be getting *more traffic* than usual!" = "Your site appears to be getting *more traffic* than usual!"; -/* Header of the domains list section in the Domains Dashboard. */ -"Your Site Domains" = "Your Site Domains"; - /* User-facing string, presented to reflect that site assembly completed successfully. */ "Your site has been created!" = "Your site has been created!"; @@ -11502,12 +11477,6 @@ from anywhere."; /* Describes the expected behavior when the user enables in-app notifications in Reader Comments. */ "You’re following this conversation. You will receive an email whenever a new comment is made." = "You’re following this conversation. You will receive an email whenever a new comment is made."; -/* Popup content about why this post is being opened in block editor */ -"You’re now using the block editor for new pages — great! If you’d like to change to the classic editor, go to ‘My Site’ > ‘Site Settings’." = "You’re now using the block editor for new pages — great! If you’d like to change to the classic editor, go to ‘My Site’ > ‘Site Settings’."; - -/* Popup content about why this post is being opened in block editor */ -"You’re now using the block editor for new posts — great! If you’d like to change to the classic editor, go to ‘My Site’ > ‘Site Settings’." = "You’re now using the block editor for new posts — great! If you’d like to change to the classic editor, go to ‘My Site’ > ‘Site Settings’."; - /* Label for button to log in using Google. The {G} will be replaced with the Google logo. */ "{G} Log in with Google." = "{G} Log in with Google."; diff --git a/WordPress/Resources/es.lproj/Localizable.strings b/WordPress/Resources/es.lproj/Localizable.strings index 8c405774ce90..c3e3bcdae385 100644 --- a/WordPress/Resources/es.lproj/Localizable.strings +++ b/WordPress/Resources/es.lproj/Localizable.strings @@ -1,4 +1,4 @@ -/* Translation-Revision-Date: 2023-10-18 16:54:08+0000 */ +/* Translation-Revision-Date: 2024-01-03 08:48:15+0000 */ /* Plural-Forms: nplurals=2; plural=n != 1; */ /* Generator: GlotPress/4.0.0-alpha.11 */ /* Language: es */ @@ -152,9 +152,6 @@ /* Difference label for Period Overview stat, indicating change from previous period. Ex: +99.9K (5%) */ "%@%@ (%@%%)" = "%1$@%2$@ (%3$@%%)"; -/* Accessibility label for a post in the post list. The parameters are the title, and date respectively. For example, \"Let it Go, 1 hour ago.\" */ -"%@, %@." = "%1$@, %2$@."; - /* Accessibility value for a Stats' Posting Activity Month if the user selected a day with posts. The first parameter is day (e.g. November 2019). The second parameter is the number of posts. */ "%@. %d posts." = "%1$@. %2$d entradas."; @@ -210,9 +207,6 @@ /* Age between dates over one year. */ "%d years" = "%d años"; -/* Max image size in pixels (e.g. 300x300px) */ -"%dx%dpx" = "%1$dx%2$d px"; - /* One menu area available in the theme */ "%i menu area in this theme" = "%i área de menú de este tema"; @@ -271,6 +265,9 @@ translators: Block name. %s: The localized block name */ /* translators: %s: social link name e.g: \"Instagram\". */ "%s social icon" = "Icono social %s"; +/* translators: displayed right after the classic block is converted to blocks. %s: The localized classic block name */ +"'%s' block converted to blocks" = "Bloque '%s' convertido en bloques"; + /* translators: Missing block alert title. %s: The localized block name */ "'%s' is not fully-supported" = "«%s» no es totalmente compatible"; @@ -478,13 +475,6 @@ translators: Block name. %s: The localized block name */ /* Label for the Activity Type filter button when there are more than 1 activity type selected */ "Activity Type (%1$d)" = "Tipo de actividad (%1$d)"; -/* Accessibility label for add button to add items to the user's media library - Remove asset from media picker list */ -"Add" = "Añadir"; - -/* Action for Media Picker to indicate selection of media. The argument in the string represents the number of elements (as numeric digits) selected */ -"Add %@" = "Añadir %@"; - /* No comment provided by engineer. */ "Add Block After" = "Añadir el bloque después"; @@ -581,9 +571,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Add menu item to children" = "Añadir elemento del menú a los elementos secundarios"; -/* Accessibility hint for add button to add items to the user's media library */ -"Add new media" = "Añadir nuevo medio"; - /* Screen reader text for Menus button that adds a new menu to a site. */ "Add new menu" = "Añadir un nuevo menú"; @@ -649,9 +636,6 @@ translators: Block name. %s: The localized block name */ /* Option to select the Airmail app when logging in with magic links */ "Airmail" = "Airmail"; -/* Description of albums in the photo libraries */ -"Albums" = "Álbumes"; - /* Image alignment option title. Title of the screen for choosing an image's alignment. */ "Alignment" = "Alineación"; @@ -665,6 +649,9 @@ translators: Block name. %s: The localized block name */ Title of the drafts filter. This filter shows a list of draft posts. */ "All" = "Todo"; +/* Information about redeeming domain credit on site dashboard. */ +"All WordPress.com annual plans include a custom domain name. Register your free domain now." = "Todos los planes anuales de WordPress.com incluyen un nombre de dominio personalizado. Registra tu dominio gratis ahora."; + /* Footer of the free domain registration section for a paid plan. Information about redeeming domain credit on site dashboard. */ "All WordPress.com plans include a custom domain name. Register your free premium domain now." = "Todos los planes de WordPress.com incluyen un nombre de dominio personalizado. Registra ahora tu dominio premium gratuito."; @@ -730,10 +717,13 @@ translators: Block name. %s: The localized block name */ "Alt Text" = "Texto Alt"; /* No comment provided by engineer. */ -"Alternatively, you can detach and edit these blocks separately by tapping “Detach patterns”." = "Alternativamente, puedes separar y editar estos bloques por separado tocando en «Separar patrones»."; +"Alternatively, you can convert the content to blocks." = "Alternativamente, puedes convertir el contenido en bloques."; /* No comment provided by engineer. */ -"Alternatively, you can detach and edit this block separately by tapping “Detach pattern”." = "También puedes pulsar en \"Separar patrón\" para separar y editar este bloque en exclusiva."; +"Alternatively, you can detach and edit this block separately by tapping “Detach”." = "También puedes tocar en «Separar» para separar y editar este bloque en exclusiva."; + +/* translators: Alternative option included in a warning related to having blocks deeply nested. */ +"Alternatively, you can flatten the content by ungrouping the block." = "También puedes aplanar el contenido desagrupando el bloque."; /* Instruction text to explain to help users type their password instead of using magic link login option. */ "Alternatively, you may enter the password for this account." = "También puedes introducir la contraseña de esta cuenta."; @@ -822,7 +812,7 @@ translators: Block name. %s: The localized block name */ /* Title for a call-to-action button in the create new bottom action sheet. Title for a call-to-action button on the prompts card. */ -"Answer Prompt" = "Responder estímulo"; +"Answer Prompt" = "Responder sugerencia"; /* Title of answered Blogging Prompts filter. */ "Answered" = "Contestada"; @@ -882,15 +872,9 @@ translators: Block name. %s: The localized block name */ /* Message prompting the user to confirm that they want to disconnect Jetpack from the site. */ "Are you sure you want to disconnect Jetpack from the site?" = "¿Estás seguro de querer desconectar Jetpack del sitio?"; -/* Message prompting the user to confirm that they want to permanently delete a group of media items. */ -"Are you sure you want to permanently delete these items?" = "¿Seguro que deseas borrar permanentemente estos elementos?"; - /* Message prompting the user to confirm that they want to permanently delete a media item. Should match Calypso. */ "Are you sure you want to permanently delete this item?" = "¿Estás seguro de que quieres eliminar permanentemente este elemento?"; -/* Message of the confirmation alert when deleting a page from the trash. */ -"Are you sure you want to permanently delete this page?" = "¿Seguro que quieres eliminar esta página permanentemente?"; - /* Message of the confirmation alert when deleting a post from the trash. */ "Are you sure you want to permanently delete this post?" = "¿Estás seguro de querer borrar permanentemente esta entrada?"; @@ -922,9 +906,6 @@ translators: Block name. %s: The localized block name */ /* Title of message shown when user taps submit for review. */ "Are you sure you want to submit for review?" = "¿Estás seguro de que quieres enviarlo para su valoración?"; -/* Message of the trash page confirmation alert. */ -"Are you sure you want to trash this page?" = "¿Seguro que quieres enviar esta página a la papelera de reciclaje?"; - /* Message of the trash confirmation alert. */ "Are you sure you want to trash this post?" = "¿Estás seguro de querer enviar a la papelera esta entrada?"; @@ -965,9 +946,6 @@ translators: Block name. %s: The localized block name */ /* translators: accessibility text. Empty Audio caption. */ "Audio caption. Empty" = "Leyenda del audio. Vacía"; -/* Accessibility label for audio items in the media collection view. The parameter is the creation date of the audio. */ -"Audio, %@" = "Audio, %@"; - /* No comment provided by engineer. */ "Authenticating" = "Autenticando"; @@ -1178,10 +1156,7 @@ translators: Block name. %s: The localized block name */ "Blocks menu" = "Menú de bloques"; /* translators: Warning related to having blocks deeply nested. %d: The deepest nesting level. */ -"Blocks nested deeper than %d levels may not render properly in the mobile editor. For this reason, we recommend flattening the content by ungrouping the block or editing the block using the web editor." = "Los bloques anidados a más profundidad de %d niveles puede que no se procesen correctamente en el editor móvil. Por este motivo, recomendamos allanar el contenido desagrupando el bloque o editando el bloque usando el editor web."; - -/* translators: Warning related to having blocks deeply nested. %d: The deepest nesting level. */ -"Blocks nested deeper than %d levels may not render properly in the mobile editor. For this reason, we recommend flattening the content by ungrouping the block or editing the block using your web browser." = "Los bloques anidados a más profundidad de %d niveles puede que no se procesen correctamente en el editor móvil. Por este motivo, recomendamos allanar el contenido desagrupando el bloque o editando el bloque usando tu navegador web."; +"Blocks nested deeper than %d levels may not render properly in the mobile editor." = "Es posible que los bloques anidados a más de %d niveles no se muestren correctamente en el editor móvil."; /* Title of a button that displays the WordPress.com blog */ "Blog" = "Blog"; @@ -1259,9 +1234,6 @@ translators: Block name. %s: The localized block name */ /* Label for the post author in the post detail. */ "By " = "Por "; -/* Accessibility label for the post author in the post list. The parameter is the author name. For example, \"By Elsa.\" */ -"By %@." = "Por %@."; - /* Legal disclaimer for logging in. The underscores _..._ denote underline. */ "By continuing, you agree to our _Terms of Service_." = "Al continuar, aceptas nuestros _términos del servicio_."; @@ -1281,8 +1253,7 @@ translators: Block name. %s: The localized block name */ /* Label for size of media while it's being calculated. */ "Calculating..." = "Calculando…"; -/* Accessibility label for taking an image or video with the camera on formatting toolbar. - Accessibility label for the camera tile in the collection view */ +/* Accessibility label for taking an image or video with the camera on formatting toolbar. */ "Camera" = "Cámara"; /* Title of an alert informing the user the camera permission for the app is disabled and its needed to proceed */ @@ -1333,10 +1304,7 @@ translators: Block name. %s: The localized block name */ Cancels an Action Cancels an alert. Cancels the mark all as read action. - Dismiss the post action sheet Editing GIF alert cancel action button. - Label for a cancel button - Label for the auto-upload cancelation button in the post list. Tapping will prevent the app from auto-uploading the post. Menus cancel button for deleting a menu. Menus cancel button within text bar while editing items. Menus: Cancel button title for canceling an edited menu item. @@ -1353,10 +1321,6 @@ translators: Block name. %s: The localized block name */ Verb. Title for Jetpack Restore cancel button. */ "Cancel" = "Cancelar"; -/* Label for the Post List option that cancels automatic uploading of a post. - Media Library option to cancel an in-progress or failed upload. */ -"Cancel Upload" = "Cancelar subida"; - /* Menus alert button title to cancel discarding changes and not createa a new menu. Menus alert button title to cancel discarding changes and not select a new menu Menus alert button title to cancel discarding changes and not select a new menu location */ @@ -1415,9 +1379,6 @@ translators: Block name. %s: The localized block name */ Main title */ "Change Password" = "Cambiar contraseña"; -/* Title of button that takes user to the system Settings section for the app */ -"Change Settings" = "Cambiar ajustes"; - /* Change Username title. */ "Change Username" = "Cambiar nombre de usuario"; @@ -1557,9 +1518,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Choose file" = "Elegir el archivo"; -/* Menu option for selecting media from the device's photo library. */ -"Choose from My Device" = "Elegir de mi dispositivo"; - /* Explanatory text for Homepage Settings homepage type selection. */ "Choose from a homepage that displays your latest posts (classic blog) or a fixed \/ static page." = "Elige entre una página de inicio que muestre tus últimas entradas (blog clásico) o una página fija \/ estática."; @@ -1760,9 +1718,6 @@ translators: Block name. %s: The localized block name */ /* Community & Non-Profit site intent topic */ "Community & Non-Profit" = "Comunitario y ONG"; -/* Accessibility indication that the current Post List style is currently Compact. */ -"Compact" = "Compacto"; - /* The action is completed */ "Completed" = "Completado"; @@ -1948,10 +1903,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Copied block" = "Bloque copiado"; -/* Copy the post url and paste anywhere in phone - Label for page copy link. Tapping copy the url of page */ -"Copy Link" = "Copiar enlace"; - /* Copy link to oomment button title */ "Copy Link to Comment" = "Copiar enlace al comentario"; @@ -2060,9 +2011,6 @@ translators: Block name. %s: The localized block name */ /* Error title displayed when unable to close user account. */ "Couldn’t close account automatically" = "No se ha podido cerrar la cuenta automáticamente"; -/* Message to show while media data source is finding the number of items available. */ -"Counting media items..." = "Contando elementos multimedia..."; - /* Period Stats 'Countries' header */ "Countries" = "Países"; @@ -2313,7 +2261,6 @@ translators: Block name. %s: The localized block name */ /* Delete Delete button title for the warning shown to the user when he refuses to re-login when the authToken is missing. Title for button that permanently deletes a media item (photo / video) - Title for button that permanently deletes one or more media items (photos / videos) Title of a delete button Title of the trash confirmation alert. */ "Delete" = "Borrar"; @@ -2321,15 +2268,11 @@ translators: Block name. %s: The localized block name */ /* Menus confirmation button for deleting a menu. */ "Delete Menu" = "Eliminar menú"; -/* Delete option in the confirmation alert when deleting a page from the trash. - Delete option in the confirmation alert when deleting a post from the trash. - Label for a button permanently deletes a page. - Label for the delete post option. Tapping permanently deletes a post. +/* Delete option in the confirmation alert when deleting a post from the trash. Title for button on the comment details page that deletes the comment when tapped. */ "Delete Permanently" = "Eliminar permanentemente"; -/* Title of the confirmation alert when deleting a page from the trash. - Title of the confirmation alert when deleting a post from the trash. */ +/* Title of the confirmation alert when deleting a post from the trash. */ "Delete Permanently?" = "¿Borrar permanentemente?"; /* Button label for deleting the current site @@ -2448,7 +2391,6 @@ translators: Block name. %s: The localized block name */ /* Accessibility label for button to dismiss a bottom sheet Accessibility label for the transparent space above the login dialog which acts as a button to dismiss the dialog. Accessibility label for the transparent space above the signup dialog which acts as a button to dismiss the dialog. - Action to show on alert when view asset fails. Dismiss a view. Verb */ "Dismiss" = "Descartar"; @@ -2466,12 +2408,6 @@ translators: Block name. %s: The localized block name */ User's Display Name */ "Display Name" = "Nombre público"; -/* Accessibility label for other media items in the media collection view. The parameter is the creation date of the document. */ -"Document, %@" = "Documento, %@"; - -/* Accessibility label for other media items in the media collection view. The parameter is the filename file. */ -"Document: %@" = "Documento: %@"; - /* Message shown when all tours have been completed */ "Doesn't it feel good to cross things off a list?" = "¿A que se siente uno bien terminando una lista?"; @@ -2632,8 +2568,7 @@ translators: Block name. %s: The localized block name */ /* Description of a Quick Start Tour */ "Draft and publish a post." = "Guarda en borrador y publica tu una entrada."; -/* Title of the drafts filter. This filter shows a list of draft posts. - Title of the drafts header in search list. */ +/* Title of the drafts filter. This filter shows a list of draft posts. */ "Drafts" = "Borradores"; /* No comment provided by engineer. */ @@ -2645,10 +2580,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Drag to adjust focal point" = "Arrastra para ajustar el punto focal"; -/* Label for page duplicate option. Tapping creates a copy of the page. - Label for post duplicate option. Tapping creates a copy of the post. */ -"Duplicate" = "Duplicar"; - /* No comment provided by engineer. */ "Duplicate block" = "Duplicar bloque"; @@ -2661,13 +2592,9 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Each block has its own settings. To find them, tap on a block. Its settings will appear on the toolbar at the bottom of the screen." = "Cada bloque tiene sus propios ajustes. Para encontrarlos, toca en un bloque. Sus ajustes aparecerán en la barra de herramientas de la parte inferior de la pantalla."; -/* Accessibility label for edit button to enable multi selection mode in the user's media library - Edit the post. - Editing GIF alert default action button. +/* Editing GIF alert default action button. Edits a Comment Edits the comment - Label for a button that opens the Edit Page view controller - Label for the edit post button. Tapping displays the editor. User action to edit media details. Verb, edit a comment */ "Edit" = "Editar"; @@ -2675,9 +2602,6 @@ translators: Block name. %s: The localized block name */ /* Title for the edit more button section */ "Edit \"More\" button" = "Editar botón «Más»"; -/* Button that displays the media editor to the user */ -"Edit %@" = "Editar %@"; - /* Blocklist Keyword Edition Title */ "Edit Blocklist Word" = "Editar palabra de la lista negra"; @@ -2870,9 +2794,6 @@ translators: Block name. %s: The localized block name */ /* Secondary message shown when there are no domains that match the user entered text. */ "Enter different words above and we'll look for an address that matches it." = "Introduce arriba distintas palabras y buscaremos una dirección que coincida."; -/* Accessibility hint for edit button to enable multi selection mode in the user's media library */ -"Enter edit mode to enable multi select to delete" = "Entra en el modo de edición para permitir la selección múltiple a la hora de borrar"; - /* (placeholder) Help enter WordPress password Placeholder of a field to type a password to protect the post. */ "Enter password" = "Escribe contraseña"; @@ -3028,9 +2949,6 @@ translators: Block name. %s: The localized block name */ /* Short title telling the user they will receive a blogging reminder every day of the week. */ "Every day at %@" = "Cada día a las %@"; -/* Label for the post author filter. This filter shows posts for all users on the blog. */ -"Everyone" = "Todo el mundo"; - /* Example story title description */ "Example story title" = "Título de la entrada de historia"; @@ -3040,9 +2958,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Excerpt length (words)" = "Longitud del extracto (palabras)"; -/* Accessibility label for a post's excerpt in the post list. The parameter is the post excerpt. For example, \"Excerpt. This is the first paragraph.\" */ -"Excerpt. %@." = "Extracto. %@."; - /* Should be the same as the text displayed if the user clicks the (i) in Calypso. */ "Excerpts are optional hand-crafted summaries of your content." = "Los extractos son resúmenes manuales opcionales de tu contenido."; @@ -3052,8 +2967,7 @@ translators: Block name. %s: The localized block name */ /* Accessibility Label for the exit full screen button on the full screen comment reply mode */ "Exit Full Screen" = "Salir de la pantalla completa"; -/* Accessibility indication that the current Post List style is currently Expanded. - Screen reader text to represent the expanded state of a UI control */ +/* Screen reader text to represent the expanded state of a UI control */ "Expanded" = "Ampliado"; /* Accessibility hint */ @@ -3103,9 +3017,6 @@ translators: Block name. %s: The localized block name */ The action failed */ "Failed" = "Fallado"; -/* Error title when picked media cannot be imported into stories. */ -"Failed Media Export" = "Exportación fallida de medios"; - /* Message for mark all as read success notice */ "Failed marking Notifications as read" = "Fallo al marcar los avisos como leídos"; @@ -3307,6 +3218,12 @@ translators: Block name. %s: The localized block name */ /* An example tag used in the login prologue screens. */ "Football" = "Fútbol"; +/* translators: Recommendation included in a warning related to having blocks deeply nested. */ +"For this reason, we recommend editing the block using the web editor." = "Por este motivo, te recomendamos que edites el bloque utilizando el editor web."; + +/* translators: Recommendation included in a warning related to having blocks deeply nested. */ +"For this reason, we recommend editing the block using your web browser." = "Por este motivo, te recomendamos que edites el bloque utilizando tu navegador web."; + /* Register Domain - Domain contact information section header description */ "For your convenience, we have pre-filled your WordPress.com contact information. Please review to be sure it’s the correct information you want to use for this domain." = "Para tu comodidad, hemos rellenado previamente tu información de contacto de WordPress.com. Por favor, revísala para asegurarte de que es la información correcta que deseas utilizar para este dominio."; @@ -3624,8 +3541,7 @@ translators: Block name. %s: The localized block name */ "Home" = "Inicio"; /* Title for setting which shows the current page assigned as a site's homepage - Title for the homepage section in site settings screen - Title of the Homepage Badge */ + Title for the homepage section in site settings screen */ "Homepage" = "Página de inicio"; /* Label for Homepage Settings site settings section @@ -3722,9 +3638,6 @@ translators: Block name. %s: The localized block name */ /* Hint for image title on image settings. */ "Image title" = "Título de la imagen"; -/* Accessibility label for image thumbnails in the media collection view. The parameter is the creation date of the image. */ -"Image, %@" = "Imagen: %@"; - /* Undated post time label */ "Immediately" = "Inmediatamente"; @@ -3753,7 +3666,7 @@ translators: Block name. %s: The localized block name */ "Inactive, Autoupdates on" = "Inactivo, actualizaciones automáticas activas"; /* Title of the switch to turn on or off the blogging prompts feature. */ -"Include a Blogging Prompt" = "Incluir un estímulo para bloguear"; +"Include a Blogging Prompt" = "Incluir una sugerencia de publicación"; /* Describes a standard *.wordpress.com site domain */ "Included with Site" = "Incluido con el sitio"; @@ -3862,7 +3775,7 @@ translators: Block name. %s: The localized block name */ "Interior Design" = "Diseño de interiores"; /* Title displayed on the feature introduction view. */ -"Introducing Blogging Prompts" = "Presentamos los estímulos para bloguear"; +"Introducing Blogging Prompts" = "Presentamos las sugerencias de publicación"; /* Stories intro header title */ "Introducing Story Posts" = "Presentando las entradas de historias"; @@ -4097,7 +4010,7 @@ translators: Block name. %s: The localized block name */ /* Accessibility label for the blogging prompts info button on the Blogging Reminders Settings screen. Accessibility label for the blogging prompts info button on the prompts header view. */ -"Learn more about prompts" = "Más información sobre los estímulos"; +"Learn more about prompts" = "Más información sobre las sugerencias"; /* Footer text for Invite People role field. */ "Learn more about roles" = "Saber más sobre los perfiles"; @@ -4210,9 +4123,6 @@ translators: Block name. %s: The localized block name */ Settings: Comments Approval settings */ "Links in comments" = "Enlaces en los comentarios"; -/* The accessibility label for the list style button in the Post List. */ -"List style" = "Estilo de lista"; - /* Title of the screen that load selected the revisions. */ "Load" = "Cargar"; @@ -4228,18 +4138,12 @@ translators: Block name. %s: The localized block name */ /* Text displayed while loading the activity feed for a site */ "Loading Backups..." = "Cargando copias de seguridad..."; -/* Phrase to show when the user has searched for GIFs and they are being loaded. */ -"Loading GIFs..." = "Cargando GIFs…"; - /* Menus label text displayed while menus are loading */ "Loading Menus..." = "Cargando menús…"; /* Text displayed while loading site People. */ "Loading People..." = "Cargando personas…"; -/* Phrase to show when the user has searched for images and they are being loaded. */ -"Loading Photos..." = "Cargando fotos…"; - /* Text displayed while loading plans details */ "Loading Plan..." = "Cargando plan..."; @@ -4274,7 +4178,7 @@ translators: Block name. %s: The localized block name */ "Loading plugins..." = "Cargando plugins…"; /* Displayed while blogging prompts are being loaded. */ -"Loading prompts..." = "Cargando estímulos…"; +"Loading prompts..." = "Cargando sugerencias…"; /* A short message to inform the user the requested stream is being loaded. */ "Loading stream..." = "Cargando hilo…"; @@ -4300,8 +4204,7 @@ translators: Block name. %s: The localized block name */ /* Local Services site intent topic */ "Local Services" = "Servicios locales"; -/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. - Title of the Local Changes Badge */ +/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. */ "Local changes" = "Cambios locales"; /* Title for alert when a generic error happened when trying to find the location of the device */ @@ -4465,7 +4368,6 @@ translators: Block name. %s: The localized block name */ "Max Video Upload Size" = "Tamaño máximo de subida de vídeo"; /* Accessibility label for the Me button in My Site. - Label for the post author filter. This filter shows posts only authored by the current user. Me page title Noun. Title. Links to the Me screen. The accessibility value of the me tab. @@ -4473,9 +4375,7 @@ translators: Block name. %s: The localized block name */ "Me" = "Yo"; /* Noun. Title. Links to the blog's Media library. - Tab bar title for the Media tab in Media Picker The menu item to select during a guided tour. - Title for Media Library section of the app. Title for the media section in site settings screen Title label for the media settings section in the app settings */ "Media" = "Medios"; @@ -4487,13 +4387,6 @@ translators: Block name. %s: The localized block name */ /* Label for size of media cache in the app. */ "Media Cache Size" = "Tamaño de la caché de medios"; -/* Title for alert when access to media capture is not granted */ -"Media Capture" = "Captura de multimedia"; - -/* Title for alert when a generic error happened when loading media - Title for alert when access to the media library is not granted by the user */ -"Media Library" = "Librería multimedia"; - /* Title for action sheet with media options. */ "Media Options" = "Opciones de medios"; @@ -4516,9 +4409,6 @@ translators: Block name. %s: The localized block name */ /* translators: %s: block title e.g: \"Paragraph\". */ "Media options" = "Opciones de medios"; -/* Alert title when there is issues loading an asset to preview. */ -"Media preview failed." = "Fallo en la vista previa del medio."; - /* Alert displayed to the user when multiple media items have uploaded successfully. */ "Media uploaded (%ld files)" = "Medios subidos (%ld archivos)"; @@ -4556,9 +4446,6 @@ translators: Block name. %s: The localized block name */ Label for the share message field on the post settings. */ "Message" = "Mensaje"; -/* Title of section containing image / video metadata such as size and file type */ -"Metadata" = "Metadatos"; - /* Option to select the Microsft Outlook app when logging in with magic links */ "Microsoft Outlook" = "Microsoft Outlook"; @@ -4578,13 +4465,11 @@ translators: Block name. %s: The localized block name */ "Months and Years" = "Meses y años"; /* Accessibility label for more button in dashboard quick start card. - Accessibility label for the More button in Page List. Accessibility label for the More button in Post List (compact view). Accessibility label for the More button on formatting toolbar. Accessibility label for the More button on Reader Cell Accessibility label for the More button on Reader's post details - Action button to display more available options - Label for the more post button. Tapping displays an action sheet with post options. */ + Action button to display more available options */ "More" = "Más"; /* Action button to display more available options @@ -4642,15 +4527,8 @@ translators: Block name. %s: The localized block name */ /* Screen reader text for button that will move the menu item */ "Move menu item" = "Mover elemento del menú"; -/* Label for a button that moves a page to the draft folder - Label for an option that moves a post to the draft folder */ -"Move to Draft" = "Mover a borradores"; - -/* Label for a button that moves a page to the trash folder - Label for a option that moves a post to the trash folder - Title for button on the comment details page that moves the comment to trash when tapped. +/* Title for button on the comment details page that moves the comment to trash when tapped. Trash option in the trash confirmation alert. - Trash option in the trash page confirmation alert. Trashes the comment */ "Move to Trash" = "Mover a la papelera"; @@ -4682,7 +4560,8 @@ translators: Block name. %s: The localized block name */ Title of My Site tab */ "My Site" = "Mi sitio"; -/* Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ +/* Title for site picker screen. + Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ "My Sites" = "Mis sitios"; /* Siri Suggestion to open My Sites */ @@ -4932,9 +4811,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title for the view when there aren't any Activities to display in the Activity Log for a given filter. */ "No matching events found." = "No se han encontrado eventos coincidentes."; -/* Message displayed when no results are returned from a media library search. Should match Calypso. - Phrase to show when the user search for images but there are no result to show. - Phrase to show when the user searches for GIFs but there are no result to show. */ +/* Message displayed when no results are returned from a media library search. Should match Calypso. */ "No media matching your search" = "No hay medios que coincidan con tu búsqueda"; /* Menus text shown when no menus were available for loading the Menus editor. */ @@ -4952,8 +4829,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Displayed in the Notifications Tab as a title, when there are no notifications */ "No notifications yet" = "Aún no hay notificaciones"; -/* Displayed when the user is searching the pages list and there are no matching pages - Text displayed when there's no matching with the text search */ +/* Text displayed when there's no matching with the text search */ "No pages matching your search" = "Ninguna página coincide con tu búsqueda"; /* Text displayed when search for plugins returns no results */ @@ -4974,9 +4850,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Message shown whent the reader finds no posts for the chosen tag */ "No posts have been made recently with this tag." = "No se han creado entradas recientemente con esta etiqueta."; -/* Displayed when the user is searching the posts list and there are no matching posts */ -"No posts matching your search" = "Ninguna entrada coincide con tu búsqueda"; - /* Accessibility value for a Stats' Posting Activity Month if there are no posts. */ "No posts." = "No hay entradas."; @@ -4987,7 +4860,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "No primary site address found" = "No se ha encontrado ninguna dirección del sitio principal"; /* Title displayed when there are no blogging prompts to display. */ -"No prompts yet" = "Todavía no hay estímulos"; +"No prompts yet" = "Todavía no hay sugerencias"; /* A message title */ "No recent posts" = "No hay entradas recientes"; @@ -5077,9 +4950,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* A message title */ "Nothing liked yet" = "Aún no hay ningún «Me gusta»"; -/* Default message for empty media picker */ -"Nothing to show" = "Nada que mostrar"; - /* Notifications Details Accessibility Identifier */ "Notification Details Table" = "Tabla de detalles de notificación"; @@ -5139,7 +5009,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Alert dismissal title Button title. Acknowledges a prompt. Button title. An acknowledgement of the message displayed in a prompt. - Confirmation of action Default action Dismisses the alert Marks all notifications as read. @@ -5201,9 +5070,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* No comment provided by engineer. */ "Only show excerpt" = "Mostrar solo el extracto"; -/* Message telling the user that they've only enabled limited photo library permissions for the app. */ -"Only the selected photos you've given access to are available." = "Solo están disponibles las fotos seleccionadas a las que hayas dado acceso."; - /* An informal exclaimation that means `something went wrong`. Title for the view when there's an error loading a comment. Title for the view when there's an error loading Activity Log @@ -5238,9 +5104,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title of a button that opens the apps settings in the system Settings.app */ "Open Settings" = "Abrir configuración"; -/* Editor button to swich the media picker from quick mode to full picker */ -"Open full media picker" = "Abrir selector completo de multimedia"; - /* No comment provided by engineer. */ "Open in Safari" = "Abrir en Safari"; @@ -5280,6 +5143,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Divider on initial auth view separating auth options. */ "Or" = "O"; +/* Instruction text for other forms of two-factor auth methods. */ +"Or choose another form of authentication." = "O elige otra forma de identificación."; + /* Label for button to log in using site address. Underscores _..._ denote underline. */ "Or log in by _entering your site address_." = "O accede _introduciendo la dirección de tu sitio_."; @@ -5338,15 +5204,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title shown when selecting a post type of Page from the Share Extension. */ "Page" = "Página"; -/* Prompts the user that a restored page was moved to the drafts list. */ -"Page Restored to Drafts" = "Se ha restaurado la página en Borradores"; - -/* Prompts the user that a restored page was moved to the published list. */ -"Page Restored to Published" = "Se ha restaurado la página en Publicado"; - -/* Prompts the user that a restored page was moved to the scheduled list. */ -"Page Restored to Scheduled" = "Se ha restaurado la página en Programado"; - /* Name of the button to open the page settings The title of the Page Settings screen. */ "Page Settings" = "Ajustes de la página"; @@ -5363,9 +5220,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of notification displayed when a page has failed to upload. */ "Page failed to upload" = "Página que falló al subirse"; -/* A short message explaining that a page was moved to the trash bin. */ -"Page moved to trash." = "Se ha enviado la página a la papelera."; - /* Title of notification displayed when a page has been successfully saved as a draft. */ "Page pending review" = "Página pendiente de revisión"; @@ -5437,8 +5291,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title of pending Comments filter. */ "Pending" = "Pendiente"; -/* Name for the status of a post pending review. - Title of the Pending Review Badge */ +/* Name for the status of a post pending review. */ "Pending review" = "Pendiente de revisión"; /* Noun. Title of the people management feature. @@ -5467,12 +5320,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Photography site intent topic */ "Photography" = "Fotografía"; -/* Tab bar title for the Photos tab in Media Picker */ -"Photos" = "Fotos"; - -/* Subtitle for placeholder in Free Photos. The company name 'Pexels' should always be written as it is. */ -"Photos provided by Pexels" = "Fotos proporcionadas por Pexels"; - /* Title for selecting a new username in the site creation flow. */ "Pick username" = "Elige un nombre de usuario"; @@ -5565,7 +5412,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Please enter the password for your WordPress.com account to log in with your Apple ID." = "Por favor, introduce la contraseña de tu cuenta en WordPress.com para acceder con tu ID de Apple."; /* Instruction text on the two-factor screen. */ -"Please enter the verification code from your authenticator app, or tap the link below to receive a code via SMS." = "Por favor, introduce el código de verificación de tu aplicación de identificación o toca el siguiente enlace para recibir un código por SMS."; +"Please enter the verification code from your authenticator app." = "Por favor, introduce el código de verificación de tu aplicación Authenticator."; /* Popup message to ask for user credentials (fields shown below). */ "Please enter your credentials" = "Introduce tus credenciales"; @@ -5660,15 +5507,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ The post formats available for the post. Should be the same as in core WP. */ "Post Format" = "Formato de entrada"; -/* Prompts the user that a restored post was moved to the drafts list. */ -"Post Restored to Drafts" = "Se ha restaurado la entrada a borradores"; - -/* Prompts the user that a restored post was moved to the published list. */ -"Post Restored to Published" = "Se ha restaurado la entrada a publicado"; - -/* Prompts the user that a restored post was moved to the scheduled list. */ -"Post Restored to Scheduled" = "Se ha restaurado la entrada a programado"; - /* Name of the button to open the post settings The title of the Post Settings screen. */ "Post Settings" = "Ajustes de la entrada"; @@ -5688,9 +5526,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of notification displayed when a post has failed to upload. */ "Post failed to upload" = "Entrada que falló al subirse"; -/* A short message explaining that a post was moved to the trash bin. */ -"Post moved to trash." = "Se ha enviado la entrada a la papelera."; - /* Title of notification displayed when a post has been successfully saved as a draft. */ "Post pending review" = "Entrada pendiente de revisión"; @@ -5749,9 +5584,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Period Stats 'Posts and Pages' header */ "Posts and Pages" = "Entradas y páginas"; -/* Title of the Posts Page Badge */ -"Posts page" = "Página de entradas"; - /* Message informing the user that their static homepage for posts was set successfully */ "Posts page successfully updated" = "Página de entradas actualizada correctamente"; @@ -5764,9 +5596,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* A message explaining the Posts I Like feature in the reader */ "Posts that you like will appear here." = "Aquí aparecerán las entradas que te gusten."; -/* Subtitle for placeholder in Tenor picker. `The company name 'Tenor' should always be written as it is. */ -"Powered by Tenor" = "Funciona con Tenor"; - /* Browse premium themes selection title */ "Premium" = "Premium"; @@ -5785,18 +5614,12 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title for screen to preview a static content. */ "Preview" = "Vista previa"; -/* Action for Media Picker to preview the selected media items. The argument in the string represents the number of elements (as numeric digits) selected */ -"Preview %@" = "Vista previa %@"; - /* Title for web preview device switching button */ "Preview Device" = "Vista previa del dispositivo"; /* Title on display preview error */ "Preview Unavailable" = "Vista previa no disponible"; -/* Accessibility label for media item preview for user's viewing an item in their media library */ -"Preview media" = "Previsualizar medios"; - /* No comment provided by engineer. */ "Preview page" = "Previsualizar la página"; @@ -5843,8 +5666,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Privacy notice for California users" = "Aviso de privacidad para usuarios de California"; /* Name for the status of a post that is marked private. - Privacy setting for posts set to 'Private'. Should be the same as in core WP. - Title of the Private Badge */ + Privacy setting for posts set to 'Private'. Should be the same as in core WP. */ "Private" = "Privada"; /* No comment provided by engineer. */ @@ -5872,12 +5694,12 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Projects" = "Proyectos"; /* Title of the notification presented when a prompt is skipped */ -"Prompt skipped" = "Se ha omitido el estímulo"; +"Prompt skipped" = "Se ha omitido la sugerencia"; /* Title label for blogging prompts in the create new bottom action sheet. Title label for the Prompts card in My Sites tab. View title for Blogging Prompts list. */ -"Prompts" = "Estímulos"; +"Prompts" = "Sugerencias"; /* Privacy setting for posts set to 'Public' (default). Should be the same as in core WP. */ "Public" = "Pública"; @@ -5894,12 +5716,10 @@ translators: %s: Select control button label e.g. \"Button width\" */ Label for the publish date button. */ "Publish Date" = "Fecha de publicación"; -/* A short phrase indicating a post is due to be immedately published. - Label for a button that moves a page to the published folder, publishing with the current date/time. */ +/* A short phrase indicating a post is due to be immedately published. */ "Publish Immediately" = "Publicar Inmediatamente"; /* Label for a button that publishes the post - Label for an option that moves a publishes a post immediately Title of button allowing the user to immediately publish the post they are editing. */ "Publish Now" = "Publicar ahora"; @@ -5917,8 +5737,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Name for the status of a published post. Period Stats 'Published' header - Title of the published filter. This filter shows a list of posts that the user has published. - Title of the published header in search list. */ + Title of the published filter. This filter shows a list of posts that the user has published. */ "Published" = "Publicadas"; /* Precedes the name of the blog just posted on */ @@ -6022,7 +5841,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Redo" = "Rehacer"; /* Label for link title in Referrers stat. */ -"Referrer" = "Referencia"; +"Referrer" = "Remitente"; /* Period Stats 'Referrers' header */ "Referrers" = "Referencias"; @@ -6060,8 +5879,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of the completion screen of the Blogging Reminders Settings screen when the reminders are removed. */ "Reminders removed" = "Recordatorios eliminados"; -/* Add asset to media picker list - Alert button to confirm a plugin to be removed +/* Alert button to confirm a plugin to be removed Button label when removing a blog Label for the Remove Feature Image icon. Tapping will show a confirmation screen for removing the feature image from the post. Remove Action @@ -6214,9 +6032,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of secondary button on alert prompting verify their accounts while attempting to publish */ "Resend" = "Reenviar"; -/* Title of the reset button */ -"Reset" = "Restablecer"; - /* Accessibility label for the reset activity type button */ "Reset Activity Type filter" = "Restablecer filtro de tipo de actividad"; @@ -6271,12 +6086,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ Button title, displayed when media has failed to upload. Allows the user to try the upload again. Button title. Retries uploading a post. If a user taps the button with this label, the action that evinced this error view will be retried. - Label for a button that attempts to re-upload a page that previously failed to upload. - Label for the retry post upload button. Tapping attempts to upload the post again. Opens the media library . Retries the upload of a user's post. Retry updating User's Role - Retry uploading the post. Retry. Action Retry. Verb – retry a failed media upload. The Jetpack view button title used when an error occurred @@ -6288,9 +6100,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Button title that triggers a scan */ "Retry Scan" = "Reintentar el análisis"; -/* User action to retry media upload. */ -"Retry Upload" = "Reintentar la subida"; - /* User action to retry all failed media uploads. */ "Retry all" = "Reintentar todo"; @@ -6388,9 +6197,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Accessibility label for the 'Save Post' button when a post has been saved. */ "Saved Post" = "Entrada guardada"; -/* Text displayed in HUD when a media item's metadata (title, etc) is saved successfully. */ -"Saved!" = "¡Guardado!"; - /* Accessibility hint for the 'Save Post' button. */ "Saves this post for later." = "Guarda esta entrada para más tarde."; @@ -6401,7 +6207,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Saving post…" = "Guardando entrada..."; /* Menus save button title while it is saving a Menu. - Text displayed in HUD while a media item's metadata (title, etc) is being saved. Text displayed in HUD while a post is being saved as a draft. */ "Saving..." = "Guardando..."; @@ -6492,21 +6297,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* No comment provided by engineer. */ "Search or type URL" = "Busca o escribe la URL"; -/* Text displayed when the search controller will be presented */ -"Search pages" = "Buscar páginas"; - -/* Text displayed when the search controller will be presented */ -"Search posts" = "Buscar entradas"; - /* No comment provided by engineer. */ "Search settings" = "Ajustes de búsqueda"; -/* Title for placeholder in Tenor picker */ -"Search to find GIFs to add to your Media Library!" = "¡Busca para encontrar fotos gratuitas que añadir a tu biblioteca de medios!"; - -/* Title for placeholder in Free Photos */ -"Search to find free photos to add to your Media Library!" = "¡Busca para descubrir fotos gratuitas que añadir a tu biblioteca de medios!"; - /* Menus search bar placeholder text. */ "Search..." = "Buscar…"; @@ -6577,9 +6370,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Register Domain - Domain contact information field placeholder for Country */ "Select Country" = "Seleccionar país"; -/* Title of button that allows the user to select more photos to access within the app */ -"Select More" = "Seleccionar más"; - /* Blog Picker's Title */ "Select Site" = "Seleccionar sitio"; @@ -6601,9 +6391,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Register domain - Title for the Choose domain button of Suggested domains screen */ "Select domain" = "Seleccionar el dominio"; -/* Accessibility hint for actions when displaying media items. */ -"Select media." = "Elige medios."; - /* Accessibility label for selecting paragraph style button on formatting toolbar. */ "Select paragraph style" = "Elegir el estilo del párrafo"; @@ -6707,19 +6494,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label for connected service in Publicize stat. */ "Service" = "Servicio"; -/* Label for a button that opens the Set Parent options view controller - Navigation title displayed on the navigation bar */ +/* Navigation title displayed on the navigation bar */ "Set Parent" = "Establecer como padre"; /* No comment provided by engineer. */ "Set as Featured Image" = "Establecer como imagen destacada"; -/* Label for a button that sets the selected page as the site's Homepage */ -"Set as Homepage" = "Configurar como página de inicio"; - -/* Label for a button that sets the selected page as the site's Posts page */ -"Set as Posts Page" = "Configurar como página de entradas"; - /* Notice confirming that an image has been set as the post's featured image. */ "Set as featured image" = "Establecer como imagen destacada"; @@ -6763,7 +6543,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Accessibility label for share buttons in nav bars Button label to share a post Button label to share a web page - Share the post. Shares the comment URL Spoken accessibility label Title for a button that allows the user to share their answer to the prompt. @@ -7149,8 +6928,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Name of setting configured when a site uses a static page as its homepage */ "Static Homepage" = "Página de inicio estática"; -/* Label for post stats option. Tapping displays statistics for a post. - Noun. Abbreviation of Statistics. Name of the Stats feature +/* Noun. Abbreviation of Statistics. Name of the Stats feature Noun. Abbv. of Statistics. Links to a blog's Stats screen. Stats 3D Touch Shortcut Stats window title @@ -7181,9 +6959,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label text that defines a post marked as sticky */ "Sticky" = "Fija"; -/* Accessibility label for a sticky post in the post list. */ -"Sticky." = "Fija."; - /* User action to stop upload. */ "Stop upload" = "Detener subida"; @@ -7240,7 +7015,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ Theme Support action title */ "Support" = "Soporte"; -/* Button used to switch site */ +/* Title for back button that leads to the site picker screen. */ "Switch Site" = "Cambiar de sitio"; /* Switches the Editor to HTML Mode */ @@ -7328,9 +7103,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label explaining why users might want to add tags. */ "Tags help tell readers what a post is about. Separate different tags with commas." = "Las etiquetas ayudan a decir a los lectores de qué trata una entrada. Separa las diferentes etiquetas con comas."; -/* Menu option for taking an image or video with the device's camera. */ -"Take Photo or Video" = "Toma una foto o vídeo"; - /* No comment provided by engineer. */ "Take a Photo" = "Haz una foto"; @@ -7381,7 +7153,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "Tap to hide the keyboard" = "Toca para ocultar el teclado"; /* Title for a push notification with fixed content that invites the user to load today's blogging prompt. */ -"Tap to load today's prompt..." = "Toca para cargar el estímulo de hoy…"; +"Tap to load today's prompt..." = "Toca para cargar la sugerencia de hoy…"; /* Accessibility hint for referrer action row. */ "Tap to mark referrer as not spam." = "Toca para marcar al referente como no spam."; @@ -7401,12 +7173,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Accessibility hint */ "Tap to select the previous period" = "Toca para seleccionar el periodo anterior"; -/* Accessibility hint for button used to switch site */ -"Tap to switch to another site, or add a new site" = "Toca para cambiar a otro sitio o para añadir un nuevo sitio"; - -/* Accessibility hint for media item preview for user's viewing an item in their media library */ -"Tap to view media in full screen" = "Toca para ver los medios a pantalla completa"; - /* Accessibility hint for a button that opens a new view with more details. */ "Tap to view more details." = "Toca para ver más detalles."; @@ -7452,10 +7218,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* No comment provided by engineer. */ "Text formatting controls are located within the toolbar positioned above the keyboard while editing a text block" = "Los controles de formato de texto están dentro de la barra de herramientas situada encima del teclado mientras editas un bloque de texto"; -/* Button title - The button's title text to send a 2FA code via SMS text message. */ +/* Button title */ "Text me a code instead" = "Envía un código por mensaje de texto"; +/* The button's title text to send a 2FA code via SMS text message. */ +"Text me a code via SMS" = "Envíame un texto con un código mediante SMS"; + /* Message of alert when theme activation succeeds */ "Thanks for choosing %@ by %@" = "Gracias por elegir %1$@ de %2$@"; @@ -7483,9 +7251,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Error message shown to a user who is trying to share to Facebook but does not have any available Facebook Pages. */ "The Facebook connection cannot find any Pages. Publicize cannot connect to Facebook Profiles, only published Pages." = "La conexión con Facebook no puede encontrar ninguna página. Difundir no puede conectar con perfiles de Facebook, solo con páginas publicadas."; -/* Message shown when a GIF failed to load while trying to add it to the Media library. */ -"The GIF could not be added to the Media Library." = "No se pudo añadir el GIF a la biblioteca de medios."; - /* Description shown when a user logs in with Google but no matching WordPress.com account is found */ "The Google account \"%@\" doesn't match any account on WordPress.com" = "La cuenta de Google «%@» no coincide con ninguna cuenta de WordPress.com"; @@ -7515,7 +7280,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "The basics" = "Lo básico"; /* Subtitle displayed on the feature introduction view. */ -"The best way to become a better writer is to build a writing habit and share with others - that’s where Prompts come in!" = "El mejor modo de convertirte en un mejor escritor es crear un hábito de escritura y compartir con otros - ¡aquí es donde entran los estímulos! "; +"The best way to become a better writer is to build a writing habit and share with others - that’s where Prompts come in!" = "El mejor modo de convertirte en un mejor escritor es crear un hábito de escritura y compartir con otros - ¡aquí es donde entran las sugerencias!"; /* No comment provided by engineer. */ "The certificate for this server is invalid. You might be connecting to a server that is pretending to be “%@” which could put your confidential information at risk.\n\nWould you like to trust the certificate anyway?" = "El certificado para este servidor no es válido. Puede que estés conectando con un servidor que aparenta ser «%@», lo que podría poner en peligro tu información confidencial.\n\n¿Deseas confiar en el certificado de todos modos?"; @@ -7613,7 +7378,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Error message shown when user attempts to remove the site owner. */ "The user you are trying to remove is the owner of this site. Please contact support for assistance." = "El usuario que estás intentando eliminar es el propietario de este sitio. Por favor contacta con soporte para que te ayuden."; -/* Error message informing a user about an invalid password. */ +/* No comment provided by engineer. */ "The username or password stored in the app may be out of date. Please re-enter your password in the settings and try again." = "El usuario o contraseña guardados pueden estar obsoletos. Introduce tu contraseña de nuevo en ajustes y prueba otra vez."; /* Message shown when a video failed to load while trying to add it to the Media library. */ @@ -7681,9 +7446,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* A short error message letting the user know about a problem displaying a post. */ "There was a problem displaying this post." = "Hubo un problema mostrando esta entrada."; -/* Error message displayed when the Media Library is unable to load a full sized preview of an item. */ -"There was a problem loading the media item." = "Hubo un problema al cargar el elemento multimedia."; - /* The loading view subtitle displayed when an error occurred */ "There was a problem loading your data, refresh your page to try again." = "Hubo un problema al cargar tus datos, recarga tu página para intentarlo de nuevo."; @@ -7696,9 +7458,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Explaining to the user there was an error trying to obtain the current location of the user. */ "There was a problem when trying to access your location. Please try again later." = "Hubo un problema al intentar acceder a tu localización. Por favor, prueba de nuevo."; -/* Explaining to the user there was an generic error accesing media. */ -"There was a problem when trying to access your media. Please try again later." = "Se ha producido un problema al intentar acceder a tus elementos multimedia. Inténtalo de nuevo más tarde."; - /* Message for stories unknown error. */ "There was a problem with the Stories editor. If the problem persists you can contact us via the Me > Help & Support screen." = "Hubo un problema al tratar de seguir el sitio. Si persiste el problema puedes contactarnos desde la pantalla Yo > Ayuda y soporte"; @@ -7716,7 +7475,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "There was an error loading plugins" = "Hubo un error al cargar los plugins"; /* Text displayed when there is a failure loading blogging prompts. */ -"There was an error loading prompts." = "Se ha producido un error al cargar los estímulos."; +"There was an error loading prompts." = "Se ha producido un error al cargar las sugerencias."; /* Text displayed when there is a failure loading a comment. */ "There was an error loading the comment." = "Se ha producido un error al cargar el comentario."; @@ -7769,9 +7528,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* A description informing the user in order to proceed with this feature we will need camera permissions, and how to enable it. */ "This app needs permission to access the Camera to scan login codes, tap on the Open Settings button to enable it." = "Esta aplicación requiere permiso para acceder a la cámara para escanear códigos de acceso. Toca en el botón Abrir la configuración para habilitarlo."; -/* Explaining to the user why the app needs access to the device media library. */ -"This app needs permission to access your device media library in order to add photos and\/or video to your posts. Please change the privacy settings if you wish to allow this." = "Esta aplicación necesita permiso para acceder a la biblioteca multimedia del dispositivo con el fin de añadir fotos y vídeos en tus entradas. Cambia la configuración de privacidad si deseas permitir esta acción."; - /* No comment provided by engineer. */ "This color combination may be hard for people to read. Try using a brighter background color and\/or a darker text color." = "Esta combinación de colores puede dificultar la lectura. Prueba a utilizar un color de fondo más brillante o un color de texto más oscuro."; @@ -7881,6 +7637,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Description of a Quick Start Tour */ "Time to finish setting up your site! Our checklist walks you through the next steps." = "¡Hora de terminar de configurar tu sitio! Nuestra lista de tareas te guía por los siguientes pasos."; +/* Error when the uses takes more than 1 minute to submit a security key. */ +"Time's up, but don't worry, your security is our priority. Please try again!" = "Se acabó el tiempo, pero no te preocupes, tu seguridad es nuestra prioridad. ¡Vuelve a intentarlo!"; + /* WordPress.com Marketing Footer Text */ "Tips for getting the most out of WordPress.com." = "Trucos para sacar el máximo provecho a WordPress.com"; @@ -7938,7 +7697,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "Today" = "Hoy"; /* Title for a push notification showing today's blogging prompt. */ -"Today's Prompt 💡" = "Estímulo de hoy 💡"; +"Today's Prompt 💡" = "Sugerencia de hoy 💡"; /* Insights Management 'Today's Stats' title */ "Today's Stats" = "Estadísticas de hoy"; @@ -8004,24 +7763,20 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title for the traffic section in site settings screen */ "Traffic" = "Tráfico"; +/* Describes a domain that was transferred from elsewhere to wordpress.com */ +"Transferred Domain" = "Dominio transferido"; + /* translators: %s: block title e.g: \"Paragraph\". */ "Transform %s to" = "Transformar %s a"; /* No comment provided by engineer. */ "Transform block…" = "Transformar bloque…"; -/* Accessibility label for trash button to delete items from the user's media library - Accessibility label for trash buttons in nav bars +/* Accessibility label for trash buttons in nav bars Trashes a comment Trashes the comment */ "Trash" = "Papelera"; -/* Accessibility hint for trash button to delete items from the user's media library */ -"Trash selected media" = "Enviar a la papelera los medios seleccionados"; - -/* Title of the trash page confirmation alert. */ -"Trash this page?" = "¿Enviar esta página a la papelera?"; - /* Title of the trash confirmation alert. */ "Trash this post?" = "¿Enviar a la papelera esta entrada?"; @@ -8080,7 +7835,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "Try with the site address" = "Prueba con la dirección del sitio"; /* Destructive menu title to remove the prompt card from the dashboard. */ -"Turn off prompts" = "Desactivar estímulos"; +"Turn off prompts" = "Desactivar las sugerencias"; /* Verb. An option to switch off site notifications. */ "Turn off site notifications" = "Desactivar los avisos del sitio"; @@ -8139,9 +7894,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Shown when a user logs in with Google but it subsequently fails to work as login to WordPress.com */ "Unable To Connect" = "No fue posible conectar"; -/* An error message. */ -"Unable to Connect" = "No se ha podido conectar"; - /* Title for stories unknown error. */ "Unable to Create Stories Editor" = "No se ha podido crear el editor de historias"; @@ -8157,9 +7909,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* An error message shown when there is an issue creating new invite links. */ "Unable to create new invite links." = "No se han podido crear nuevos enlaces de invitación."; -/* Text displayed in HUD if there was an error attempting to delete a group of media items. */ -"Unable to delete all media items." = "No ha sido posible eliminar todos los elementos multimedia."; - /* Text displayed in HUD if there was an error attempting to delete a media item. */ "Unable to delete media item." = "No ha sido posible eliminar el elemento multimedia."; @@ -8223,12 +7972,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Message displayed when sharing a link to the downloadable backup fails. */ "Unable to share link" = "No es posible compartir el enlace"; -/* Message that appears when a user tries to trash a page while their device is offline. */ -"Unable to trash pages while offline. Please try again later." = "No se han podido enviar las páginas a la papelera mientras estabas sin conexión. Por favor, inténtalo de nuevo más tarde."; - -/* Message that appears when a user tries to trash a post while their device is offline. */ -"Unable to trash posts while offline. Please try again later." = "No es posible enviar entradas a la papelera estando desconectado. Por favor, inténtalo de nuevo más tarde."; - /* Notice title when turning site notifications off fails. */ "Unable to turn off site notifications" = "No es posible desactivar los avisos del sitio"; @@ -8301,8 +8044,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ Button title. Reverts the previous notification operation Revert an operation Revert enabling notification after successfully subcribing to the comments for the post. - The title of an 'undo' button. Tapping the button moves a trashed page out of the trash folder. - The title of an 'undo' button. Tapping the button moves a trashed post out of the trash folder. Undo action */ "Undo" = "Deshacer"; @@ -8345,9 +8086,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title for Unknown HTML Editor */ "Unknown HTML" = "HTML desconocido"; -/* Label to use when creation date from media asset is not know. */ -"Unknown creation date" = "Fecha de creación desconocida"; - /* No comment provided by engineer. */ "Unknown error" = "Error desconocido"; @@ -8513,6 +8251,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of a row displayed on the debug screen used to configure the sandbox store use in the App. */ "Use Sandbox Store" = "Usar la tienda en un entorno de pruebas"; +/* The button's title text to use a security key. */ +"Use a security key" = "Usa una clave de seguridad"; + /* Option to enable the block editor for new posts */ "Use block editor" = "Usar el editor de bloques"; @@ -8588,15 +8329,10 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of an alert informing users that the video they are trying to select is not allowed. */ "Video not uploaded" = "Vídeo no subido"; -/* Accessibility label for video thumbnails in the media collection view. The parameter is the creation date of the video. */ -"Video, %@" = "Vídeo, %@"; - /* Period Stats 'Videos' header */ "Videos" = "Vídeos"; /* Button title. Displays a summary / sharing screen for a specific post. - Label for a button that opens the page when tapped. - Label for the view post button. Tapping displays the post as it appears on the web. Opens the post epilogue screen to allow sharing / viewing of a post. Theme View action title Verb. The screen title shown when viewing a post inside the app. */ @@ -8629,7 +8365,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "View more" = "Ver más"; /* Menu title to show more prompts. */ -"View more prompts" = "Ver más estímulos"; +"View more prompts" = "Ver más sugerencias"; /* Title of a Quick Start Tour */ "View your site" = "Ver tu sitio"; @@ -8711,6 +8447,10 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Message shown on screen while waiting for Google to finish its signup process. */ "Waiting for Google to complete…" = "Esperando a que Google termine…"; +/* Text while the webauthn signature is being verified + Text while waiting for a security key challenge */ +"Waiting for security key" = "Esperando a la clave de seguridad"; + /* View title during the Google auth process. */ "Waiting..." = "Esperando..."; @@ -9011,7 +8751,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "We’ll notify you when its done." = "Te avisaremos cuando hayamos terminado."; /* Description of Blogging Prompts displayed in the Feature Introduction view. */ -"We’ll show you a new prompt each day on your dashboard to help get those creative juices flowing!" = "¡Te mostraremos un nuevo estímulo cada día en tu escritorio para ayudarte a que fluyan esos fluidos creativos!"; +"We’ll show you a new prompt each day on your dashboard to help get those creative juices flowing!" = "¡Te mostraremos una nueva sugerencia cada día en tu escritorio para ayudarte a que fluya la creatividad!"; /* Hint displayed when we fail to fetch the status of the backup in progress. */ "We’ll still attempt to backup your site." = "Volveremos a intentar hacer copia de seguridad de tu sitio."; @@ -9085,6 +8825,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* An error message shown when a wpcom user provides the wrong password. */ "Whoops, something went wrong and we couldn't log you in. Please try again!" = "Vaya, algo salió mal y no pudimos conectarte. ¡Por favor, inténtalo de nuevo!"; +/* Generic error on the 2FA screen */ +"Whoops, something went wrong. Please try again!" = "Vaya, algo ha ido mal. ¡Por favor, inténtalo de nuevo!"; + +/* Error when the uses chooses an invalid security key on the 2FA screen. */ +"Whoops, that security key does not seem valid. Please try again with another one" = "Vaya, esa clave de seguridad parece que no es válida. Por favor, inténtalo de nuevo con otra"; + /* Error message shown when an incorrect two factor code is provided. */ "Whoops, that's not a valid two-factor verification code. Double-check your code and try again!" = "Vaya, eso no es un código de verificación en dos pasos. ¡Vuelve a comprobar tu código e inténtalo de nuevo!"; @@ -9112,9 +8858,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Siri Suggestion to open Support */ "WordPress Help" = "Ayuda de WordPress"; -/* Name for the WordPress Media Library */ -"WordPress Media" = "Medios de WordPress"; - /* Accessibility label for selecting an image or video from the user's WordPress media library on formatting toolbar. */ "WordPress Media Library" = "Biblioteca de medios de WordPress"; @@ -9268,10 +9011,10 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "You can always log in with a link like the one you just used, but you can also set up a password if you prefer." = "Siempre puedes acceder con un enlace como el que acabas de usar, pero también puedes configurar una contraseña si lo prefieres."; /* Note displayed in the Feature Introduction view. */ -"You can control Blogging Prompts and Reminders at any time in My Site > Settings > Blogging" = "Puedes gestionar los recordatorios y estímulos para bloguear en cualquier momento desde Mi sitio > Ajustes > Bloguear."; +"You can control Blogging Prompts and Reminders at any time in My Site > Settings > Blogging" = "Puedes gestionar los recordatorios y las sugerencias de publicación en cualquier momento desde Mi sitio > Ajustes > Bloguear."; /* Accessibility hint for Note displayed in the Feature Introduction view. */ -"You can control Blogging Prompts and Reminders at any time in My Site, Settings, Blogging" = "Puedes gestionar los recordatorios y estímulos para bloguear en cualquier momento desde Mi sitio > Ajustes > Bloguear."; +"You can control Blogging Prompts and Reminders at any time in My Site, Settings, Blogging" = "Puedes gestionar los recordatorios y las sugerencias de publicación en cualquier momento desde «Mi sitio > Ajustes > Bloguear»."; /* No comment provided by engineer. */ "You can edit this block using the web version of the editor." = "Puedes editar este bloque usando la versión web del editor."; @@ -9429,9 +9172,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Share extension error dialog text. */ "Your account does not have permission to upload media to this site. The Site Administrator can change these permissions." = "Tu cuenta no tiene permisos para subir medios a este sitio. El administrador del sitio puede cambiar estos permisos."; -/* Explaining to the user why the app needs access to the device media library. */ -"Your app is not authorized to access media library due to active restrictions such as parental controls. Please check your parental control settings in this device." = "Tu aplicación no está autorizada para acceder a la biblioteca de medios debido a restricciones activas tales como controles parentales. Por favor, revisa los ajustes de control parental en este dispositivo."; - /* Title for the Jetpack Backup Complete message. */ "Your backup is now available for download" = "Tu copia de seguridad ya está disponible para descargarla"; @@ -9450,9 +9190,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of the site address section in the Domains Dashboard. */ "Your free WordPress.com address is" = "Tu dirección gratuita de WordPress.com es"; -/* Error message when picked media cannot be imported into stories. */ -"Your media could not be exported. If the problem persists you can contact us via the Me > Help & Support screen." = "No se han podido exportar tus medios. Si continúa el problema, puedes contactar con nosotros a través de la pantalla «Yo > Ayuda y soporte»."; - /* Details about recently acquired domain on domain credit redemption success screen */ "Your new domain %@ is being set up. It may take up to 30 minutes for your domain to start working." = "Tu nuevo dominio %@ está siendo configurando. Tu dominio puede tardar hasta 30 minutos en empezar a funcionar."; @@ -9576,8 +9313,38 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* This is the string we display when prompting the user to review the WordPress app */ "appRatings.wordpress.prompt" = "¿Qué opinas de WordPress?"; -/* Label displayed on audio media items. */ -"audio" = "audio"; +/* Option to enable the optimization of images when uploading. */ +"appSettings.media.imageOptimizationRow" = "Optimizar imágenes"; + +/* Indicates an image will use high quality when uploaded. */ +"appSettings.media.imageQuality.high" = "Alto"; + +/* Indicates an image will use low quality when uploaded. */ +"appSettings.media.imageQuality.low" = "Baja"; + +/* Indicates an image will use maximum quality when uploaded. */ +"appSettings.media.imageQuality.maximum" = "Máxima"; + +/* Indicates an image will use medium quality when uploaded. */ +"appSettings.media.imageQuality.medium" = "Media"; + +/* The quality of image used when uploading */ +"appSettings.media.imageQuality.title" = "Calidad"; + +/* Title for the image quality settings option. */ +"appSettings.media.imageQualityRow" = "Calidad de imagen"; + +/* Message of an alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.message" = "La optimización de imágenes las reduce para subirlas más rápido.\n\nEsta opción está activada por defecto, pero puedes cambiarla en los ajustes de la aplicación en cualquier momento."; + +/* Title of an alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.title" = "¿Seguir optimizando las imágenes?"; + +/* Title of button for turning off image optimization, displayed in the alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.turnOff" = "No, apágala"; + +/* Title of button for leaving on image optimization, displayed in the alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.turnOn" = "Sí, déjala encendida"; /* translators: displays audio file extension. e.g. MP3 audio file */ "audio file" = "archivo de audio"; @@ -9691,7 +9458,44 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "blogHeader.actionCopyURL" = "Copiar URL"; /* Context menu button title */ -"blogHeader.actionOpenInBrowser" = "Abrir en el navegador"; +"blogHeader.actionVisitSite" = "Visitar sitio"; + +/* Title for a button that, when tapped, shows more info about participating in Bloganuary. */ +"bloganuary.dashboard.card.button.learnMore" = "Más información"; + +/* Short description for the Bloganuary event, shown right below the title. */ +"bloganuary.dashboard.card.description" = "Durante el mes de enero, las sugerencias para escribir en el blog provendrán de Bloganuary, nuestro reto comunitario para crear un hábito de blogueo para el nuevo año."; + +/* Title for the Bloganuary dashboard card while Bloganuary is running. */ +"bloganuary.dashboard.card.runningTitle" = "¡Bloganuary ya está aquí!"; + +/* Title for the Bloganuary dashboard card. */ +"bloganuary.dashboard.card.title" = "¡Bloganuary está a la vuelta de la esquina!"; + +/* Title of a button that calls the user to enable the Blogging Prompts feature. */ +"bloganuary.learnMore.modal.button.promptsDisabled" = "Activa las sugerencias de publicación"; + +/* Title of a button that will dismiss the Bloganuary modal when tapped. +Note that the word 'go' here should have a closer meaning to 'start' rather than 'move forward'. */ +"bloganuary.learnMore.modal.button.promptsEnabled" = "¡Vamos allá!"; + +/* The second line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.description.second" = "Publica tu respuesta."; + +/* The third line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.description.third" = "Lee las respuestas de otros blogueros para conseguir inspiración y hacer nuevas conexiones."; + +/* The first line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.descriptions.first" = "Recibe una sugerencia nueva para inspirarte cada día."; + +/* An additional piece of information shown in case the user has the Blogging Prompts feature disabled. */ +"bloganuary.learnMore.modal.footer.addition" = "Para unirte a Bloganuary tienes que habilitar las sugerencias de publicación."; + +/* An informative excerpt shown in a subtler tone. */ +"bloganuary.learnMore.modal.footer.text" = "Bloganuary utilizará las sugerencias de publicación diarias para enviarte temas durante el mes de enero."; + +/* The headline text of the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.headline" = "Únete a nuestro reto de escritura de un mes"; /* Verb. Dismisses the blogging prompt notification. */ "bloggingPrompt.pushNotification.customActionDescription.dismiss" = "Descartar"; @@ -9720,6 +9524,12 @@ Example: Comment on Example: Reply to Pamela Nguyen */ "comment.header.subText.reply" = "Responder a %1$@"; +/* Error message informing a user about an invalid password. */ +"common.reEnterPasswordMessage" = "Es posible que el nombre de usuario o la contraseña que están guardados en la aplicación hayan caducado. Introduce de nuevo tu contraseña en la configuración y vuelve a intentarlo."; + +/* An error message. */ +"common.unableToConnect" = "No se ha podido conectar"; + /* Footnote for the privacy compliance popover. */ "compliance.analytics.popover.footnote" = "Estas cookies nos permiten optimizar el rendimiento al recopilar información sobre cómo los usuarios interactúan con nuestras webs."; @@ -9870,47 +9680,92 @@ Example: Reply to Pamela Nguyen */ /* Title for a menu action in the context menu on the Jetpack install card. */ "domain.dashboard.card.menu.hide" = "Ocultar esto"; -/* Domain Purchase Completion footer */ -"domain.purchase.preview.footer" = "Puede tardar hasta 30 minutos en que tu dominio personalizado empiece a funcionar."; +/* Search domain - Title for the Suggested domains screen */ +"domain.management.addDomain.search.title" = "Buscar un dominio"; -/* Domain Purchase Completion description (only for FREE domains). */ -"domain.purchase.preview.free.description" = "A continuación te ayudaremos a que estés preparado para que se pueda navegar por tu sitio."; +/* Domain management buy domain card button title */ +"domain.management.buy.card.button.title" = "Obtener dominio"; -/* Domain Purchase Completion description (only for PAID domains). */ -"domain.purchase.preview.paid.description" = "Te hemos enviado por correo electrónico tu recibo. A continuación te ayudaremos a que estés preparado para todos."; +/* Domain management buy domain card subtitle */ +"domain.management.buy.card.subtitle" = "Añade un sitio más tarde."; -/* Reflects that site is live when domain purchase feature flag is ON. */ -"domain.purchase.preview.title" = "¡Genial, tu sitio está listo!"; +/* Domain management buy domain card title */ +"domain.management.buy.card.title" = "Solo tienes que comprar un dominio."; + +/* The expired label of the domain card in All Domains screen. */ +"domain.management.card.expired.label" = "Caducado"; + +/* The renews label of the domain card in All Domains screen. */ +"domain.management.card.renews.label" = "Se renueva"; -/* Status of a domain in `Action Required` state */ -"domain.status.action.required" = "Acción requerida"; +/* The empty state button title in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.button.title" = "Buscar un dominio"; -/* Status of a domain in `Active` state */ -"domain.status.active" = "Activa"; +/* The empty state description in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.description" = "Pulsa a continuación para encontrar tu dominio perfecto."; -/* Status of a domain in `Complete Setup` state */ -"domain.status.complete.setup" = "Completar instalación"; +/* The empty state title in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.title" = "No tienes ningún domino."; -/* Status of a domain in `Expired` state */ -"domain.status.expired" = "Caducado"; +/* The empty state description in All Domains screen when an error occurs */ +"domain.management.error.empty.state.description" = "Se ha producido un error al cargar tus dominios. Ponte en contacto con el servicio de soporte si el problema continúa."; -/* Status of a domain in `Expiring Soon` state */ -"domain.status.expiring.soon" = "Caduca pronto"; +/* The empty state title in All Domains screen when an error occurs */ +"domain.management.error.empty.state.title" = "Se ha producido un error."; -/* Status of a domain in `Failed` state */ -"domain.status.failed" = "Fallido"; +/* The empty state button title in All Domains screen when an error occurs */ +"domain.management.error.state.button.title" = "Volver a intentarlo"; -/* Status of a domain in `In Progress` state */ -"domain.status.in.progress" = "En curso"; +/* The empty state description in All Domains screen when the user is offline */ +"domain.management.offline.empty.state.description" = "Comprueba tu conexión a la red e inténtalo de nuevo."; -/* Status of a domain in `Renew` state */ -"domain.status.renew" = "Renovar"; +/* The empty state title in All Domains screen when the user is offline */ +"domain.management.offline.empty.state.title" = "Sin conexión a Internet"; -/* Status of a domain in `Verify Email` state */ -"domain.status.verify.email" = "Verificar correo electrónico"; +/* Domain management choose site card button title */ +"domain.management.purchase.footer" = "* Los planes anuales de pago incluyen un dominio gratis durante un año."; -/* Status of a domain in `Verifying` state */ -"domain.status.verifying" = "Verificando"; +/* Domain management purchase domain screen title */ +"domain.management.purchase.subtitle" = "No te preocupes, puedes añadir un sitio fácilmente más adelante."; + +/* Domain management purchase domain screen title. */ +"domain.management.purchase.title" = "Elige cómo usar tu dominio."; + +/* The search bar title in All Domains screen. */ +"domain.management.search-bar.title" = "Buscar dominios"; + +/* The empty state description in All Domains screen when the are no domains matching the search criteria */ +"domain.management.search.empty.state.description" = "No hemos encontrado ningún dominio que se ajuste a tu búsqueda por «%@»."; + +/* The empty state title in All Domains screen when the are no domains matching the search criteria */ +"domain.management.search.empty.state.title" = "No se ha encontrado ningún dominio que coincida"; + +/* Domain management choose site card button title */ +"domain.management.site.card.button.title" = "Elige el sitio"; + +/* Domain management choose site card subtitle */ +"domain.management.site.card.footer" = "Dominio gratuito durante el primer año*"; + +/* Domain management choose site card subtitle */ +"domain.management.site.card.subtitle" = "Utilízalo con un sitio que ya hayas empezado a crear."; + +/* Domain management choose site card title */ +"domain.management.site.card.title" = "Sitio de WordPress.com ya existente"; + +/* Domain Management Screen Title */ +"domain.management.title" = "Todos los dominios"; + +/* Domain Purchase Completion footer */ +"domain.purchase.preview.footer" = "Puede tardar hasta 30 minutos en que tu dominio personalizado empiece a funcionar."; + +/* Domain Purchase Completion description (only for FREE domains). */ +"domain.purchase.preview.free.description" = "A continuación te ayudaremos a que estés preparado para que se pueda navegar por tu sitio."; + +/* Domain Purchase Completion description (only for PAID domains). */ +"domain.purchase.preview.paid.description" = "Te hemos enviado por correo electrónico tu recibo. A continuación te ayudaremos a que estés preparado para todos."; + +/* Reflects that site is live when domain purchase feature flag is ON. */ +"domain.purchase.preview.title" = "¡Genial, tu sitio está listo!"; /* The 'Best Alternative' label under the domain name in 'Choose a domain' screen */ "domain.suggestions.row.best-alternative" = "Mejor alternativa"; @@ -9933,12 +9788,24 @@ Example: Reply to Pamela Nguyen */ /* The text to display for paid domains in 'Site Creation > Choose a domain' screen */ "domain.suggestions.row.yearly" = "por año"; +/* Title for the checkout screen. */ +"domains.checkout.title" = "Finalizar compra"; + /* Action shown in a bottom notice to dismiss it. */ "domains.failure.dismiss" = "Descartar"; /* Content show when the domain selection action fails. */ "domains.failure.title" = "Lo sentimos, el dominio que quieres añadir no se puede comprar en la aplicación Jetpack en estos momentos."; +/* Title for the screen where the user can choose how to use the domain they're end up purchasing. */ +"domains.purchase.choice.title" = "Comprar dominio"; + +/* Back button title that navigates back to the search domains screen. */ +"domains.search.backButton.title" = "Buscar"; + +/* Title of screen where user chooses a site to connect to their selected domain */ +"domains.sitePicker.title" = "Elegir sitio"; + /* No comment provided by engineer. */ "double-tap to change unit" = "doble toque para cambiar de unidad"; @@ -9956,6 +9823,15 @@ Example: Reply to Pamela Nguyen */ Site Address placeholder */ "example.com" = "example.com"; +/* Title for confirmation navigation bar button item */ +"externalMediaPicker.add" = "Añadir"; + +/* Bottom toolbar title in the selection mode */ +"externalMediaPicker.toolbarSelectItemsPrompt" = "Seleccionar imágenes"; + +/* Bottom toolbar title in the selection mode */ +"externalMediaPicker.toolbarViewSelected" = "Ver seleccionados (%@)"; + /* Title of screen the displays the details of an advertisement campaign. */ "feature.blaze.campaignDetails.title" = "Detalles de la campaña"; @@ -10055,9 +9931,6 @@ Example: Reply to Pamela Nguyen */ /* (placeholder) Help the user enter a URL into the field */ "http:\/\/my-site-address (URL)" = "http:\/\/direccion-de-mi-sitio (URL)"; -/* Label displayed on image media items. */ -"image" = "imagen"; - /* Sentence to justify why the app is asking permission from the user to use their camera. */ "infoplist.NSCameraUsageDescription" = "Para hacer las fotos o vídeos que deseas usar en tus entradas."; @@ -10358,6 +10231,9 @@ Example: Reply to Pamela Nguyen */ /* Indicating that referrer was marked as spam */ "marked as spam" = "marcado como spam"; +/* Products header text in Me Screen. */ +"me.products.header" = "Productos"; + /* Title of error prompt shown when a sync fails. */ "media.syncFailed" = "No se pueden sincronizar los medios"; @@ -10370,18 +10246,24 @@ Example: Reply to Pamela Nguyen */ /* Message of an alert informing users that the video they are trying to select is not allowed. */ "mediaExporter.videoLimitExceededError" = "Subir vídeos de más de 5 minutos requiere un plan de pago."; -/* Verb. User action to dismiss error alert when failing to load media item. */ -"mediaItemTable.errorAlert.dismissButton" = "Descartar"; - /* Accessibility hint for add button to add items to the user's media library */ "mediaLibrary.addButtonAccessibilityHint" = "Añadir nuevo medio"; /* Accessibility label for add button to add items to the user's media library */ "mediaLibrary.addButtonAccessibilityLabel" = "Añadir"; +/* Button name in the more menu */ +"mediaLibrary.aspectRatioGrid" = "Cuadrícula de relación de aspecto"; + +/* Context menu button */ +"mediaLibrary.buttonDelete" = "Borrar"; + /* Media screen navigation bar button Select title */ "mediaLibrary.buttonSelect" = "Seleccionar"; +/* Context menu button */ +"mediaLibrary.buttonShare" = "Compartir"; + /* Verb. Button title. Tapping cancels an action. */ "mediaLibrary.deleteConfirmationCancel" = "Cancelar"; @@ -10403,6 +10285,21 @@ Example: Reply to Pamela Nguyen */ /* Text displayed in HUD after successfully deleting a media item */ "mediaLibrary.deletionSuccessMessage" = "¡Borrado!"; +/* The name of the media filter */ +"mediaLibrary.filterAll" = "Todos"; + +/* The name of the media filter */ +"mediaLibrary.filterAudio" = "Audio"; + +/* The name of the media filter */ +"mediaLibrary.filterDocuments" = "Documentos"; + +/* The name of the media filter */ +"mediaLibrary.filterImages" = "Imágenes"; + +/* The name of the media filter */ +"mediaLibrary.filterVideos" = "Vídeos"; + /* User action to delete un-uploaded media. */ "mediaLibrary.retryOptionsAlert.delete" = "Borrar"; @@ -10415,6 +10312,12 @@ Example: Reply to Pamela Nguyen */ /* Message displayed when no results are returned from a media library search. Should match Calypso. */ "mediaLibrary.searchResultsEmptyTitle" = "Ningún medio coincide con tu búsqueda"; +/* Text displayed in HUD if there was an error attempting to share a group of media items. */ +"mediaLibrary.sharingFailureMessage" = "No se pueden compartir los elementos seleccionados."; + +/* Button name in the more menu */ +"mediaLibrary.squareGrid" = "Cuadrícula"; + /* Media screen navigation title */ "mediaLibrary.title" = "Medios"; @@ -10436,6 +10339,12 @@ Example: Reply to Pamela Nguyen */ /* The title of the button to dismiss the alert shown when the picked media cannot be imported into stories. */ "mediaPicker.failedMediaExportAlert.dismissButton" = "Descartar"; +/* Error message when picked media cannot be imported into stories. */ +"mediaPicker.failedMediaExportAlert.message" = "No se han podido exportar tus medios. Si continúa el problema, puedes contactar con nosotros a través de la pantalla «Yo > Ayuda y soporte»."; + +/* Error title when picked media cannot be imported into stories. */ +"mediaPicker.failedMediaExportAlert.title" = "Ha fallado la exportación de medios"; + /* Message for alert when access to camera is not granted */ "mediaPicker.noCameraAccessMessage" = "Esta aplicación necesita permiso para acceder a la cámara con el fin de capturar nuevos elementos multimedia; cambia la configuración de privacidad si deseas permitir esta acción."; @@ -10469,6 +10378,12 @@ Example: Reply to Pamela Nguyen */ /* The name of the action in the context menu */ "mediaPicker.takeVideo" = "Grabar vídeo"; +/* Navigation title for media preview. Example: 1 of 3 */ +"mediaPreview.NofM" = "%1$@ de %2$@"; + +/* Max image size in pixels (e.g. 300x300px) */ +"mediaSizeSlider.valueFormat" = "%1$d × %2$d px"; + /* The description in the Delete WordPress screen */ "migration.deleteWordpress.description" = "Parece que todavía tienes la aplicación WordPress instalada."; @@ -10481,9 +10396,6 @@ Example: Reply to Pamela Nguyen */ /* The title in the Delete WordPress screen */ "migration.deleteWordpress.title" = "Ya no necesitas la aplicación de WordPress en tu dispositivo"; -/* Primary button title in the migration done screen. */ -"migration.done.actions.primary.title" = "Terminar"; - /* Footer for the migration done screen. */ "migration.done.footer" = "Recomendamos desinstalar la aplicación WordPress en tu dispositivo para evitar conflictos de datos."; @@ -10493,6 +10405,9 @@ Example: Reply to Pamela Nguyen */ /* Primary description in the migration done screen. */ "migration.done.primaryDescription" = "Hemos transferido todos tus datos y ajustes. Todo está tal y como lo dejaste."; +/* Secondary description (second paragraph) in the migration done screen. */ +"migration.done.secondaryDescription" = "¡Es hora de continuar tu viaje por WordPress en la aplicación Jetpack!"; + /* Title of the migration done screen. */ "migration.done.title" = "¡Gracias por cambiar a Jetpack!"; @@ -10541,6 +10456,9 @@ Example: Reply to Pamela Nguyen */ /* The title in the migration welcome screen */ "migration.welcome.title" = "¡Te damos la bienvenida a Jetpack!"; +/* Primary button title in the migration done screen. */ +"migrationDone.actions.primaryTitle" = "Vamos"; + /* Description for the static screen displayed prompting users to switch the Jetpack app. */ "movedToJetpack.description" = "La aplicación de Jetpack tiene todas las funciones de la aplicación de WordPress, y ahora ofrece acceso exclusivo a Estadísticas, Lector, Notificaciones y mucho más."; @@ -10616,6 +10534,30 @@ Example: Reply to Pamela Nguyen */ /* Message title for when a user has no sites. */ "mySite.noSites.title" = "No tienes sitios"; +/* Menu title for the add site option */ +"mySite.siteActions.addSite" = "Añadir sitio"; + +/* Button that reveals more site actions */ +"mySite.siteActions.button" = "Acciones del sitio"; + +/* Accessibility hint for button used to show more site actions */ +"mySite.siteActions.hint" = "Toca para mostrar más acciones del sitio"; + +/* Menu title for the personalize home option */ +"mySite.siteActions.personalizeHome" = "Personalizar Inicio"; + +/* Menu title for the change site icon option */ +"mySite.siteActions.siteIcon" = "Cambiar icono del sitio"; + +/* Menu title for the change site title option */ +"mySite.siteActions.siteTitle" = "Cambiar título del sitio"; + +/* Menu title for the switch site option */ +"mySite.siteActions.switchSite" = "Cambiar de sitio"; + +/* Menu title for the visit site option */ +"mySite.siteActions.visitSite" = "Visitar sitio"; + /* Dismiss button title. */ "noResultsViewController.dismissButton" = "Descartar"; @@ -10631,14 +10573,20 @@ Example: Reply to Pamela Nguyen */ /* This is one of the buttons we display when prompting the user for a review */ "notifications.appRatings.sendFeedback.yes.buttonTitle" = "Enviar comentarios"; -/* Word separating the current index from the total amount. I.e.: 7 of 9 */ -"of" = "de"; +/* Badge for page cells */ +"pageList.badgeHomepage" = "Página de inicio"; -/* Label displayed on media items that are not video, image, or audio. */ -"other" = "otros"; +/* Badge for page cells */ +"pageList.badgeLocalChanges" = "Cambios locales"; -/* Promote the page with Blaze. */ -"pages.blaze.actionTitle" = "Promocionar con Blaze"; +/* Badge for page cells */ +"pageList.badgePendingReview" = "Pendiente de revisión"; + +/* Badge for page cells */ +"pageList.badgePosts" = "Página de entradas"; + +/* Badge for page cells */ +"pageList.badgePrivate" = "Privado"; /* Subtitle of the theme template homepage cell */ "pages.template.subtitle" = "Tu página de inicio usa una plantilla de tema, por lo que se abrirá en el editor web."; @@ -10646,6 +10594,36 @@ Example: Reply to Pamela Nguyen */ /* Title of the theme template homepage cell */ "pages.template.title" = "Página de inicio"; +/* Message informing the user that their static homepage page was set successfully */ +"pages.updatePage.successTitle" = "La página se ha actualizado correctamente."; + +/* Delete option in the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.actionTitle" = "Borrar permanentemente"; + +/* Message of the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.alertMessage" = "¿Seguro que quieres eliminar esta página permanentemente?"; + +/* Title of the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.alertTitle" = "¿Borrar permanentemente?"; + +/* Menu option for filtering posts by everyone */ +"pagesList.pagesByEveryone" = "Páginas de todos"; + +/* Menu option for filtering posts by me */ +"pagesList.pagesByMe" = "Mis páginas"; + +/* Trash option in the trash page confirmation alert. */ +"pagesList.trash.actionTitle" = "Mover a la papelera"; + +/* Message of the trash page confirmation alert. */ +"pagesList.trash.alertMessage" = "¿Seguro que quieres enviar esta página a la papelera de reciclaje?"; + +/* Title of the trash page confirmation alert. */ +"pagesList.trash.alertTitle" = "¿Enviar esta página a la papelera?"; + +/* Cancels an Action */ +"pagesList.trash.cancel" = "Cancelar"; + /* No comment provided by engineer. */ "password" = "contraseña"; @@ -10685,6 +10663,51 @@ Example: Reply to Pamela Nguyen */ /* Register Domain - Domain contact information field Phone */ "phone number" = "número de teléfono"; +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.createdTimeAgo" = "Creado el %@"; + +/* Status mesasge for post cells */ +"post.deletingPostPermanentlyStatusMessage" = "Borrando entrada..."; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.editedTimeAgo" = "Editado el %@"; + +/* Status mesasge for post cells */ +"post.movingToTrashStatusMessage" = "Moviendo a la papelera..."; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.publishedTimeAgo" = "Publicado %@"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.scheduledForDate" = "Programado %@"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.trashedTimeAgo" = "Enviado a la papelera %@"; + +/* Accessibility label for the post author in the post list. The parameter is the author name. For example, \"By Elsa.\" */ +"postList.a11y.authorChunkFormat" = "Por %@."; + +/* Accessibility label for a post's excerpt in the post list. The parameter is the post excerpt. For example, \"Excerpt. This is the first paragraph.\" */ +"postList.a11y.exerptChunkFormat" = "Extracto. %@."; + +/* Accessibility label for a sticky post in the post list. */ +"postList.a11y.sticky" = "Fija."; + +/* Accessibility label for a post in the post list. The first placeholder is the post title. The second placeholder is the date. */ +"postList.a11y.titleAndDateChunkFormat" = "%1$@, %2$@."; + +/* Title for the 'Trash' post list row swipe action */ +"postList.swipeActionDelete" = "Papelera"; + +/* Title for the 'Delete' post list row swipe action */ +"postList.swipeActionDeletePermanently" = "Borrar"; + +/* Title for the 'Share' post list row swipe action */ +"postList.swipeActionShare" = "Compartir"; + +/* Title for the 'View' post list row swipe action */ +"postList.swipeActionView" = "Ver"; + /* User action to dismiss featured media options. */ "postSettings.featuredImageUploadActionSheet.dismiss" = "Descartar"; @@ -10703,9 +10726,84 @@ Example: Reply to Pamela Nguyen */ /* Button in Post Settings */ "postSettings.setFeaturedImageButton" = "Establecer la imagen destacada"; +/* Error message on post/page settings screen */ +"postSettings.updateFailedMessage" = "Error al actualizar los ajustes de la entrada"; + /* Promote the post with Blaze. */ "posts.blaze.actionTitle" = "Promocionar con Blaze"; +/* Label for the Post List option that cancels automatic uploading of a post. */ +"posts.cancelUpload.actionTitle" = "Cancelar subida"; + +/* Label for post comments option. Tapping displays comments for a post. */ +"posts.comments.actionTitle" = "Comentarios"; + +/* Label for the delete post option. Tapping permanently deletes a post. */ +"posts.delete.actionTitle" = "Borrar permanentemente"; + +/* Label for an option that moves a post to the draft folder */ +"posts.draft.actionTitle" = "Mover a borradores"; + +/* Label for post duplicate option. Tapping creates a copy of the post. */ +"posts.duplicate.actionTitle" = "Duplicar"; + +/* Opens a submenu for page attributes. */ +"posts.pageAttributes.actionTitle" = "Atributos de página"; + +/* Label for the preview post button. Tapping displays the post as it appears on the web. */ +"posts.preview.actionTitle" = "Vista previa"; + +/* Label for an option that moves a publishes a post immediately */ +"posts.publish.actionTitle" = "Publicar ahora"; + +/* Retry uploading the post. */ +"posts.retry.actionTitle" = "Volver a intentar"; + +/* Set the selected page as the homepage. */ +"posts.setHomepage.actionTitle" = "Establecer como página de inicio"; + +/* Set the parent page for the selected page. */ +"posts.setParent.actionTitle" = "Establecer como padre"; + +/* Set the selected page as a posts page. */ +"posts.setPostsPage.actionTitle" = "Establecer como página de entradas"; + +/* Set the selected page as a regular page. */ +"posts.setRegularPage.actionTitle" = "Establecer como página normal"; + +/* Label for post settings option. Tapping displays settings for a post. */ +"posts.settings.actionTitle" = "Ajustes"; + +/* Share the post. */ +"posts.share.actionTitle" = "Compartir"; + +/* Label for post stats option. Tapping displays statistics for a post. */ +"posts.stats.actionTitle" = "Estadísticas"; + +/* Label for a option that moves a post to the trash folder */ +"posts.trash.actionTitle" = "Mover a la papelera"; + +/* Label for the view post button. Tapping displays the post as it appears on the web. */ +"posts.view.actionTitle" = "Ver"; + +/* A short message explaining that a page was deleted permanently. */ +"postsList.deletePage.message" = "Página eliminada permanentemente"; + +/* A short message explaining that a post was deleted permanently. */ +"postsList.deletePost.message" = "Entrada eliminada permanentemente"; + +/* A short message explaining that a page was moved to the trash bin. */ +"postsList.movePageToTrash.message" = "Página movida a la papelera."; + +/* A short message explaining that a post was moved to the trash bin. */ +"postsList.movePostToTrash.message" = "Entrada movida a la papelera."; + +/* Menu option for filtering posts by everyone */ +"postsList.postsByEveryone" = "Entradas de todos"; + +/* Menu option for filtering posts by me */ +"postsList.postsByMe" = "Mis entradas"; + /* Title for the button to subscribe to Jetpack Social on the remaining shares view */ "postsettings.social.remainingshares.subscribe" = "Suscríbete ahora para compartir más"; @@ -10777,7 +10875,7 @@ Tapping on this row allows the user to edit the sharing message. */ "prompts.notification.removed.subtitle" = "Visitar los ajustes del sitio para volver a activar"; /* Title of the notification when prompts are hidden from the dashboard card */ -"prompts.notification.removed.title" = "Se han ocultado los estímulos para bloguear."; +"prompts.notification.removed.title" = "Se han ocultado las sugerencias de publicación"; /* Button label that dismisses the qr log in flow and returns the user back to the previous screen */ "qrLoginVerifyAuthorization.completedInstructions.dismiss" = "Descartar"; @@ -10850,13 +10948,15 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Text for the 'Like' button on the reader post card cell. */ "reader.post.button.like" = "Me gusta"; -/* Accessibility hint for the like button on the reader post card cell - Accessibility hint for the liked button on the reader post card cell */ +/* Accessibility hint for the like button on the reader post card cell */ "reader.post.button.like.accessibility.hint" = "Da me gusta a la entrada."; /* Text for the 'Liked' button on the reader post card cell. */ "reader.post.button.liked" = "Me gustó"; +/* Accessibility hint for the liked button on the reader post card cell */ +"reader.post.button.liked.accessibility.hint" = "No le gusta la entrada."; + /* Accessibility hint for the site header on the reader post card cell */ "reader.post.button.menu.accessibility.hint" = "Abre un menú con más acciones."; @@ -10920,6 +11020,12 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Title for the tooltip anchor. */ "readerDetail.tooltipAnchorTitle.accessibilityLabel" = "Nueva"; +/* The button title for the transfer footer view in Register Domain screen */ +"register.domain.transfer.button.title" = "Transferir dominio"; + +/* The title for the transfer footer view in Register Domain screen */ +"register.domain.transfer.title" = "¿Quieres transferir un dominio que ya tienes?"; + /* Information of what related post are and how they are presented */ "relatedPostsSettings.optionsFooter" = "Las entradas relacionadas muestran contenido relacionado de tu sitio bajo tus entradas."; @@ -11001,6 +11107,9 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Button to progress to the next step after selecting domain in Site Creation */ "siteCreation.domains.buttons.selectDomain" = "Elegir dominio"; +/* Accessibility label for audio items in the media collection view. The parameter is the creation date of the audio. */ +"siteMedia.accessibilityLabelAudio" = "Audio, %@"; + /* Accessibility label for other media items in the media collection view. The parameter is the filename file. */ "siteMedia.accessibilityLabelDocument" = "Documento, %@"; @@ -11016,6 +11125,21 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Accessibility hint for actions when displaying media items. */ "siteMedia.cellAccessibilityHint" = "Elige medios."; +/* Accessibility hint for media item preview for user's viewing an item in their media library */ +"siteMediaItem.contentViewAccessibilityHint" = "Toca para ver los medios a pantalla completa"; + +/* Accessibility label for media item preview for user's viewing an item in their media library */ +"siteMediaItem.contentViewAccessibilityLabel" = "Previsualizar medios"; + +/* Title for confirmation navigation bar button item */ +"siteMediaPicker.add" = "Añadir"; + +/* Button selection media in media picker */ +"siteMediaPicker.deselect" = "Anular selección"; + +/* Button selection media in media picker */ +"siteMediaPicker.select" = "Seleccionar"; + /* Media screen navigation title */ "siteMediaPicker.title" = "Medios"; @@ -11023,7 +11147,7 @@ Example: given a notice format "Following %@" and empty site name, this will be "siteSettings.privacy.title" = "Privacidad"; /* Hint for users when hidden privacy setting is set */ -"siteVisibility.hidden.hint" = "Tu sitio es visible por todos pero pide a los motores de búsqueda que no lo indexen."; +"siteVisibility.hidden.hint" = "El sitio web se ocultará a los visitantes y mostrará el aviso \"Próximamente\" hasta que se pueda visualizar."; /* Text for privacy settings: Hidden */ "siteVisibility.hidden.title" = "Oculto"; @@ -11184,6 +11308,12 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Dismiss the AlertView */ "stockPhotos.strings.dismiss" = "Descartar"; +/* Subtitle for placeholder in Free Photos. The company name 'Pexels' should always be written as it is. */ +"stockPhotos.subtitle" = "Fotos proporcionadas por Pexels"; + +/* Title for placeholder in Free Photos */ +"stockPhotos.title" = "¡Busca fotos gratuitas que añadir a tu biblioteca de medios!"; + /* Section title for prominent suggestions */ "suggestions.section.prominent" = "En esta conversación"; @@ -11331,6 +11461,9 @@ Example: given a notice format "Following %@" and empty site name, this will be /* View title for Help & Support page. */ "support.title" = "Ayuda"; +/* Title for placeholder in Tenor picker */ +"tenor.welcomeMessage" = "¡Busca GIFs para añadir a tu biblioteca de medios!"; + /* Header of delete screen section listing things that will be deleted. */ "these items will be deleted:" = "Se eliminarán estos elementos:"; @@ -11346,9 +11479,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Displayed in the confirmation alert when marking unread notifications as read. */ "unread" = "no leído"; -/* Label displayed on video media items. */ -"video" = "vídeo"; - /* Portion of a message for Jetpack users that have multisite WP installation, thus Restore is not available. This part is a link, colored with a different color. */ "visit our documentation page" = "visita nuestra página de documentación"; diff --git a/WordPress/Resources/fr.lproj/Localizable.strings b/WordPress/Resources/fr.lproj/Localizable.strings index a79da9699039..d97bbdc5f565 100644 --- a/WordPress/Resources/fr.lproj/Localizable.strings +++ b/WordPress/Resources/fr.lproj/Localizable.strings @@ -1,4 +1,4 @@ -/* Translation-Revision-Date: 2023-10-18 12:54:08+0000 */ +/* Translation-Revision-Date: 2024-01-03 18:54:08+0000 */ /* Plural-Forms: nplurals=2; plural=n > 1; */ /* Generator: GlotPress/4.0.0-alpha.11 */ /* Language: fr */ @@ -152,9 +152,6 @@ /* Difference label for Period Overview stat, indicating change from previous period. Ex: +99.9K (5%) */ "%@%@ (%@%%)" = "%1$@%2$@ (%3$@ %%)"; -/* Accessibility label for a post in the post list. The parameters are the title, and date respectively. For example, \"Let it Go, 1 hour ago.\" */ -"%@, %@." = "%1$@, %2$@."; - /* Accessibility value for a Stats' Posting Activity Month if the user selected a day with posts. The first parameter is day (e.g. November 2019). The second parameter is the number of posts. */ "%@. %d posts." = "%1$@. %2$d articles."; @@ -210,9 +207,6 @@ /* Age between dates over one year. */ "%d years" = "%d ans"; -/* Max image size in pixels (e.g. 300x300px) */ -"%dx%dpx" = "%1$d x %2$d px"; - /* One menu area available in the theme */ "%i menu area in this theme" = "%i zone de menu dans ce thème"; @@ -271,6 +265,9 @@ translators: Block name. %s: The localized block name */ /* translators: %s: social link name e.g: \"Instagram\". */ "%s social icon" = "Icône de réseau social %s"; +/* translators: displayed right after the classic block is converted to blocks. %s: The localized classic block name */ +"'%s' block converted to blocks" = "Bloc « %s » converti en blocs"; + /* translators: Missing block alert title. %s: The localized block name */ "'%s' is not fully-supported" = "« %s » n’est pas entièrement pris en charge"; @@ -478,13 +475,6 @@ translators: Block name. %s: The localized block name */ /* Label for the Activity Type filter button when there are more than 1 activity type selected */ "Activity Type (%1$d)" = "Type d’activité (%1$d)"; -/* Accessibility label for add button to add items to the user's media library - Remove asset from media picker list */ -"Add" = "Ajouter"; - -/* Action for Media Picker to indicate selection of media. The argument in the string represents the number of elements (as numeric digits) selected */ -"Add %@" = "Ajouter %@"; - /* No comment provided by engineer. */ "Add Block After" = "Ajouter un bloc après"; @@ -581,9 +571,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Add menu item to children" = "Ajouter un élément de menu au sous-élément"; -/* Accessibility hint for add button to add items to the user's media library */ -"Add new media" = "Ajouter un nouveau média"; - /* Screen reader text for Menus button that adds a new menu to a site. */ "Add new menu" = "Ajouter un nouveau menu"; @@ -649,9 +636,6 @@ translators: Block name. %s: The localized block name */ /* Option to select the Airmail app when logging in with magic links */ "Airmail" = "Airmail"; -/* Description of albums in the photo libraries */ -"Albums" = "Albums"; - /* Image alignment option title. Title of the screen for choosing an image's alignment. */ "Alignment" = "Alignement"; @@ -665,6 +649,9 @@ translators: Block name. %s: The localized block name */ Title of the drafts filter. This filter shows a list of draft posts. */ "All" = "Toutes"; +/* Information about redeeming domain credit on site dashboard. */ +"All WordPress.com annual plans include a custom domain name. Register your free domain now." = "Tous les plans annuels WordPress.com incluent un nom de domaine personnalisé. Enregistrez votre domaine gratuit dès maintenant."; + /* Footer of the free domain registration section for a paid plan. Information about redeeming domain credit on site dashboard. */ "All WordPress.com plans include a custom domain name. Register your free premium domain now." = "Tous les plans WordPress.com inclus un nom de domaine personnalisé. Enregistrer votre domaine Premium gratuit dès maintenant."; @@ -730,10 +717,10 @@ translators: Block name. %s: The localized block name */ "Alt Text" = "Texte alternatif"; /* No comment provided by engineer. */ -"Alternatively, you can detach and edit these blocks separately by tapping “Detach patterns”." = "Vous pouvez également détacher et modifier ces blocs séparément en appuyant sur « Détacher les compositions »."; +"Alternatively, you can convert the content to blocks." = "Vous pouvez également convertir le contenu en blocs."; /* No comment provided by engineer. */ -"Alternatively, you can detach and edit this block separately by tapping “Detach pattern”." = "Vous pouvez également détacher et modifier ce bloc séparément en appuyant sur « Détacher la composition »."; +"Alternatively, you can detach and edit this block separately by tapping “Detach”." = "Vous pouvez également détacher et modifier ce bloc séparément en appuyant sur « Détacher »."; /* Instruction text to explain to help users type their password instead of using magic link login option. */ "Alternatively, you may enter the password for this account." = "Vous pouvez également saisir le mot de passe de ce compte."; @@ -882,15 +869,9 @@ translators: Block name. %s: The localized block name */ /* Message prompting the user to confirm that they want to disconnect Jetpack from the site. */ "Are you sure you want to disconnect Jetpack from the site?" = "Confirmez-vous vouloir déconnecter Jetpack de votre site ?"; -/* Message prompting the user to confirm that they want to permanently delete a group of media items. */ -"Are you sure you want to permanently delete these items?" = "Êtes-vous sûr de vouloir supprimer définitivement les éléments sélectionnés ?"; - /* Message prompting the user to confirm that they want to permanently delete a media item. Should match Calypso. */ "Are you sure you want to permanently delete this item?" = "Confirmez-vous vouloir supprimer définitivement cet élément ?"; -/* Message of the confirmation alert when deleting a page from the trash. */ -"Are you sure you want to permanently delete this page?" = "Confirmez-vous vouloir supprimer définitivement cette page ?"; - /* Message of the confirmation alert when deleting a post from the trash. */ "Are you sure you want to permanently delete this post?" = "Confirmez-vous vouloir supprimer définitivement cet article ?"; @@ -922,9 +903,6 @@ translators: Block name. %s: The localized block name */ /* Title of message shown when user taps submit for review. */ "Are you sure you want to submit for review?" = "Confirmez-vous vouloir envoyer pour relecture ?"; -/* Message of the trash page confirmation alert. */ -"Are you sure you want to trash this page?" = "Voulez-vous vraiment placer cette page dans la Corbeille ?"; - /* Message of the trash confirmation alert. */ "Are you sure you want to trash this post?" = "Confirmez-vous vraiment supprimer cet article ?"; @@ -965,9 +943,6 @@ translators: Block name. %s: The localized block name */ /* translators: accessibility text. Empty Audio caption. */ "Audio caption. Empty" = "Légende audio. Vide"; -/* Accessibility label for audio items in the media collection view. The parameter is the creation date of the audio. */ -"Audio, %@" = "Audio, %@"; - /* No comment provided by engineer. */ "Authenticating" = "Connexion"; @@ -1177,12 +1152,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Blocks menu" = "Menu Blocs"; -/* translators: Warning related to having blocks deeply nested. %d: The deepest nesting level. */ -"Blocks nested deeper than %d levels may not render properly in the mobile editor. For this reason, we recommend flattening the content by ungrouping the block or editing the block using the web editor." = "Les blocs imbriqués à un niveau supérieur à %d peuvent ne pas s’afficher correctement dans l’éditeur mobile. Pour cette raison, nous vous recommandons d’aplatir le contenu en dégroupant le bloc ou en le modifiant à l’aide de l’éditeur web."; - -/* translators: Warning related to having blocks deeply nested. %d: The deepest nesting level. */ -"Blocks nested deeper than %d levels may not render properly in the mobile editor. For this reason, we recommend flattening the content by ungrouping the block or editing the block using your web browser." = "Les blocs imbriqués à un niveau supérieur à %d peuvent ne pas s’afficher correctement dans l’éditeur mobile. Pour cette raison, nous vous recommandons d’aplatir le contenu en dégroupant le bloc ou en le modifiant à l’aide de votre navigateur web."; - /* Title of a button that displays the WordPress.com blog */ "Blog" = "Blog"; @@ -1259,9 +1228,6 @@ translators: Block name. %s: The localized block name */ /* Label for the post author in the post detail. */ "By " = "Par "; -/* Accessibility label for the post author in the post list. The parameter is the author name. For example, \"By Elsa.\" */ -"By %@." = "Par %@."; - /* Legal disclaimer for logging in. The underscores _..._ denote underline. */ "By continuing, you agree to our _Terms of Service_." = "En continuant, vous acceptez les _conditions d’utilisation_."; @@ -1281,8 +1247,7 @@ translators: Block name. %s: The localized block name */ /* Label for size of media while it's being calculated. */ "Calculating..." = "Calcul…"; -/* Accessibility label for taking an image or video with the camera on formatting toolbar. - Accessibility label for the camera tile in the collection view */ +/* Accessibility label for taking an image or video with the camera on formatting toolbar. */ "Camera" = "Appareil photo"; /* Title of an alert informing the user the camera permission for the app is disabled and its needed to proceed */ @@ -1333,10 +1298,7 @@ translators: Block name. %s: The localized block name */ Cancels an Action Cancels an alert. Cancels the mark all as read action. - Dismiss the post action sheet Editing GIF alert cancel action button. - Label for a cancel button - Label for the auto-upload cancelation button in the post list. Tapping will prevent the app from auto-uploading the post. Menus cancel button for deleting a menu. Menus cancel button within text bar while editing items. Menus: Cancel button title for canceling an edited menu item. @@ -1353,10 +1315,6 @@ translators: Block name. %s: The localized block name */ Verb. Title for Jetpack Restore cancel button. */ "Cancel" = "Annuler"; -/* Label for the Post List option that cancels automatic uploading of a post. - Media Library option to cancel an in-progress or failed upload. */ -"Cancel Upload" = "Annuler le téléversement"; - /* Menus alert button title to cancel discarding changes and not createa a new menu. Menus alert button title to cancel discarding changes and not select a new menu Menus alert button title to cancel discarding changes and not select a new menu location */ @@ -1415,9 +1373,6 @@ translators: Block name. %s: The localized block name */ Main title */ "Change Password" = "Changer le mot de passe"; -/* Title of button that takes user to the system Settings section for the app */ -"Change Settings" = "Modifier les réglages"; - /* Change Username title. */ "Change Username" = "Modifier l'identifiant"; @@ -1557,9 +1512,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Choose file" = "Choisir un fichier"; -/* Menu option for selecting media from the device's photo library. */ -"Choose from My Device" = "Choisir depuis mon appareil"; - /* Explanatory text for Homepage Settings homepage type selection. */ "Choose from a homepage that displays your latest posts (classic blog) or a fixed \/ static page." = "Choisir une page d’accueil listant les dernier articles (blog classique) ou un page fixe\/statique."; @@ -1760,9 +1712,6 @@ translators: Block name. %s: The localized block name */ /* Community & Non-Profit site intent topic */ "Community & Non-Profit" = "Communauté et association à but non-lucratif"; -/* Accessibility indication that the current Post List style is currently Compact. */ -"Compact" = "Compact"; - /* The action is completed */ "Completed" = "Terminé"; @@ -1948,10 +1897,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Copied block" = "Bloc copié"; -/* Copy the post url and paste anywhere in phone - Label for page copy link. Tapping copy the url of page */ -"Copy Link" = "Copier le lien"; - /* Copy link to oomment button title */ "Copy Link to Comment" = "Copier le lien dans le commentaire."; @@ -2060,9 +2005,6 @@ translators: Block name. %s: The localized block name */ /* Error title displayed when unable to close user account. */ "Couldn’t close account automatically" = "Fermeture automatique du compte impossible"; -/* Message to show while media data source is finding the number of items available. */ -"Counting media items..." = "Nombre d'éléments multimédias..."; - /* Period Stats 'Countries' header */ "Countries" = "Pays"; @@ -2313,7 +2255,6 @@ translators: Block name. %s: The localized block name */ /* Delete Delete button title for the warning shown to the user when he refuses to re-login when the authToken is missing. Title for button that permanently deletes a media item (photo / video) - Title for button that permanently deletes one or more media items (photos / videos) Title of a delete button Title of the trash confirmation alert. */ "Delete" = "Effacer"; @@ -2321,15 +2262,11 @@ translators: Block name. %s: The localized block name */ /* Menus confirmation button for deleting a menu. */ "Delete Menu" = "Supprimer le menu"; -/* Delete option in the confirmation alert when deleting a page from the trash. - Delete option in the confirmation alert when deleting a post from the trash. - Label for a button permanently deletes a page. - Label for the delete post option. Tapping permanently deletes a post. +/* Delete option in the confirmation alert when deleting a post from the trash. Title for button on the comment details page that deletes the comment when tapped. */ "Delete Permanently" = "Supprimer définitivement"; -/* Title of the confirmation alert when deleting a page from the trash. - Title of the confirmation alert when deleting a post from the trash. */ +/* Title of the confirmation alert when deleting a post from the trash. */ "Delete Permanently?" = "Supprimer définitivement ?"; /* Button label for deleting the current site @@ -2448,7 +2385,6 @@ translators: Block name. %s: The localized block name */ /* Accessibility label for button to dismiss a bottom sheet Accessibility label for the transparent space above the login dialog which acts as a button to dismiss the dialog. Accessibility label for the transparent space above the signup dialog which acts as a button to dismiss the dialog. - Action to show on alert when view asset fails. Dismiss a view. Verb */ "Dismiss" = "Ignorer"; @@ -2466,12 +2402,6 @@ translators: Block name. %s: The localized block name */ User's Display Name */ "Display Name" = "Nom affiché"; -/* Accessibility label for other media items in the media collection view. The parameter is the creation date of the document. */ -"Document, %@" = "Document : %@"; - -/* Accessibility label for other media items in the media collection view. The parameter is the filename file. */ -"Document: %@" = "Document : %@"; - /* Message shown when all tours have been completed */ "Doesn't it feel good to cross things off a list?" = "N’est-ce pas formidable de pouvoir rayer des éléments de la liste ?"; @@ -2632,8 +2562,7 @@ translators: Block name. %s: The localized block name */ /* Description of a Quick Start Tour */ "Draft and publish a post." = "Rédigez un brouillon et publiez un article de blog."; -/* Title of the drafts filter. This filter shows a list of draft posts. - Title of the drafts header in search list. */ +/* Title of the drafts filter. This filter shows a list of draft posts. */ "Drafts" = "Brouillons"; /* No comment provided by engineer. */ @@ -2645,10 +2574,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Drag to adjust focal point" = "Glisser pour ajuster le point focal"; -/* Label for page duplicate option. Tapping creates a copy of the page. - Label for post duplicate option. Tapping creates a copy of the post. */ -"Duplicate" = "Dupliquer"; - /* No comment provided by engineer. */ "Duplicate block" = "Dupliquer le bloc"; @@ -2661,13 +2586,9 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Each block has its own settings. To find them, tap on a block. Its settings will appear on the toolbar at the bottom of the screen." = "Chaque bloc possède ses propres réglages. Pour les trouver, appuyez sur un bloc. Ses réglages apparaissent sur la barre d’outils en bas de l’écran."; -/* Accessibility label for edit button to enable multi selection mode in the user's media library - Edit the post. - Editing GIF alert default action button. +/* Editing GIF alert default action button. Edits a Comment Edits the comment - Label for a button that opens the Edit Page view controller - Label for the edit post button. Tapping displays the editor. User action to edit media details. Verb, edit a comment */ "Edit" = "Modifier"; @@ -2675,9 +2596,6 @@ translators: Block name. %s: The localized block name */ /* Title for the edit more button section */ "Edit \"More\" button" = "Modifier le bouton \"Plus\""; -/* Button that displays the media editor to the user */ -"Edit %@" = "Modifier %@"; - /* Blocklist Keyword Edition Title */ "Edit Blocklist Word" = "Modifier le mot de la liste de blocage"; @@ -2870,9 +2788,6 @@ translators: Block name. %s: The localized block name */ /* Secondary message shown when there are no domains that match the user entered text. */ "Enter different words above and we'll look for an address that matches it." = "Saisissez des mots différents ci-dessus et nous regarderons si une adresse correspond."; -/* Accessibility hint for edit button to enable multi selection mode in the user's media library */ -"Enter edit mode to enable multi select to delete" = "Passez en mode d’édition pour activer la suppression multiple"; - /* (placeholder) Help enter WordPress password Placeholder of a field to type a password to protect the post. */ "Enter password" = "Saisissez un mot de passe"; @@ -3028,9 +2943,6 @@ translators: Block name. %s: The localized block name */ /* Short title telling the user they will receive a blogging reminder every day of the week. */ "Every day at %@" = "Tous les jours à %@"; -/* Label for the post author filter. This filter shows posts for all users on the blog. */ -"Everyone" = "Tout le monde"; - /* Example story title description */ "Example story title" = "Exemple de titre de story"; @@ -3040,9 +2952,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Excerpt length (words)" = "Longueur de l’extrait (mots)"; -/* Accessibility label for a post's excerpt in the post list. The parameter is the post excerpt. For example, \"Excerpt. This is the first paragraph.\" */ -"Excerpt. %@." = "Extrait. %@."; - /* Should be the same as the text displayed if the user clicks the (i) in Calypso. */ "Excerpts are optional hand-crafted summaries of your content." = "Les extraits sont des résumés optionnels de votre contenu."; @@ -3052,8 +2961,7 @@ translators: Block name. %s: The localized block name */ /* Accessibility Label for the exit full screen button on the full screen comment reply mode */ "Exit Full Screen" = "Sortir du plein écran"; -/* Accessibility indication that the current Post List style is currently Expanded. - Screen reader text to represent the expanded state of a UI control */ +/* Screen reader text to represent the expanded state of a UI control */ "Expanded" = "Développé"; /* Accessibility hint */ @@ -3103,9 +3011,6 @@ translators: Block name. %s: The localized block name */ The action failed */ "Failed" = "Échec"; -/* Error title when picked media cannot be imported into stories. */ -"Failed Media Export" = "Échec de l’exportation du média"; - /* Message for mark all as read success notice */ "Failed marking Notifications as read" = "Impossible de marquer les notifications comme lues"; @@ -3624,8 +3529,7 @@ translators: Block name. %s: The localized block name */ "Home" = "Accueil"; /* Title for setting which shows the current page assigned as a site's homepage - Title for the homepage section in site settings screen - Title of the Homepage Badge */ + Title for the homepage section in site settings screen */ "Homepage" = "Page d’accueil"; /* Label for Homepage Settings site settings section @@ -3722,9 +3626,6 @@ translators: Block name. %s: The localized block name */ /* Hint for image title on image settings. */ "Image title" = "Titre de l’image"; -/* Accessibility label for image thumbnails in the media collection view. The parameter is the creation date of the image. */ -"Image, %@" = "Image, %@"; - /* Undated post time label */ "Immediately" = "Immédiatement"; @@ -4210,9 +4111,6 @@ translators: Block name. %s: The localized block name */ Settings: Comments Approval settings */ "Links in comments" = "Liens dans les commentaires"; -/* The accessibility label for the list style button in the Post List. */ -"List style" = "Style de liste"; - /* Title of the screen that load selected the revisions. */ "Load" = "Charger"; @@ -4228,18 +4126,12 @@ translators: Block name. %s: The localized block name */ /* Text displayed while loading the activity feed for a site */ "Loading Backups..." = "Chargement des sauvegardes en cours..."; -/* Phrase to show when the user has searched for GIFs and they are being loaded. */ -"Loading GIFs..." = "Chargement des GIF…"; - /* Menus label text displayed while menus are loading */ "Loading Menus..." = "Chargement des menus en cours..."; /* Text displayed while loading site People. */ "Loading People..." = "Chargement des gens…"; -/* Phrase to show when the user has searched for images and they are being loaded. */ -"Loading Photos..." = "Chargement des photos…"; - /* Text displayed while loading plans details */ "Loading Plan..." = "Chargement des offres..."; @@ -4300,8 +4192,7 @@ translators: Block name. %s: The localized block name */ /* Local Services site intent topic */ "Local Services" = "Services locaux"; -/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. - Title of the Local Changes Badge */ +/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. */ "Local changes" = "Changements locaux"; /* Title for alert when a generic error happened when trying to find the location of the device */ @@ -4465,7 +4356,6 @@ translators: Block name. %s: The localized block name */ "Max Video Upload Size" = "Taille maximum d’envoi de vidéo."; /* Accessibility label for the Me button in My Site. - Label for the post author filter. This filter shows posts only authored by the current user. Me page title Noun. Title. Links to the Me screen. The accessibility value of the me tab. @@ -4473,9 +4363,7 @@ translators: Block name. %s: The localized block name */ "Me" = "Moi"; /* Noun. Title. Links to the blog's Media library. - Tab bar title for the Media tab in Media Picker The menu item to select during a guided tour. - Title for Media Library section of the app. Title for the media section in site settings screen Title label for the media settings section in the app settings */ "Media" = "Média"; @@ -4487,13 +4375,6 @@ translators: Block name. %s: The localized block name */ /* Label for size of media cache in the app. */ "Media Cache Size" = "Taille du cache du média"; -/* Title for alert when access to media capture is not granted */ -"Media Capture" = "Capture de médias"; - -/* Title for alert when a generic error happened when loading media - Title for alert when access to the media library is not granted by the user */ -"Media Library" = "Médiathèque"; - /* Title for action sheet with media options. */ "Media Options" = "Options de média"; @@ -4516,9 +4397,6 @@ translators: Block name. %s: The localized block name */ /* translators: %s: block title e.g: \"Paragraph\". */ "Media options" = "Options de média"; -/* Alert title when there is issues loading an asset to preview. */ -"Media preview failed." = "L’aperçu du média a échoué."; - /* Alert displayed to the user when multiple media items have uploaded successfully. */ "Media uploaded (%ld files)" = "Médias téléversés (%ld fichiers)"; @@ -4556,9 +4434,6 @@ translators: Block name. %s: The localized block name */ Label for the share message field on the post settings. */ "Message" = "Message"; -/* Title of section containing image / video metadata such as size and file type */ -"Metadata" = "Méta-donnée"; - /* Option to select the Microsft Outlook app when logging in with magic links */ "Microsoft Outlook" = "Microsoft Outlook"; @@ -4578,13 +4453,11 @@ translators: Block name. %s: The localized block name */ "Months and Years" = "Mois et Années"; /* Accessibility label for more button in dashboard quick start card. - Accessibility label for the More button in Page List. Accessibility label for the More button in Post List (compact view). Accessibility label for the More button on formatting toolbar. Accessibility label for the More button on Reader Cell Accessibility label for the More button on Reader's post details - Action button to display more available options - Label for the more post button. Tapping displays an action sheet with post options. */ + Action button to display more available options */ "More" = "Plus"; /* Action button to display more available options @@ -4642,15 +4515,8 @@ translators: Block name. %s: The localized block name */ /* Screen reader text for button that will move the menu item */ "Move menu item" = "Déplacer l’élément de menu"; -/* Label for a button that moves a page to the draft folder - Label for an option that moves a post to the draft folder */ -"Move to Draft" = "Passer en brouillon"; - -/* Label for a button that moves a page to the trash folder - Label for a option that moves a post to the trash folder - Title for button on the comment details page that moves the comment to trash when tapped. +/* Title for button on the comment details page that moves the comment to trash when tapped. Trash option in the trash confirmation alert. - Trash option in the trash page confirmation alert. Trashes the comment */ "Move to Trash" = "Mettre dans la Corbeille"; @@ -4682,7 +4548,8 @@ translators: Block name. %s: The localized block name */ Title of My Site tab */ "My Site" = "Mon site"; -/* Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ +/* Title for site picker screen. + Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ "My Sites" = "Mes sites"; /* Siri Suggestion to open My Sites */ @@ -4932,9 +4799,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title for the view when there aren't any Activities to display in the Activity Log for a given filter. */ "No matching events found." = "Aucun événement ne correspond."; -/* Message displayed when no results are returned from a media library search. Should match Calypso. - Phrase to show when the user search for images but there are no result to show. - Phrase to show when the user searches for GIFs but there are no result to show. */ +/* Message displayed when no results are returned from a media library search. Should match Calypso. */ "No media matching your search" = "Aucun média ne correspond à votre recherche"; /* Menus text shown when no menus were available for loading the Menus editor. */ @@ -4952,8 +4817,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Displayed in the Notifications Tab as a title, when there are no notifications */ "No notifications yet" = "Aucune notification pour l'instant"; -/* Displayed when the user is searching the pages list and there are no matching pages - Text displayed when there's no matching with the text search */ +/* Text displayed when there's no matching with the text search */ "No pages matching your search" = "Aucune page ne correspond à votre recherche"; /* Text displayed when search for plugins returns no results */ @@ -4974,9 +4838,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Message shown whent the reader finds no posts for the chosen tag */ "No posts have been made recently with this tag." = "Aucun article n'a été publié récemment avec cette étiquette."; -/* Displayed when the user is searching the posts list and there are no matching posts */ -"No posts matching your search" = "Aucun article ne correspond à votre recherche"; - /* Accessibility value for a Stats' Posting Activity Month if there are no posts. */ "No posts." = "Aucun article."; @@ -5077,9 +4938,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* A message title */ "Nothing liked yet" = "Aucun « J'aime » pour l’instant"; -/* Default message for empty media picker */ -"Nothing to show" = "Rien à afficher"; - /* Notifications Details Accessibility Identifier */ "Notification Details Table" = "Tableau des détails de notification"; @@ -5139,7 +4997,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Alert dismissal title Button title. Acknowledges a prompt. Button title. An acknowledgement of the message displayed in a prompt. - Confirmation of action Default action Dismisses the alert Marks all notifications as read. @@ -5201,9 +5058,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* No comment provided by engineer. */ "Only show excerpt" = "N’afficher que l’extrait"; -/* Message telling the user that they've only enabled limited photo library permissions for the app. */ -"Only the selected photos you've given access to are available." = "Seules les photos sélectionnées auxquelles vous avez donné accès sont disponibles."; - /* An informal exclaimation that means `something went wrong`. Title for the view when there's an error loading a comment. Title for the view when there's an error loading Activity Log @@ -5238,9 +5092,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title of a button that opens the apps settings in the system Settings.app */ "Open Settings" = "Ouvrir les paramètres"; -/* Editor button to swich the media picker from quick mode to full picker */ -"Open full media picker" = "Ouvrir le sélecteur de média complet."; - /* No comment provided by engineer. */ "Open in Safari" = "Ouvrir dans Safari"; @@ -5280,6 +5131,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Divider on initial auth view separating auth options. */ "Or" = "Ou"; +/* Instruction text for other forms of two-factor auth methods. */ +"Or choose another form of authentication." = "Vous pouvez aussi choisir un autre mode d'authentification."; + /* Label for button to log in using site address. Underscores _..._ denote underline. */ "Or log in by _entering your site address_." = "Ou connectez-vous en _saisissez l’adresse de votre site_."; @@ -5338,15 +5192,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title shown when selecting a post type of Page from the Share Extension. */ "Page" = "Page"; -/* Prompts the user that a restored page was moved to the drafts list. */ -"Page Restored to Drafts" = "Page rétablie dans Brouillons"; - -/* Prompts the user that a restored page was moved to the published list. */ -"Page Restored to Published" = "Page rétablie pour Publiée"; - -/* Prompts the user that a restored page was moved to the scheduled list. */ -"Page Restored to Scheduled" = "Page rétablie pour Planifiée"; - /* Name of the button to open the page settings The title of the Page Settings screen. */ "Page Settings" = "Réglages de la page"; @@ -5363,9 +5208,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of notification displayed when a page has failed to upload. */ "Page failed to upload" = "Le téléversement de la page a échoué"; -/* A short message explaining that a page was moved to the trash bin. */ -"Page moved to trash." = "Page déplacée dans la Corbeille."; - /* Title of notification displayed when a page has been successfully saved as a draft. */ "Page pending review" = "Page en attente de relecture"; @@ -5437,8 +5279,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title of pending Comments filter. */ "Pending" = "En attente"; -/* Name for the status of a post pending review. - Title of the Pending Review Badge */ +/* Name for the status of a post pending review. */ "Pending review" = "En attente de relecture"; /* Noun. Title of the people management feature. @@ -5467,12 +5308,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Photography site intent topic */ "Photography" = "Photographie"; -/* Tab bar title for the Photos tab in Media Picker */ -"Photos" = "Photos"; - -/* Subtitle for placeholder in Free Photos. The company name 'Pexels' should always be written as it is. */ -"Photos provided by Pexels" = "Photos mises à disposition par Pexels"; - /* Title for selecting a new username in the site creation flow. */ "Pick username" = "Sélectionner un identifiant"; @@ -5565,7 +5400,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Please enter the password for your WordPress.com account to log in with your Apple ID." = "Veuillez saisir le mot de passe de votre compte WordPress.com, afin de vous connecter avec votre identifiant Apple ID."; /* Instruction text on the two-factor screen. */ -"Please enter the verification code from your authenticator app, or tap the link below to receive a code via SMS." = "Veuillez saisir le code de vérification de votre application d’authentification ou toucher le lien ci-dessous pour recevoir un code par SMS."; +"Please enter the verification code from your authenticator app." = "Veuillez saisir le code de vérification de votre application d’authentification."; /* Popup message to ask for user credentials (fields shown below). */ "Please enter your credentials" = "Veuillez saisir vos identifiants"; @@ -5660,15 +5495,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ The post formats available for the post. Should be the same as in core WP. */ "Post Format" = "Format d'article"; -/* Prompts the user that a restored post was moved to the drafts list. */ -"Post Restored to Drafts" = "Article rétabli dans Brouillons"; - -/* Prompts the user that a restored post was moved to the published list. */ -"Post Restored to Published" = "Article rétabli pour publication"; - -/* Prompts the user that a restored post was moved to the scheduled list. */ -"Post Restored to Scheduled" = "Article rétabli pour planification"; - /* Name of the button to open the post settings The title of the Post Settings screen. */ "Post Settings" = "Réglages d’article"; @@ -5688,9 +5514,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of notification displayed when a post has failed to upload. */ "Post failed to upload" = "Le téléversement de l’article a échoué"; -/* A short message explaining that a post was moved to the trash bin. */ -"Post moved to trash." = "Article déplacé dans la Corbeille."; - /* Title of notification displayed when a post has been successfully saved as a draft. */ "Post pending review" = "Article en attente de relecture"; @@ -5749,9 +5572,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Period Stats 'Posts and Pages' header */ "Posts and Pages" = "Articles et pages"; -/* Title of the Posts Page Badge */ -"Posts page" = "Page des articles"; - /* Message informing the user that their static homepage for posts was set successfully */ "Posts page successfully updated" = "Page des articles mise à jour avec succès"; @@ -5764,9 +5584,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* A message explaining the Posts I Like feature in the reader */ "Posts that you like will appear here." = "Les articles que vous aimez apparaîtront ici."; -/* Subtitle for placeholder in Tenor picker. `The company name 'Tenor' should always be written as it is. */ -"Powered by Tenor" = "Propulsé par Tenor"; - /* Browse premium themes selection title */ "Premium" = "Premium"; @@ -5785,18 +5602,12 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title for screen to preview a static content. */ "Preview" = "Prévisualisation "; -/* Action for Media Picker to preview the selected media items. The argument in the string represents the number of elements (as numeric digits) selected */ -"Preview %@" = "Aperçu de %@"; - /* Title for web preview device switching button */ "Preview Device" = "Appareil utilisé pour l’aperçu"; /* Title on display preview error */ "Preview Unavailable" = "Aperçu non disponible"; -/* Accessibility label for media item preview for user's viewing an item in their media library */ -"Preview media" = "Prévisualiser le média"; - /* No comment provided by engineer. */ "Preview page" = "Prévisualiser la page"; @@ -5843,8 +5654,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Privacy notice for California users" = "Avis de confidentialité pour les utilisateurs californiens"; /* Name for the status of a post that is marked private. - Privacy setting for posts set to 'Private'. Should be the same as in core WP. - Title of the Private Badge */ + Privacy setting for posts set to 'Private'. Should be the same as in core WP. */ "Private" = "Privé"; /* No comment provided by engineer. */ @@ -5894,12 +5704,10 @@ translators: %s: Select control button label e.g. \"Button width\" */ Label for the publish date button. */ "Publish Date" = "Date de publication"; -/* A short phrase indicating a post is due to be immedately published. - Label for a button that moves a page to the published folder, publishing with the current date/time. */ +/* A short phrase indicating a post is due to be immedately published. */ "Publish Immediately" = "Publier immédiatement"; /* Label for a button that publishes the post - Label for an option that moves a publishes a post immediately Title of button allowing the user to immediately publish the post they are editing. */ "Publish Now" = "Publier maintenant"; @@ -5917,8 +5725,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Name for the status of a published post. Period Stats 'Published' header - Title of the published filter. This filter shows a list of posts that the user has published. - Title of the published header in search list. */ + Title of the published filter. This filter shows a list of posts that the user has published. */ "Published" = "Publié"; /* Precedes the name of the blog just posted on */ @@ -6060,8 +5867,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of the completion screen of the Blogging Reminders Settings screen when the reminders are removed. */ "Reminders removed" = "Rappels supprimés"; -/* Add asset to media picker list - Alert button to confirm a plugin to be removed +/* Alert button to confirm a plugin to be removed Button label when removing a blog Label for the Remove Feature Image icon. Tapping will show a confirmation screen for removing the feature image from the post. Remove Action @@ -6214,9 +6020,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of secondary button on alert prompting verify their accounts while attempting to publish */ "Resend" = "Renvoyer"; -/* Title of the reset button */ -"Reset" = "Mettre à zéro"; - /* Accessibility label for the reset activity type button */ "Reset Activity Type filter" = "Réinitialiser le filtre Type d’activité"; @@ -6271,12 +6074,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ Button title, displayed when media has failed to upload. Allows the user to try the upload again. Button title. Retries uploading a post. If a user taps the button with this label, the action that evinced this error view will be retried. - Label for a button that attempts to re-upload a page that previously failed to upload. - Label for the retry post upload button. Tapping attempts to upload the post again. Opens the media library . Retries the upload of a user's post. Retry updating User's Role - Retry uploading the post. Retry. Action Retry. Verb – retry a failed media upload. The Jetpack view button title used when an error occurred @@ -6288,9 +6088,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Button title that triggers a scan */ "Retry Scan" = "Relancer l’analyse"; -/* User action to retry media upload. */ -"Retry Upload" = "Téléverser à nouveau"; - /* User action to retry all failed media uploads. */ "Retry all" = "Tout réessayez "; @@ -6388,9 +6185,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Accessibility label for the 'Save Post' button when a post has been saved. */ "Saved Post" = "Article enregistré"; -/* Text displayed in HUD when a media item's metadata (title, etc) is saved successfully. */ -"Saved!" = "Enregistré !"; - /* Accessibility hint for the 'Save Post' button. */ "Saves this post for later." = "Enregistrer cet article pour plus tard."; @@ -6401,7 +6195,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Saving post…" = "Enregistrement de l’article…"; /* Menus save button title while it is saving a Menu. - Text displayed in HUD while a media item's metadata (title, etc) is being saved. Text displayed in HUD while a post is being saved as a draft. */ "Saving..." = "Enregistrement en cours..."; @@ -6492,21 +6285,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* No comment provided by engineer. */ "Search or type URL" = "Rechercher ou saisir une URL"; -/* Text displayed when the search controller will be presented */ -"Search pages" = "Chercher des pages"; - -/* Text displayed when the search controller will be presented */ -"Search posts" = "Rechercher des publications"; - /* No comment provided by engineer. */ "Search settings" = "Paramètres de recherche"; -/* Title for placeholder in Tenor picker */ -"Search to find GIFs to add to your Media Library!" = "Recherchez des images GIF gratuites pour les ajouter à votre médiathèque !"; - -/* Title for placeholder in Free Photos */ -"Search to find free photos to add to your Media Library!" = "Recherchez des photos gratuites pour les ajouter à votre médiathèque !"; - /* Menus search bar placeholder text. */ "Search..." = "Rechercher…"; @@ -6577,9 +6358,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Register Domain - Domain contact information field placeholder for Country */ "Select Country" = "Choisir le pays"; -/* Title of button that allows the user to select more photos to access within the app */ -"Select More" = "En sélectionner plus"; - /* Blog Picker's Title */ "Select Site" = "Sélectionner le site"; @@ -6601,9 +6379,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Register domain - Title for the Choose domain button of Suggested domains screen */ "Select domain" = "Sélectionner un domaine"; -/* Accessibility hint for actions when displaying media items. */ -"Select media." = "Sélectionner un média."; - /* Accessibility label for selecting paragraph style button on formatting toolbar. */ "Select paragraph style" = "Sélectionner le style de paragraphe"; @@ -6707,19 +6482,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label for connected service in Publicize stat. */ "Service" = "Service"; -/* Label for a button that opens the Set Parent options view controller - Navigation title displayed on the navigation bar */ +/* Navigation title displayed on the navigation bar */ "Set Parent" = "Configurer le parent"; /* No comment provided by engineer. */ "Set as Featured Image" = "Définir en tant qu’image mise en avant"; -/* Label for a button that sets the selected page as the site's Homepage */ -"Set as Homepage" = "Définir comme page d’accueil"; - -/* Label for a button that sets the selected page as the site's Posts page */ -"Set as Posts Page" = "Définir comme page des articles"; - /* Notice confirming that an image has been set as the post's featured image. */ "Set as featured image" = "Définir en tant qu’image mise en avant"; @@ -6763,7 +6531,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Accessibility label for share buttons in nav bars Button label to share a post Button label to share a web page - Share the post. Shares the comment URL Spoken accessibility label Title for a button that allows the user to share their answer to the prompt. @@ -7149,8 +6916,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Name of setting configured when a site uses a static page as its homepage */ "Static Homepage" = "Page d’accueil statique"; -/* Label for post stats option. Tapping displays statistics for a post. - Noun. Abbreviation of Statistics. Name of the Stats feature +/* Noun. Abbreviation of Statistics. Name of the Stats feature Noun. Abbv. of Statistics. Links to a blog's Stats screen. Stats 3D Touch Shortcut Stats window title @@ -7181,9 +6947,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label text that defines a post marked as sticky */ "Sticky" = "Épinglé"; -/* Accessibility label for a sticky post in the post list. */ -"Sticky." = "Épinglé"; - /* User action to stop upload. */ "Stop upload" = "Arrêter le téléversement"; @@ -7240,7 +7003,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ Theme Support action title */ "Support" = "Support"; -/* Button used to switch site */ +/* Title for back button that leads to the site picker screen. */ "Switch Site" = "Changer de site"; /* Switches the Editor to HTML Mode */ @@ -7328,9 +7091,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label explaining why users might want to add tags. */ "Tags help tell readers what a post is about. Separate different tags with commas." = "Les étiquettes aident à préciser au lecteur le sujet de l’article. Séparer les différentes étiquettes avec des virgules."; -/* Menu option for taking an image or video with the device's camera. */ -"Take Photo or Video" = "Prenez un photo ou un vidéo"; - /* No comment provided by engineer. */ "Take a Photo" = "Prendre une photo"; @@ -7401,12 +7161,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Accessibility hint */ "Tap to select the previous period" = "Toucher pour choisir la période précédente"; -/* Accessibility hint for button used to switch site */ -"Tap to switch to another site, or add a new site" = "Appuyer pour passer à un autre site, ou ajouter un nouveau site"; - -/* Accessibility hint for media item preview for user's viewing an item in their media library */ -"Tap to view media in full screen" = "Appuyer pour voir le média en plein écran"; - /* Accessibility hint for a button that opens a new view with more details. */ "Tap to view more details." = "Appuyez pour afficher plus de détails."; @@ -7452,10 +7206,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* No comment provided by engineer. */ "Text formatting controls are located within the toolbar positioned above the keyboard while editing a text block" = "Les contrôles de mise en forme du texte se trouvent dans la barre d’outils placée au-dessus du clavier lors de l’édition d’un bloc paragraphe"; -/* Button title - The button's title text to send a 2FA code via SMS text message. */ +/* Button title */ "Text me a code instead" = "Envoyez-moi un code à la place."; +/* The button's title text to send a 2FA code via SMS text message. */ +"Text me a code via SMS" = "M’envoyer un code par SMS"; + /* Message of alert when theme activation succeeds */ "Thanks for choosing %@ by %@" = "Merci d'avoir choisi %1$@ de %2$@"; @@ -7483,9 +7239,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Error message shown to a user who is trying to share to Facebook but does not have any available Facebook Pages. */ "The Facebook connection cannot find any Pages. Publicize cannot connect to Facebook Profiles, only published Pages." = "La connexion à Facebook ne trouve aucune page. Publicize ne peut pas se connecter aux profils, seulement au pages publiées."; -/* Message shown when a GIF failed to load while trying to add it to the Media library. */ -"The GIF could not be added to the Media Library." = "Les GIF ne peuvent pas être ajoutés à la médiathèque."; - /* Description shown when a user logs in with Google but no matching WordPress.com account is found */ "The Google account \"%@\" doesn't match any account on WordPress.com" = "Le compte Google « %@ » ne correspond pas avec le compte WordPress.com."; @@ -7613,7 +7366,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Error message shown when user attempts to remove the site owner. */ "The user you are trying to remove is the owner of this site. Please contact support for assistance." = "L’utilisateur que vous essayez de retirer est le propriétaire de ce site. Veuillez contacter le support pour assistance."; -/* Error message informing a user about an invalid password. */ +/* No comment provided by engineer. */ "The username or password stored in the app may be out of date. Please re-enter your password in the settings and try again." = "Le mot de passe et l’identifiant stockés dans l'application peuvent ne pas être à jour. Veuillez saisir à nouveau votre mot de passe dans les réglages, puis réessayer."; /* Message shown when a video failed to load while trying to add it to the Media library. */ @@ -7681,9 +7434,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* A short error message letting the user know about a problem displaying a post. */ "There was a problem displaying this post." = "Un problème est survenu lors de l’affichage de cet article."; -/* Error message displayed when the Media Library is unable to load a full sized preview of an item. */ -"There was a problem loading the media item." = "Il y a eu un problème de chargement de l’élément multimédia."; - /* The loading view subtitle displayed when an error occurred */ "There was a problem loading your data, refresh your page to try again." = "Un problème a eu lieu lors du chargement des données. Rafraichissez votre page pour réessayer."; @@ -7696,9 +7446,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Explaining to the user there was an error trying to obtain the current location of the user. */ "There was a problem when trying to access your location. Please try again later." = "Il y a eu un problème en tentant d'accéder à votre localisation. Veuillez essayer à nouveau."; -/* Explaining to the user there was an generic error accesing media. */ -"There was a problem when trying to access your media. Please try again later." = "Un problème est survenu lors de la tentative d'accès à votre appareil multimédia. Réessayez ultérieurement."; - /* Message for stories unknown error. */ "There was a problem with the Stories editor. If the problem persists you can contact us via the Me > Help & Support screen." = "Un problème est survenu avec l’éditeur Stories. Si le problème persiste, vous pouvez nous contacter via l’écran Moi > Aide & Support."; @@ -7769,9 +7516,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* A description informing the user in order to proceed with this feature we will need camera permissions, and how to enable it. */ "This app needs permission to access the Camera to scan login codes, tap on the Open Settings button to enable it." = "Cette application a besoin d’une autorisation pour accéder à l’appareil photo afin de scanner les codes de connexion. Appuyez sur Ouvrir les réglages pour l’activer."; -/* Explaining to the user why the app needs access to the device media library. */ -"This app needs permission to access your device media library in order to add photos and\/or video to your posts. Please change the privacy settings if you wish to allow this." = "Cette application nécessite une autorisation pour accéder à la médiathèque de votre appareil, afin d’ajouter des photos et\/ou une vidéo à vos articles. Pour ce faire, veuillez modifier les réglages de confidentialité."; - /* No comment provided by engineer. */ "This color combination may be hard for people to read. Try using a brighter background color and\/or a darker text color." = "Cette combinaison de couleurs peut gêner la lecture chez certaines personnes. Essayez une couleur d’arrière-plan plus claire et\/ou une couleur de texte plus foncée."; @@ -7881,6 +7625,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Description of a Quick Start Tour */ "Time to finish setting up your site! Our checklist walks you through the next steps." = "C’est le moment de terminer les réglages de votre ! Notre check-list vous amènera à la prochaine étape."; +/* Error when the uses takes more than 1 minute to submit a security key. */ +"Time's up, but don't worry, your security is our priority. Please try again!" = "Le temps est écoulé, mais ne vous inquiétez pas : votre sécurité est notre priorité. Veuillez réessayer."; + /* WordPress.com Marketing Footer Text */ "Tips for getting the most out of WordPress.com." = "Conseils pour tirer le meilleur de WordPress.com."; @@ -8004,24 +7751,20 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title for the traffic section in site settings screen */ "Traffic" = "Trafic"; +/* Describes a domain that was transferred from elsewhere to wordpress.com */ +"Transferred Domain" = "Domaine transféré"; + /* translators: %s: block title e.g: \"Paragraph\". */ "Transform %s to" = "Transformer %s en"; /* No comment provided by engineer. */ "Transform block…" = "Transformer le bloc…"; -/* Accessibility label for trash button to delete items from the user's media library - Accessibility label for trash buttons in nav bars +/* Accessibility label for trash buttons in nav bars Trashes a comment Trashes the comment */ "Trash" = "Déplacer vers la corbeille"; -/* Accessibility hint for trash button to delete items from the user's media library */ -"Trash selected media" = "Mettre à la corbeille le fichier média sélectionné"; - -/* Title of the trash page confirmation alert. */ -"Trash this page?" = "Déplacer cette page vers la corbeille ?"; - /* Title of the trash confirmation alert. */ "Trash this post?" = "Mettre à la corbeille cet article ?"; @@ -8139,9 +7882,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Shown when a user logs in with Google but it subsequently fails to work as login to WordPress.com */ "Unable To Connect" = "Impossible de connecter"; -/* An error message. */ -"Unable to Connect" = "Connexion impossible"; - /* Title for stories unknown error. */ "Unable to Create Stories Editor" = "Impossible de créer un éditeur Stories"; @@ -8157,9 +7897,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* An error message shown when there is an issue creating new invite links. */ "Unable to create new invite links." = "Impossible de créer de nouveaux liens d’invitation."; -/* Text displayed in HUD if there was an error attempting to delete a group of media items. */ -"Unable to delete all media items." = "Impossible de supprimer tous les médias."; - /* Text displayed in HUD if there was an error attempting to delete a media item. */ "Unable to delete media item." = "Impossible de supprimer le média."; @@ -8223,12 +7960,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Message displayed when sharing a link to the downloadable backup fails. */ "Unable to share link" = "Impossible de partager le lien"; -/* Message that appears when a user tries to trash a page while their device is offline. */ -"Unable to trash pages while offline. Please try again later." = "Impossible de déplacer des pages vers la corbeille en étant hors ligne. Veuillez réessayer plus tard."; - -/* Message that appears when a user tries to trash a post while their device is offline. */ -"Unable to trash posts while offline. Please try again later." = "Impossible de mettre des publications à la corbeille en mode hors connexion. Veuillez réessayer ultérieurement."; - /* Notice title when turning site notifications off fails. */ "Unable to turn off site notifications" = "Impossible de désactiver les notifications de site"; @@ -8301,8 +8032,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ Button title. Reverts the previous notification operation Revert an operation Revert enabling notification after successfully subcribing to the comments for the post. - The title of an 'undo' button. Tapping the button moves a trashed page out of the trash folder. - The title of an 'undo' button. Tapping the button moves a trashed post out of the trash folder. Undo action */ "Undo" = "Annuler"; @@ -8345,9 +8074,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title for Unknown HTML Editor */ "Unknown HTML" = "HTML inconnu"; -/* Label to use when creation date from media asset is not know. */ -"Unknown creation date" = "Date de création inconnue"; - /* No comment provided by engineer. */ "Unknown error" = "Erreur inconnue"; @@ -8513,6 +8239,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of a row displayed on the debug screen used to configure the sandbox store use in the App. */ "Use Sandbox Store" = "Utiliser une boutique « bac à sable »"; +/* The button's title text to use a security key. */ +"Use a security key" = "Utiliser une clé de sécurité"; + /* Option to enable the block editor for new posts */ "Use block editor" = "Utiliser l’éditeur de blocs"; @@ -8588,15 +8317,10 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of an alert informing users that the video they are trying to select is not allowed. */ "Video not uploaded" = "La vidéo n’a pas été mise en ligne"; -/* Accessibility label for video thumbnails in the media collection view. The parameter is the creation date of the video. */ -"Video, %@" = "Vidéo, %@"; - /* Period Stats 'Videos' header */ "Videos" = "Vidéos"; /* Button title. Displays a summary / sharing screen for a specific post. - Label for a button that opens the page when tapped. - Label for the view post button. Tapping displays the post as it appears on the web. Opens the post epilogue screen to allow sharing / viewing of a post. Theme View action title Verb. The screen title shown when viewing a post inside the app. */ @@ -8711,6 +8435,10 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Message shown on screen while waiting for Google to finish its signup process. */ "Waiting for Google to complete…" = "En attente de la fin du processus Google…"; +/* Text while the webauthn signature is being verified + Text while waiting for a security key challenge */ +"Waiting for security key" = "En attente de la clé de sécurité"; + /* View title during the Google auth process. */ "Waiting..." = "En attente…"; @@ -9082,6 +8810,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* An error message shown when a wpcom user provides the wrong password. */ "Whoops, something went wrong and we couldn't log you in. Please try again!" = "Oups, quelque chose s’est mal passé et nous ne pouvons pas vous connecter. Veuillez réessayer !"; +/* Generic error on the 2FA screen */ +"Whoops, something went wrong. Please try again!" = "Oups, une erreur s’est produite. Veuillez réessayer."; + +/* Error when the uses chooses an invalid security key on the 2FA screen. */ +"Whoops, that security key does not seem valid. Please try again with another one" = "Oups, cette clé de sécurité ne semble pas valide. Veuillez réessayer avec une autre clé"; + /* Error message shown when an incorrect two factor code is provided. */ "Whoops, that's not a valid two-factor verification code. Double-check your code and try again!" = "Oups, ce n’est pas un code de vérification deux facteurs valide. Vérifiez à nouveau et réessayez."; @@ -9109,9 +8843,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Siri Suggestion to open Support */ "WordPress Help" = "Aide de WordPress"; -/* Name for the WordPress Media Library */ -"WordPress Media" = "Média WordPress"; - /* Accessibility label for selecting an image or video from the user's WordPress media library on formatting toolbar. */ "WordPress Media Library" = "Médiathèque WordPress"; @@ -9426,9 +9157,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Share extension error dialog text. */ "Your account does not have permission to upload media to this site. The Site Administrator can change these permissions." = "Votre compte n’a pas le droit de téléverser de média sur ce site. L’administrateur du site peut changer ces droits."; -/* Explaining to the user why the app needs access to the device media library. */ -"Your app is not authorized to access media library due to active restrictions such as parental controls. Please check your parental control settings in this device." = "Votre app n’est pas autorisé à accéder à la médiathèque en raison d’une restriction active telle que le contrôle parental. Veuillez vérifier les réglages de contrôle parental sur cet appareil."; - /* Title for the Jetpack Backup Complete message. */ "Your backup is now available for download" = "Votre sauvegarde peut désormais être téléchargée"; @@ -9447,9 +9175,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of the site address section in the Domains Dashboard. */ "Your free WordPress.com address is" = "Votre adresse gratuite WordPress.com est"; -/* Error message when picked media cannot be imported into stories. */ -"Your media could not be exported. If the problem persists you can contact us via the Me > Help & Support screen." = "Votre média n’a pas pu être exporté. If the problem persists you can contact us via the Me > écran Aide & Support."; - /* Details about recently acquired domain on domain credit redemption success screen */ "Your new domain %@ is being set up. It may take up to 30 minutes for your domain to start working." = "Votre nouveau domaine %@ est en cours de configuration. Un délai de 30 minutes peut être nécessaire pour que votre domaine soit opérationnel."; @@ -9573,8 +9298,20 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* This is the string we display when prompting the user to review the WordPress app */ "appRatings.wordpress.prompt" = "Que pensez-vous de WordPress ?"; -/* Label displayed on audio media items. */ -"audio" = "Audio"; +/* Indicates an image will use high quality when uploaded. */ +"appSettings.media.imageQuality.high" = "Élevée"; + +/* Indicates an image will use low quality when uploaded. */ +"appSettings.media.imageQuality.low" = "Faible"; + +/* Indicates an image will use maximum quality when uploaded. */ +"appSettings.media.imageQuality.maximum" = "Maximale"; + +/* Indicates an image will use medium quality when uploaded. */ +"appSettings.media.imageQuality.medium" = "Moyenne"; + +/* Title of button for leaving on image optimization, displayed in the alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.turnOn" = "Oui, laisser activé"; /* translators: displays audio file extension. e.g. MP3 audio file */ "audio file" = "fichier audio"; @@ -9688,7 +9425,41 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "blogHeader.actionCopyURL" = "Copier l’URL"; /* Context menu button title */ -"blogHeader.actionOpenInBrowser" = "Ouvrir dans le navigateur"; +"blogHeader.actionVisitSite" = "Aller sur le site"; + +/* Title for a button that, when tapped, shows more info about participating in Bloganuary. */ +"bloganuary.dashboard.card.button.learnMore" = "Lire la suite"; + +/* Title for the Bloganuary dashboard card while Bloganuary is running. */ +"bloganuary.dashboard.card.runningTitle" = "Bloganuary, c’est parti !"; + +/* Title for the Bloganuary dashboard card. */ +"bloganuary.dashboard.card.title" = "C’est bientôt Bloganuary !"; + +/* Title of a button that calls the user to enable the Blogging Prompts feature. */ +"bloganuary.learnMore.modal.button.promptsDisabled" = "Activer les invites à bloguer"; + +/* Title of a button that will dismiss the Bloganuary modal when tapped. +Note that the word 'go' here should have a closer meaning to 'start' rather than 'move forward'. */ +"bloganuary.learnMore.modal.button.promptsEnabled" = "C’est parti !"; + +/* The second line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.description.second" = "Publiez votre réponse."; + +/* The third line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.description.third" = "Lisez les réponses d’autres blogueurs pour trouver de l’inspiration et faire de nouvelles rencontres."; + +/* The first line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.descriptions.first" = "Recevez chaque jour une invite pour vous inspirer."; + +/* An additional piece of information shown in case the user has the Blogging Prompts feature disabled. */ +"bloganuary.learnMore.modal.footer.addition" = "Pour participer au Bloganuary, vous devez activer les invites à bloguer."; + +/* An informative excerpt shown in a subtler tone. */ +"bloganuary.learnMore.modal.footer.text" = "Bloganuary utilisera des invites quotidiennes à bloguer pour vous envoyer des thèmes de publications tout au long du mois de janvier."; + +/* The headline text of the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.headline" = "Participer à notre défi d’écriture d’un mois"; /* Verb. Dismisses the blogging prompt notification. */ "bloggingPrompt.pushNotification.customActionDescription.dismiss" = "Ignorer"; @@ -9717,6 +9488,12 @@ Example: Comment on Example: Reply to Pamela Nguyen */ "comment.header.subText.reply" = "Répondre à %1$@"; +/* Error message informing a user about an invalid password. */ +"common.reEnterPasswordMessage" = "L‘identifiant ou le mot de passe enregistré dans l‘application est peut-être obsolète. Veuillez saisir à nouveau votre mot de passe dans les réglages et ressayer."; + +/* An error message. */ +"common.unableToConnect" = "Connexion impossible"; + /* Footnote for the privacy compliance popover. */ "compliance.analytics.popover.footnote" = "Ces cookies nous permettent d’optimiser les performances en collectant des informations sur la façon dont les utilisateurs interagissent avec nos sites Web."; @@ -9864,50 +9641,89 @@ Example: Reply to Pamela Nguyen */ /* Title for a menu action in the context menu on the Jetpack install card. */ "domain.dashboard.card.menu.hide" = "Masquer ceci"; -/* Domain Purchase Completion footer */ -"domain.purchase.preview.footer" = "Un délai de 30 minutes peut être nécessaire pour que votre domaine personnalisé soit opérationnel."; +/* Search domain - Title for the Suggested domains screen */ +"domain.management.addDomain.search.title" = "Rechercher un domaine"; -/* Domain Purchase Completion description (only for FREE domains). */ -"domain.purchase.preview.free.description" = "Ensuite, nous vous aiderons à le préparer à être consulté."; +/* Domain management buy domain card button title */ +"domain.management.buy.card.button.title" = "Obtenir un domaine"; -/* Domain Purchase Completion description (only for PAID domains). */ -"domain.purchase.preview.paid.description" = "Nous vous avons envoyé le reçu par e-mail. Ensuite, nous vous aiderons à le préparer pour tout le monde."; +/* Domain management buy domain card subtitle */ +"domain.management.buy.card.subtitle" = "Ajoutez un site plus tard."; -/* Reflects that site is live when domain purchase feature flag is ON. */ -"domain.purchase.preview.title" = "Félicitations, votre site est en ligne !"; +/* Domain management buy domain card title */ +"domain.management.buy.card.title" = "Domaine sans site"; + +/* The expired label of the domain card in All Domains screen. */ +"domain.management.card.expired.label" = "Expiré"; -/* Status of a domain in `Action Required` state */ -"domain.status.action.required" = "Action requise"; +/* The renews label of the domain card in All Domains screen. */ +"domain.management.card.renews.label" = "Se renouvelle"; -/* Status of a domain in `Active` state */ -"domain.status.active" = "Actif"; +/* The empty state button title in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.button.title" = "Trouver un domaine"; -/* Status of a domain in `Complete Setup` state */ -"domain.status.complete.setup" = "Terminer la configuration"; +/* The empty state description in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.description" = "Appuyez ci-dessous pour trouver le domaine idéal."; -/* Status of a domain in `Error` state */ -"domain.status.error" = "Erreur"; +/* The empty state title in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.title" = "Vous n’avez pas de domaine"; -/* Status of a domain in `Expired` state */ -"domain.status.expired" = "Expiré"; +/* The empty state description in All Domains screen when an error occurs */ +"domain.management.error.empty.state.description" = "Une erreur est survenue lors du chargement de vos domaines. Veuillez contacter l’assistance si le problème persiste."; -/* Status of a domain in `Expiring Soon` state */ -"domain.status.expiring.soon" = "Expire bientôt"; +/* The empty state title in All Domains screen when an error occurs */ +"domain.management.error.empty.state.title" = "Un problème est survenu"; -/* Status of a domain in `Failed` state */ -"domain.status.failed" = "Échec"; +/* The empty state button title in All Domains screen when an error occurs */ +"domain.management.error.state.button.title" = "Réessayer"; -/* Status of a domain in `In Progress` state */ -"domain.status.in.progress" = "En cours"; +/* The empty state description in All Domains screen when the user is offline */ +"domain.management.offline.empty.state.description" = "Veuillez vérifier votre connexion réseau et réessayer."; -/* Status of a domain in `Renew` state */ -"domain.status.renew" = "Renouveler"; +/* The empty state title in All Domains screen when the user is offline */ +"domain.management.offline.empty.state.title" = "Aucune connexion internet"; -/* Status of a domain in `Verify Email` state */ -"domain.status.verify.email" = "Vérifier l’adresse e-mail"; +/* Domain management choose site card button title */ +"domain.management.purchase.footer" = "*Tous les plans payants annuels sont assortis d’un domaine gratuit pendant un an"; -/* Status of a domain in `Verifying` state */ -"domain.status.verifying" = "Vérification"; +/* Domain management purchase domain screen title */ +"domain.management.purchase.subtitle" = "Ne vous inquiétez pas. Vous pourrez facilement ajouter un site plus tard."; + +/* Domain management purchase domain screen title. */ +"domain.management.purchase.title" = "Choisir comment utiliser votre domaine"; + +/* The search bar title in All Domains screen. */ +"domain.management.search-bar.title" = "Rechercher des domaines"; + +/* The empty state description in All Domains screen when the are no domains matching the search criteria */ +"domain.management.search.empty.state.description" = "Impossible de trouver un domaine correspondant à votre recherche pour « %@ »"; + +/* The empty state title in All Domains screen when the are no domains matching the search criteria */ +"domain.management.search.empty.state.title" = "Aucun domaine ne correspond"; + +/* Domain management choose site card button title */ +"domain.management.site.card.button.title" = "Choisir un site"; + +/* Domain management choose site card subtitle */ +"domain.management.site.card.footer" = "Domaine gratuit la première année*"; + +/* Domain management choose site card subtitle */ +"domain.management.site.card.subtitle" = "Utilisez un site que vous avez déjà commencé."; + +/* Domain management choose site card title */ +"domain.management.site.card.title" = "Site WordPress.com existant"; + +/* Domain Purchase Completion footer */ +"domain.purchase.preview.footer" = "Un délai de 30 minutes peut être nécessaire pour que votre domaine personnalisé soit opérationnel."; + +/* Domain Purchase Completion description (only for FREE domains). */ +"domain.purchase.preview.free.description" = "Ensuite, nous vous aiderons à le préparer à être consulté."; + +/* Domain Purchase Completion description (only for PAID domains). */ +"domain.purchase.preview.paid.description" = "Nous vous avons envoyé le reçu par e-mail. Ensuite, nous vous aiderons à le préparer pour tout le monde."; + +/* Reflects that site is live when domain purchase feature flag is ON. */ +"domain.purchase.preview.title" = "Félicitations, votre site est en ligne !"; /* The 'Best Alternative' label under the domain name in 'Choose a domain' screen */ "domain.suggestions.row.best-alternative" = "Meilleure alternative"; @@ -9930,12 +9746,24 @@ Example: Reply to Pamela Nguyen */ /* The text to display for paid domains in 'Site Creation > Choose a domain' screen */ "domain.suggestions.row.yearly" = "par an"; +/* Title for the checkout screen. */ +"domains.checkout.title" = "Validation de la commande"; + /* Action shown in a bottom notice to dismiss it. */ "domains.failure.dismiss" = "Ignorer"; /* Content show when the domain selection action fails. */ "domains.failure.title" = "Désolés, le domaine que vous tentez d’ajouter ne peut pas être acheté sur l’application Jetpack pour le moment."; +/* Title for the screen where the user can choose how to use the domain they're end up purchasing. */ +"domains.purchase.choice.title" = "Acheter un domaine"; + +/* Back button title that navigates back to the search domains screen. */ +"domains.search.backButton.title" = "Rechercher"; + +/* Title of screen where user chooses a site to connect to their selected domain */ +"domains.sitePicker.title" = "Choisir un site"; + /* No comment provided by engineer. */ "double-tap to change unit" = "appuyer deux fois pour modifier l’unité"; @@ -9953,6 +9781,15 @@ Example: Reply to Pamela Nguyen */ Site Address placeholder */ "example.com" = "exemple.com"; +/* Title for confirmation navigation bar button item */ +"externalMediaPicker.add" = "Ajouter"; + +/* Bottom toolbar title in the selection mode */ +"externalMediaPicker.toolbarSelectItemsPrompt" = "Sélectionner des images"; + +/* Bottom toolbar title in the selection mode */ +"externalMediaPicker.toolbarViewSelected" = "Voir la sélection (%@)"; + /* Title of screen the displays the details of an advertisement campaign. */ "feature.blaze.campaignDetails.title" = "Détails de la campagne"; @@ -10052,9 +9889,6 @@ Example: Reply to Pamela Nguyen */ /* (placeholder) Help the user enter a URL into the field */ "http:\/\/my-site-address (URL)" = "http:\/\/adresse-de-mon-site (URL)"; -/* Label displayed on image media items. */ -"image" = "Image"; - /* Sentence to justify why the app is asking permission from the user to use their camera. */ "infoplist.NSCameraUsageDescription" = "Pour prendre des photos ou réaliser des vidéos à utiliser dans vos articles."; @@ -10355,6 +10189,9 @@ Example: Reply to Pamela Nguyen */ /* Indicating that referrer was marked as spam */ "marked as spam" = "marqué comme indésirable"; +/* Products header text in Me Screen. */ +"me.products.header" = "Produits"; + /* Title of error prompt shown when a sync fails. */ "media.syncFailed" = "Impossible de synchroniser les médias"; @@ -10367,18 +10204,24 @@ Example: Reply to Pamela Nguyen */ /* Message of an alert informing users that the video they are trying to select is not allowed. */ "mediaExporter.videoLimitExceededError" = "La mise en ligne de vidéos de plus de 5 minutes requiert un plan payant."; -/* Verb. User action to dismiss error alert when failing to load media item. */ -"mediaItemTable.errorAlert.dismissButton" = "Ignorer"; - /* Accessibility hint for add button to add items to the user's media library */ "mediaLibrary.addButtonAccessibilityHint" = "Ajouter un nouveau média"; /* Accessibility label for add button to add items to the user's media library */ "mediaLibrary.addButtonAccessibilityLabel" = "Ajouter"; +/* Button name in the more menu */ +"mediaLibrary.aspectRatioGrid" = "Grille de proportions"; + +/* Context menu button */ +"mediaLibrary.buttonDelete" = "Supprimer"; + /* Media screen navigation bar button Select title */ "mediaLibrary.buttonSelect" = "Sélectionner"; +/* Context menu button */ +"mediaLibrary.buttonShare" = "Partager"; + /* Verb. Button title. Tapping cancels an action. */ "mediaLibrary.deleteConfirmationCancel" = "Annuler"; @@ -10400,6 +10243,21 @@ Example: Reply to Pamela Nguyen */ /* Text displayed in HUD after successfully deleting a media item */ "mediaLibrary.deletionSuccessMessage" = "Supprimé !"; +/* The name of the media filter */ +"mediaLibrary.filterAll" = "Tout"; + +/* The name of the media filter */ +"mediaLibrary.filterAudio" = "Audio"; + +/* The name of the media filter */ +"mediaLibrary.filterDocuments" = "Documents"; + +/* The name of the media filter */ +"mediaLibrary.filterImages" = "Images"; + +/* The name of the media filter */ +"mediaLibrary.filterVideos" = "Vidéos"; + /* Verb. Button title. Tapping dismisses a prompt. */ "mediaLibrary.retryOptionsAlert.dismissButton" = "Ignorer"; @@ -10409,6 +10267,12 @@ Example: Reply to Pamela Nguyen */ /* Message displayed when no results are returned from a media library search. Should match Calypso. */ "mediaLibrary.searchResultsEmptyTitle" = "Aucun média ne correspond à votre recherche"; +/* Text displayed in HUD if there was an error attempting to share a group of media items. */ +"mediaLibrary.sharingFailureMessage" = "Impossible de partager les éléments sélectionnés."; + +/* Button name in the more menu */ +"mediaLibrary.squareGrid" = "Grille carrée"; + /* Media screen navigation title */ "mediaLibrary.title" = "Médias"; @@ -10430,6 +10294,9 @@ Example: Reply to Pamela Nguyen */ /* The title of the button to dismiss the alert shown when the picked media cannot be imported into stories. */ "mediaPicker.failedMediaExportAlert.dismissButton" = "Ignorer"; +/* Error message when picked media cannot be imported into stories. */ +"mediaPicker.failedMediaExportAlert.message" = "Votre média n’a pas pu être exporté. Si le problème persiste, vous pouvez nous contacter via l’écran Moi > Aide & Support."; + /* Message for alert when access to camera is not granted */ "mediaPicker.noCameraAccessMessage" = "Cette application nécessite une autorisation pour accéder à l’appareil photo et capturer de nouveaux médias. Pour ce faire, veuillez modifier les réglages de confidentialité."; @@ -10463,6 +10330,12 @@ Example: Reply to Pamela Nguyen */ /* The name of the action in the context menu */ "mediaPicker.takeVideo" = "Prendre une vidéo"; +/* Navigation title for media preview. Example: 1 of 3 */ +"mediaPreview.NofM" = "%1$@ sur %2$@"; + +/* Max image size in pixels (e.g. 300x300px) */ +"mediaSizeSlider.valueFormat" = "%1$d x %2$d px"; + /* The description in the Delete WordPress screen */ "migration.deleteWordpress.description" = "Il semblerait que l'application WordPress soit toujours installée."; @@ -10475,9 +10348,6 @@ Example: Reply to Pamela Nguyen */ /* The title in the Delete WordPress screen */ "migration.deleteWordpress.title" = "Vous n’avez plus besoin de l’application WordPress sur votre appareil"; -/* Primary button title in the migration done screen. */ -"migration.done.actions.primary.title" = "Terminer"; - /* Footer for the migration done screen. */ "migration.done.footer" = "Nous vous recommandons de désinstaller l’application WordPress sur votre appareil pour éviter les conflits de données."; @@ -10487,6 +10357,9 @@ Example: Reply to Pamela Nguyen */ /* Primary description in the migration done screen. */ "migration.done.primaryDescription" = "Nous avons transféré tous vos réglages et données. Tout se trouve au même endroit qu’avant."; +/* Secondary description (second paragraph) in the migration done screen. */ +"migration.done.secondaryDescription" = "Il est temps de poursuivre votre aventure WordPress sur l’application Jetpack !"; + /* Title of the migration done screen. */ "migration.done.title" = "Merci d’être passé à Jetpack !"; @@ -10610,6 +10483,30 @@ Example: Reply to Pamela Nguyen */ /* Message title for when a user has no sites. */ "mySite.noSites.title" = "Vous n’avez pas de sites."; +/* Menu title for the add site option */ +"mySite.siteActions.addSite" = "Ajouter un site"; + +/* Button that reveals more site actions */ +"mySite.siteActions.button" = "Afficher les actions du site"; + +/* Accessibility hint for button used to show more site actions */ +"mySite.siteActions.hint" = "Appuyer pour afficher plus d’actions sur le site"; + +/* Menu title for the personalize home option */ +"mySite.siteActions.personalizeHome" = "Personnaliser l’accueil"; + +/* Menu title for the change site icon option */ +"mySite.siteActions.siteIcon" = "Modifier l’icône du site"; + +/* Menu title for the change site title option */ +"mySite.siteActions.siteTitle" = "Modifier le titre de site"; + +/* Menu title for the switch site option */ +"mySite.siteActions.switchSite" = "Changer de site"; + +/* Menu title for the visit site option */ +"mySite.siteActions.visitSite" = "Aller sur le site"; + /* Dismiss button title. */ "noResultsViewController.dismissButton" = "Ignorer"; @@ -10625,14 +10522,20 @@ Example: Reply to Pamela Nguyen */ /* This is one of the buttons we display when prompting the user for a review */ "notifications.appRatings.sendFeedback.yes.buttonTitle" = "Envoyer des commentaires"; -/* Word separating the current index from the total amount. I.e.: 7 of 9 */ -"of" = "sur"; +/* Badge for page cells */ +"pageList.badgeHomepage" = "Page d’accueil"; + +/* Badge for page cells */ +"pageList.badgeLocalChanges" = "Changements locaux"; -/* Label displayed on media items that are not video, image, or audio. */ -"other" = "Autre"; +/* Badge for page cells */ +"pageList.badgePendingReview" = "En attente de relecture"; -/* Promote the page with Blaze. */ -"pages.blaze.actionTitle" = "Promouvoir avec Blaze"; +/* Badge for page cells */ +"pageList.badgePosts" = "Page des articles"; + +/* Badge for page cells */ +"pageList.badgePrivate" = "Privé"; /* Subtitle of the theme template homepage cell */ "pages.template.subtitle" = "Votre page d’accueil utilise un modèle Thème et s’ouvrira dans l’éditeur web."; @@ -10640,6 +10543,36 @@ Example: Reply to Pamela Nguyen */ /* Title of the theme template homepage cell */ "pages.template.title" = "Page d’accueil"; +/* Message informing the user that their static homepage page was set successfully */ +"pages.updatePage.successTitle" = "Page mise à jour avec succès"; + +/* Delete option in the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.actionTitle" = "Supprimer définitivement"; + +/* Message of the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.alertMessage" = "Voulez-vous vraiment supprimer définitivement cette page ?"; + +/* Title of the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.alertTitle" = "Supprimer définitivement ?"; + +/* Menu option for filtering posts by everyone */ +"pagesList.pagesByEveryone" = "Pages de tout le monde"; + +/* Menu option for filtering posts by me */ +"pagesList.pagesByMe" = "Mes pages"; + +/* Trash option in the trash page confirmation alert. */ +"pagesList.trash.actionTitle" = "Envoyer dans la corbeille"; + +/* Message of the trash page confirmation alert. */ +"pagesList.trash.alertMessage" = "Voulez-vous vraiment placer cette page dans la corbeille ?"; + +/* Title of the trash page confirmation alert. */ +"pagesList.trash.alertTitle" = "Déplacer cette page vers la corbeille ?"; + +/* Cancels an Action */ +"pagesList.trash.cancel" = "Annuler"; + /* No comment provided by engineer. */ "password" = "mot de passe"; @@ -10679,6 +10612,45 @@ Example: Reply to Pamela Nguyen */ /* Register Domain - Domain contact information field Phone */ "phone number" = "numéro de téléphone"; +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.createdTimeAgo" = "Créé le %@"; + +/* Status mesasge for post cells */ +"post.deletingPostPermanentlyStatusMessage" = "Suppression de l’article…"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.editedTimeAgo" = "Modifié le %@"; + +/* Status mesasge for post cells */ +"post.movingToTrashStatusMessage" = "Déplacement de l‘article dans la corbeille…"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.publishedTimeAgo" = "Publié le %@"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.scheduledForDate" = "Planifié le %@"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.trashedTimeAgo" = "Déplacé vers la corbeille le %@"; + +/* Accessibility label for the post author in the post list. The parameter is the author name. For example, \"By Elsa.\" */ +"postList.a11y.authorChunkFormat" = "Par %@."; + +/* Accessibility label for a post's excerpt in the post list. The parameter is the post excerpt. For example, \"Excerpt. This is the first paragraph.\" */ +"postList.a11y.exerptChunkFormat" = "Extrait. %@."; + +/* Accessibility label for a sticky post in the post list. */ +"postList.a11y.sticky" = "Épinglé."; + +/* Title for the 'Trash' post list row swipe action */ +"postList.swipeActionDelete" = "Corbeille"; + +/* Title for the 'Share' post list row swipe action */ +"postList.swipeActionShare" = "Partager"; + +/* Title for the 'View' post list row swipe action */ +"postList.swipeActionView" = "Voir"; + /* User action to dismiss featured media options. */ "postSettings.featuredImageUploadActionSheet.dismiss" = "Ignorer"; @@ -10697,9 +10669,84 @@ Example: Reply to Pamela Nguyen */ /* Button in Post Settings */ "postSettings.setFeaturedImageButton" = "Définir l’image mise en avant"; +/* Error message on post/page settings screen */ +"postSettings.updateFailedMessage" = "Impossible de mettre à jour les réglages de l‘article"; + /* Promote the post with Blaze. */ "posts.blaze.actionTitle" = "Promouvoir avec Blaze"; +/* Label for the Post List option that cancels automatic uploading of a post. */ +"posts.cancelUpload.actionTitle" = "Annuler la mise en ligne"; + +/* Label for post comments option. Tapping displays comments for a post. */ +"posts.comments.actionTitle" = "Commentaires"; + +/* Label for the delete post option. Tapping permanently deletes a post. */ +"posts.delete.actionTitle" = "Supprimer définitivement"; + +/* Label for an option that moves a post to the draft folder */ +"posts.draft.actionTitle" = "Passer en brouillon"; + +/* Label for post duplicate option. Tapping creates a copy of the post. */ +"posts.duplicate.actionTitle" = "Dupliquer"; + +/* Opens a submenu for page attributes. */ +"posts.pageAttributes.actionTitle" = "Attributs de la page"; + +/* Label for the preview post button. Tapping displays the post as it appears on the web. */ +"posts.preview.actionTitle" = "Aperçu"; + +/* Label for an option that moves a publishes a post immediately */ +"posts.publish.actionTitle" = "Publier maintenant"; + +/* Retry uploading the post. */ +"posts.retry.actionTitle" = "Réessayer"; + +/* Set the selected page as the homepage. */ +"posts.setHomepage.actionTitle" = "Définir comme page d’accueil"; + +/* Set the parent page for the selected page. */ +"posts.setParent.actionTitle" = "Configurer le parent"; + +/* Set the selected page as a posts page. */ +"posts.setPostsPage.actionTitle" = "Définir comme page articles"; + +/* Set the selected page as a regular page. */ +"posts.setRegularPage.actionTitle" = "Définir comme page standard"; + +/* Label for post settings option. Tapping displays settings for a post. */ +"posts.settings.actionTitle" = "Réglages"; + +/* Share the post. */ +"posts.share.actionTitle" = "Partager"; + +/* Label for post stats option. Tapping displays statistics for a post. */ +"posts.stats.actionTitle" = "Statistiques"; + +/* Label for a option that moves a post to the trash folder */ +"posts.trash.actionTitle" = "Envoyer dans la corbeille"; + +/* Label for the view post button. Tapping displays the post as it appears on the web. */ +"posts.view.actionTitle" = "Voir"; + +/* A short message explaining that a page was deleted permanently. */ +"postsList.deletePage.message" = "Page supprimée définitivement"; + +/* A short message explaining that a post was deleted permanently. */ +"postsList.deletePost.message" = "Article supprimé définitivement"; + +/* A short message explaining that a page was moved to the trash bin. */ +"postsList.movePageToTrash.message" = "Page déplacée dans la Corbeille"; + +/* A short message explaining that a post was moved to the trash bin. */ +"postsList.movePostToTrash.message" = "Article déplacé dans la Corbeille"; + +/* Menu option for filtering posts by everyone */ +"postsList.postsByEveryone" = "Articles de tout le monde"; + +/* Menu option for filtering posts by me */ +"postsList.postsByMe" = "Mes articles"; + /* Title for the button to subscribe to Jetpack Social on the remaining shares view */ "postsettings.social.remainingshares.subscribe" = "S'abonner maintenant pour partager plus"; @@ -10844,13 +10891,15 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Text for the 'Like' button on the reader post card cell. */ "reader.post.button.like" = "J’aime"; -/* Accessibility hint for the like button on the reader post card cell - Accessibility hint for the liked button on the reader post card cell */ +/* Accessibility hint for the like button on the reader post card cell */ "reader.post.button.like.accessibility.hint" = "Ajoute la mention J’aime à l’article."; /* Text for the 'Liked' button on the reader post card cell. */ "reader.post.button.liked" = "J’aime"; +/* Accessibility hint for the liked button on the reader post card cell */ +"reader.post.button.liked.accessibility.hint" = "Retire la mention J’aime sur l’article."; + /* Accessibility hint for the site header on the reader post card cell */ "reader.post.button.menu.accessibility.hint" = "Ouvre un menu avec plus d’actions."; @@ -10914,6 +10963,12 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Title for the tooltip anchor. */ "readerDetail.tooltipAnchorTitle.accessibilityLabel" = "Nouveau"; +/* The button title for the transfer footer view in Register Domain screen */ +"register.domain.transfer.button.title" = "Transférer un domaine"; + +/* The title for the transfer footer view in Register Domain screen */ +"register.domain.transfer.title" = "Vous désirez transférer un domaine que vous possédez déjà ?"; + /* Information of what related post are and how they are presented */ "relatedPostsSettings.optionsFooter" = "Les articles similaires affichent du contenu pertinent de votre site sous les articles."; @@ -11004,6 +11059,12 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Accessibility hint for actions when displaying media items. */ "siteMedia.cellAccessibilityHint" = "Sélectionnez un média."; +/* Accessibility hint for media item preview for user's viewing an item in their media library */ +"siteMediaItem.contentViewAccessibilityHint" = "Appuyer pour voir le média en plein écran"; + +/* Title for confirmation navigation bar button item */ +"siteMediaPicker.add" = "Ajouter"; + /* Media screen navigation title */ "siteMediaPicker.title" = "Médias"; @@ -11011,7 +11072,7 @@ Example: given a notice format "Following %@" and empty site name, this will be "siteSettings.privacy.title" = "Confidentialité"; /* Hint for users when hidden privacy setting is set */ -"siteVisibility.hidden.hint" = "Votre site est visible de tous, mais demande aux moteurs de recherche de ne pas l’indexer."; +"siteVisibility.hidden.hint" = "Votre site est masqué derrière une mention « Bientôt disponible » jusqu’à ce qu’il soit prêt."; /* Text for privacy settings: Hidden */ "siteVisibility.hidden.title" = "Masqué"; @@ -11172,6 +11233,12 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Dismiss the AlertView */ "stockPhotos.strings.dismiss" = "Ignorer"; +/* Subtitle for placeholder in Free Photos. The company name 'Pexels' should always be written as it is. */ +"stockPhotos.subtitle" = "Photos mises à disposition par Pexels"; + +/* Title for placeholder in Free Photos */ +"stockPhotos.title" = "Recherchez des photos gratuites pour les ajouter à votre bibliothèque de médias !"; + /* Section title for prominent suggestions */ "suggestions.section.prominent" = "Dans cette conversation"; @@ -11319,6 +11386,9 @@ Example: given a notice format "Following %@" and empty site name, this will be /* View title for Help & Support page. */ "support.title" = "Aide"; +/* Title for placeholder in Tenor picker */ +"tenor.welcomeMessage" = "Recherchez des GIF pour les ajouter à votre bibliothèque de médias !"; + /* Header of delete screen section listing things that will be deleted. */ "these items will be deleted:" = "ces éléments vont être supprimés :"; @@ -11334,9 +11404,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Displayed in the confirmation alert when marking unread notifications as read. */ "unread" = "non lues"; -/* Label displayed on video media items. */ -"video" = "Vidéo"; - /* Portion of a message for Jetpack users that have multisite WP installation, thus Restore is not available. This part is a link, colored with a different color. */ "visit our documentation page" = "visiter notre page de documentation"; diff --git a/WordPress/Resources/he.lproj/Localizable.strings b/WordPress/Resources/he.lproj/Localizable.strings index c9649653bff6..36bbae12a556 100644 --- a/WordPress/Resources/he.lproj/Localizable.strings +++ b/WordPress/Resources/he.lproj/Localizable.strings @@ -1,4 +1,4 @@ -/* Translation-Revision-Date: 2023-10-19 16:54:08+0000 */ +/* Translation-Revision-Date: 2024-01-04 14:54:09+0000 */ /* Plural-Forms: nplurals=2; plural=n != 1; */ /* Generator: GlotPress/4.0.0-alpha.11 */ /* Language: he_IL */ @@ -152,9 +152,6 @@ /* Difference label for Period Overview stat, indicating change from previous period. Ex: +99.9K (5%) */ "%@%@ (%@%%)" = "%1$@%2$@ (%3$@%%)"; -/* Accessibility label for a post in the post list. The parameters are the title, and date respectively. For example, \"Let it Go, 1 hour ago.\" */ -"%@, %@." = "%1$@, %2$@."; - /* Accessibility value for a Stats' Posting Activity Month if the user selected a day with posts. The first parameter is day (e.g. November 2019). The second parameter is the number of posts. */ "%@. %d posts." = "%1$@. %2$d פוסטים."; @@ -210,9 +207,6 @@ /* Age between dates over one year. */ "%d years" = "%d שנים"; -/* Max image size in pixels (e.g. 300x300px) */ -"%dx%dpx" = "%1$dx%2$dpx"; - /* One menu area available in the theme */ "%i menu area in this theme" = "אזור תפריט %i בערכת עיצוב זו"; @@ -271,6 +265,9 @@ translators: Block name. %s: The localized block name */ /* translators: %s: social link name e.g: \"Instagram\". */ "%s social icon" = "סמל של הרשת החברתית %s"; +/* translators: displayed right after the classic block is converted to blocks. %s: The localized classic block name */ +"'%s' block converted to blocks" = "הבלוק '%s' בהמרה לבלוקים"; + /* translators: Missing block alert title. %s: The localized block name */ "'%s' is not fully-supported" = "'%s' לא נתמך באופן מלא"; @@ -478,13 +475,6 @@ translators: Block name. %s: The localized block name */ /* Label for the Activity Type filter button when there are more than 1 activity type selected */ "Activity Type (%1$d)" = "סוג הפעילות (⁦%1$d⁩)"; -/* Accessibility label for add button to add items to the user's media library - Remove asset from media picker list */ -"Add" = "הוספה"; - -/* Action for Media Picker to indicate selection of media. The argument in the string represents the number of elements (as numeric digits) selected */ -"Add %@" = "הוספת %@"; - /* No comment provided by engineer. */ "Add Block After" = "הוספת בלוק לאחר"; @@ -581,9 +571,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Add menu item to children" = "הוספת פריט תפריט לילדים"; -/* Accessibility hint for add button to add items to the user's media library */ -"Add new media" = "להוסיף פריט מדיה חדש"; - /* Screen reader text for Menus button that adds a new menu to a site. */ "Add new menu" = "הוספת תפריט חדש"; @@ -649,9 +636,6 @@ translators: Block name. %s: The localized block name */ /* Option to select the Airmail app when logging in with magic links */ "Airmail" = "Airmail"; -/* Description of albums in the photo libraries */ -"Albums" = "אלבומים"; - /* Image alignment option title. Title of the screen for choosing an image's alignment. */ "Alignment" = "יישור"; @@ -665,6 +649,9 @@ translators: Block name. %s: The localized block name */ Title of the drafts filter. This filter shows a list of draft posts. */ "All" = "הכל"; +/* Information about redeeming domain credit on site dashboard. */ +"All WordPress.com annual plans include a custom domain name. Register your free domain now." = "כל התוכניות השנתיות של WordPress.com כוללות דומיין אישי. ניתן לרשום את הדומיין החינמי שלך עכשיו."; + /* Footer of the free domain registration section for a paid plan. Information about redeeming domain credit on site dashboard. */ "All WordPress.com plans include a custom domain name. Register your free premium domain now." = "כל התוכניות של WordPress.com כוללות דומיין אישי. ניתן להירשם ולקבל דומיין פרימים בחינם עכשיו."; @@ -730,10 +717,10 @@ translators: Block name. %s: The localized block name */ "Alt Text" = "טקסט חלופי"; /* No comment provided by engineer. */ -"Alternatively, you can detach and edit these blocks separately by tapping “Detach patterns”." = "לחלופין, ניתן לנתק ולערוך את הבלוקים האלה בנפרד על ידי הקשה על 'להפריד מקבצי בלוקים'."; +"Alternatively, you can convert the content to blocks." = "לחלופין, אפשר להמיר את התוכן לבלוקים."; -/* No comment provided by engineer. */ -"Alternatively, you can detach and edit this block separately by tapping “Detach pattern”." = "לחלופין, ניתן לנתק ולערוך את הבלוק הזה בנפרד על ידי הקשה על 'להפריד מקבץ'."; +/* translators: Alternative option included in a warning related to having blocks deeply nested. */ +"Alternatively, you can flatten the content by ungrouping the block." = "Alternatively, you can flatten the content by ungrouping the block."; /* Instruction text to explain to help users type their password instead of using magic link login option. */ "Alternatively, you may enter the password for this account." = "לחלופין, ניתן להזין את הסיסמה של החשבון."; @@ -882,15 +869,9 @@ translators: Block name. %s: The localized block name */ /* Message prompting the user to confirm that they want to disconnect Jetpack from the site. */ "Are you sure you want to disconnect Jetpack from the site?" = "האם ברצונך לנתק את Jetpack מהאתר?"; -/* Message prompting the user to confirm that they want to permanently delete a group of media items. */ -"Are you sure you want to permanently delete these items?" = "האם אתה בטוח שברצונך למחוק לצמיתות את הפריטים שנבחרו?"; - /* Message prompting the user to confirm that they want to permanently delete a media item. Should match Calypso. */ "Are you sure you want to permanently delete this item?" = "האם אכן ברצונך למחוק פריט זה לצמיתות?"; -/* Message of the confirmation alert when deleting a page from the trash. */ -"Are you sure you want to permanently delete this page?" = "האם ברצונך למחוק את העמוד לצמיתות?"; - /* Message of the confirmation alert when deleting a post from the trash. */ "Are you sure you want to permanently delete this post?" = "ביקשת למחוק פוסט זה לצמיתות - האם ההחלטה סופית?"; @@ -922,9 +903,6 @@ translators: Block name. %s: The localized block name */ /* Title of message shown when user taps submit for review. */ "Are you sure you want to submit for review?" = "האם ברצונך לשלוח לביקורת?"; -/* Message of the trash page confirmation alert. */ -"Are you sure you want to trash this page?" = "בחרת לשלוח עמוד זה לפח – האם ההחלטה סופית?"; - /* Message of the trash confirmation alert. */ "Are you sure you want to trash this post?" = "בחרת לשלוח פוסט זה לפח - האם ההחלטה סופית?"; @@ -965,9 +943,6 @@ translators: Block name. %s: The localized block name */ /* translators: accessibility text. Empty Audio caption. */ "Audio caption. Empty" = "תיאור אודיו. ריק"; -/* Accessibility label for audio items in the media collection view. The parameter is the creation date of the audio. */ -"Audio, %@" = "אודיו, %@"; - /* No comment provided by engineer. */ "Authenticating" = "מאמת"; @@ -1178,10 +1153,7 @@ translators: Block name. %s: The localized block name */ "Blocks menu" = "תפריט בלוקים"; /* translators: Warning related to having blocks deeply nested. %d: The deepest nesting level. */ -"Blocks nested deeper than %d levels may not render properly in the mobile editor. For this reason, we recommend flattening the content by ungrouping the block or editing the block using the web editor." = "בעורך לנייד ייתכן עיבוד לא תקין של בלוקים בקינון עמוק מ-%d רמות. מסיבה זו מומלץ 'לשטח' את התוכן על ידי ביטול הקבצת הבלוק או לערוך את הבלוק בעזרת עורך האינטרנט."; - -/* translators: Warning related to having blocks deeply nested. %d: The deepest nesting level. */ -"Blocks nested deeper than %d levels may not render properly in the mobile editor. For this reason, we recommend flattening the content by ungrouping the block or editing the block using your web browser." = "בעורך לנייד ייתכן עיבוד לא תקין של בלוקים בקינון עמוק מ-%d רמות. מסיבה זו מומלץ 'לשטח' את התוכן על ידי ביטול הקבצת הבלוק או לערוך את הבלוק בעזרת דפדפן האינטרנט."; +"Blocks nested deeper than %d levels may not render properly in the mobile editor." = "בעורך לנייד ייתכן עיבוד לא תקין של בלוקים בקינון עמוק מ-%d רמות."; /* Title of a button that displays the WordPress.com blog */ "Blog" = "אתר"; @@ -1257,10 +1229,7 @@ translators: Block name. %s: The localized block name */ "Button position" = "מיקום הכפתור"; /* Label for the post author in the post detail. */ -"By " = "לפי"; - -/* Accessibility label for the post author in the post list. The parameter is the author name. For example, \"By Elsa.\" */ -"By %@." = "מאת %@."; +"By " = "לפי "; /* Legal disclaimer for logging in. The underscores _..._ denote underline. */ "By continuing, you agree to our _Terms of Service_." = "המשך הפעולה מהווה את הסכמתך לתנאי השימוש שלנו."; @@ -1281,8 +1250,7 @@ translators: Block name. %s: The localized block name */ /* Label for size of media while it's being calculated. */ "Calculating..." = "חישוב..."; -/* Accessibility label for taking an image or video with the camera on formatting toolbar. - Accessibility label for the camera tile in the collection view */ +/* Accessibility label for taking an image or video with the camera on formatting toolbar. */ "Camera" = "מצלמה"; /* Title of an alert informing the user the camera permission for the app is disabled and its needed to proceed */ @@ -1333,10 +1301,7 @@ translators: Block name. %s: The localized block name */ Cancels an Action Cancels an alert. Cancels the mark all as read action. - Dismiss the post action sheet Editing GIF alert cancel action button. - Label for a cancel button - Label for the auto-upload cancelation button in the post list. Tapping will prevent the app from auto-uploading the post. Menus cancel button for deleting a menu. Menus cancel button within text bar while editing items. Menus: Cancel button title for canceling an edited menu item. @@ -1353,10 +1318,6 @@ translators: Block name. %s: The localized block name */ Verb. Title for Jetpack Restore cancel button. */ "Cancel" = "בטל"; -/* Label for the Post List option that cancels automatic uploading of a post. - Media Library option to cancel an in-progress or failed upload. */ -"Cancel Upload" = "ביטול העלאה"; - /* Menus alert button title to cancel discarding changes and not createa a new menu. Menus alert button title to cancel discarding changes and not select a new menu Menus alert button title to cancel discarding changes and not select a new menu location */ @@ -1415,9 +1376,6 @@ translators: Block name. %s: The localized block name */ Main title */ "Change Password" = "שינוי סיסמה"; -/* Title of button that takes user to the system Settings section for the app */ -"Change Settings" = "שינוי ההגדרות"; - /* Change Username title. */ "Change Username" = "החלפת שם משתמש"; @@ -1557,9 +1515,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Choose file" = "לבחור קובץ"; -/* Menu option for selecting media from the device's photo library. */ -"Choose from My Device" = "בחירה מהמכשיר שלי"; - /* Explanatory text for Homepage Settings homepage type selection. */ "Choose from a homepage that displays your latest posts (classic blog) or a fixed \/ static page." = "לבחור בין עמוד בית שמציג את הפוסטים האחרונים שלך (בלוג קלאסי) ובין עמוד קבוע \/ סטטי."; @@ -1760,9 +1715,6 @@ translators: Block name. %s: The localized block name */ /* Community & Non-Profit site intent topic */ "Community & Non-Profit" = "קהילה ועמותות ללא מטרות רווח"; -/* Accessibility indication that the current Post List style is currently Compact. */ -"Compact" = "קומפקטי"; - /* The action is completed */ "Completed" = "הושלמה"; @@ -1948,10 +1900,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Copied block" = "הבלוק הועתק"; -/* Copy the post url and paste anywhere in phone - Label for page copy link. Tapping copy the url of page */ -"Copy Link" = "העתק קישור"; - /* Copy link to oomment button title */ "Copy Link to Comment" = "העתקת הקישור לתגובה"; @@ -2060,9 +2008,6 @@ translators: Block name. %s: The localized block name */ /* Error title displayed when unable to close user account. */ "Couldn’t close account automatically" = "לא ניתן לסגור את החשבון באופן אוטומטי"; -/* Message to show while media data source is finding the number of items available. */ -"Counting media items..." = "סופר פריטי מדיה..."; - /* Period Stats 'Countries' header */ "Countries" = "מדינות"; @@ -2313,7 +2258,6 @@ translators: Block name. %s: The localized block name */ /* Delete Delete button title for the warning shown to the user when he refuses to re-login when the authToken is missing. Title for button that permanently deletes a media item (photo / video) - Title for button that permanently deletes one or more media items (photos / videos) Title of a delete button Title of the trash confirmation alert. */ "Delete" = "מחק"; @@ -2321,15 +2265,11 @@ translators: Block name. %s: The localized block name */ /* Menus confirmation button for deleting a menu. */ "Delete Menu" = "מחיקת תפריט"; -/* Delete option in the confirmation alert when deleting a page from the trash. - Delete option in the confirmation alert when deleting a post from the trash. - Label for a button permanently deletes a page. - Label for the delete post option. Tapping permanently deletes a post. +/* Delete option in the confirmation alert when deleting a post from the trash. Title for button on the comment details page that deletes the comment when tapped. */ "Delete Permanently" = "מחק לצמיתות"; -/* Title of the confirmation alert when deleting a page from the trash. - Title of the confirmation alert when deleting a post from the trash. */ +/* Title of the confirmation alert when deleting a post from the trash. */ "Delete Permanently?" = "למחוק לצמיתות?"; /* Button label for deleting the current site @@ -2448,7 +2388,6 @@ translators: Block name. %s: The localized block name */ /* Accessibility label for button to dismiss a bottom sheet Accessibility label for the transparent space above the login dialog which acts as a button to dismiss the dialog. Accessibility label for the transparent space above the signup dialog which acts as a button to dismiss the dialog. - Action to show on alert when view asset fails. Dismiss a view. Verb */ "Dismiss" = "בטל"; @@ -2466,12 +2405,6 @@ translators: Block name. %s: The localized block name */ User's Display Name */ "Display Name" = "שם תצוגה"; -/* Accessibility label for other media items in the media collection view. The parameter is the creation date of the document. */ -"Document, %@" = "מסמך, ‎%@"; - -/* Accessibility label for other media items in the media collection view. The parameter is the filename file. */ -"Document: %@" = "מסמכים: ‎%@"; - /* Message shown when all tours have been completed */ "Doesn't it feel good to cross things off a list?" = "שמחים שהשלמת את המטרה שהצבת לעצמך!"; @@ -2632,8 +2565,7 @@ translators: Block name. %s: The localized block name */ /* Description of a Quick Start Tour */ "Draft and publish a post." = "לכתוב טיוטה ולפרסם פוסט."; -/* Title of the drafts filter. This filter shows a list of draft posts. - Title of the drafts header in search list. */ +/* Title of the drafts filter. This filter shows a list of draft posts. */ "Drafts" = "טיוטות"; /* No comment provided by engineer. */ @@ -2645,10 +2577,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Drag to adjust focal point" = "יש לגרור כדי להתאים את נקודת המוקד"; -/* Label for page duplicate option. Tapping creates a copy of the page. - Label for post duplicate option. Tapping creates a copy of the post. */ -"Duplicate" = "לשכפל"; - /* No comment provided by engineer. */ "Duplicate block" = "לשכפל בלוק"; @@ -2661,13 +2589,9 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Each block has its own settings. To find them, tap on a block. Its settings will appear on the toolbar at the bottom of the screen." = "לכל בלוק יש הגדרות משלו. כדי למצוא אותן, יש להקיש על הבלוק. ההגדרות שלו יופיעו בסרגל הכלים שנמצא בתחתית המסך."; -/* Accessibility label for edit button to enable multi selection mode in the user's media library - Edit the post. - Editing GIF alert default action button. +/* Editing GIF alert default action button. Edits a Comment Edits the comment - Label for a button that opens the Edit Page view controller - Label for the edit post button. Tapping displays the editor. User action to edit media details. Verb, edit a comment */ "Edit" = "עריכה"; @@ -2675,9 +2599,6 @@ translators: Block name. %s: The localized block name */ /* Title for the edit more button section */ "Edit \"More\" button" = "עריכת הכפתור 'עוד'"; -/* Button that displays the media editor to the user */ -"Edit %@" = "לערוך את %@"; - /* Blocklist Keyword Edition Title */ "Edit Blocklist Word" = "לערוך את המילה ברשימת החסימות"; @@ -2870,9 +2791,6 @@ translators: Block name. %s: The localized block name */ /* Secondary message shown when there are no domains that match the user entered text. */ "Enter different words above and we'll look for an address that matches it." = "יש להזין מילים שונות למעלה ואנחנו נחפש כתובת שתתאים להן."; -/* Accessibility hint for edit button to enable multi selection mode in the user's media library */ -"Enter edit mode to enable multi select to delete" = "יש לעבור למצב עריכה כדי לאפשר בחירה מרובה למחיקה"; - /* (placeholder) Help enter WordPress password Placeholder of a field to type a password to protect the post. */ "Enter password" = "הזן סיסמה"; @@ -3028,9 +2946,6 @@ translators: Block name. %s: The localized block name */ /* Short title telling the user they will receive a blogging reminder every day of the week. */ "Every day at %@" = "בכל יום בשעה %@"; -/* Label for the post author filter. This filter shows posts for all users on the blog. */ -"Everyone" = "כולם"; - /* Example story title description */ "Example story title" = "כותרת סטורי לדוגמה"; @@ -3040,9 +2955,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Excerpt length (words)" = "אורך התקציר (מילים)"; -/* Accessibility label for a post's excerpt in the post list. The parameter is the post excerpt. For example, \"Excerpt. This is the first paragraph.\" */ -"Excerpt. %@." = "תקציר. %@."; - /* Should be the same as the text displayed if the user clicks the (i) in Calypso. */ "Excerpts are optional hand-crafted summaries of your content." = "תקצירים הם סיכומים אופציונליים, \"בעבודת יד\", של התוכן שלך."; @@ -3052,8 +2964,7 @@ translators: Block name. %s: The localized block name */ /* Accessibility Label for the exit full screen button on the full screen comment reply mode */ "Exit Full Screen" = "לצאת מתצוגה של מסך מלא"; -/* Accessibility indication that the current Post List style is currently Expanded. - Screen reader text to represent the expanded state of a UI control */ +/* Screen reader text to represent the expanded state of a UI control */ "Expanded" = "הורחב"; /* Accessibility hint */ @@ -3103,9 +3014,6 @@ translators: Block name. %s: The localized block name */ The action failed */ "Failed" = "נכשל"; -/* Error title when picked media cannot be imported into stories. */ -"Failed Media Export" = "ייצוא המדיה נכשל"; - /* Message for mark all as read success notice */ "Failed marking Notifications as read" = "סימון ההודעות כ'נקרא' נכשל"; @@ -3307,6 +3215,12 @@ translators: Block name. %s: The localized block name */ /* An example tag used in the login prologue screens. */ "Football" = "פוטבול"; +/* translators: Recommendation included in a warning related to having blocks deeply nested. */ +"For this reason, we recommend editing the block using the web editor." = "מסיבה זו מומלץ 'לשטח' את התוכן על ידי ביטול הקבצת הבלוק או לערוך את הבלוק בעזרת עורך האינטרנט."; + +/* translators: Recommendation included in a warning related to having blocks deeply nested. */ +"For this reason, we recommend editing the block using your web browser." = "מסיבה זו מומלץ 'לשטח' את התוכן על ידי ביטול הקבצת הבלוק או לערוך את הבלוק בעזרת דפדפן האינטרנט."; + /* Register Domain - Domain contact information section header description */ "For your convenience, we have pre-filled your WordPress.com contact information. Please review to be sure it’s the correct information you want to use for this domain." = "לנוחותך, מילאנו מראש את הפרטים ליצירת קשר עמך ב-WordPress.com. יש לבדוק את הפרטים ולוודא שהמידע נכון לדומיין שבו ברצונך להשתמש."; @@ -3624,8 +3538,7 @@ translators: Block name. %s: The localized block name */ "Home" = "עמוד הבית"; /* Title for setting which shows the current page assigned as a site's homepage - Title for the homepage section in site settings screen - Title of the Homepage Badge */ + Title for the homepage section in site settings screen */ "Homepage" = "עמוד הבית"; /* Label for Homepage Settings site settings section @@ -3722,9 +3635,6 @@ translators: Block name. %s: The localized block name */ /* Hint for image title on image settings. */ "Image title" = "כותרת התמונה"; -/* Accessibility label for image thumbnails in the media collection view. The parameter is the creation date of the image. */ -"Image, %@" = "תמונה, %@"; - /* Undated post time label */ "Immediately" = "מיד"; @@ -4210,9 +4120,6 @@ translators: Block name. %s: The localized block name */ Settings: Comments Approval settings */ "Links in comments" = "קישורים בתגובות"; -/* The accessibility label for the list style button in the Post List. */ -"List style" = "סגנון רשימה"; - /* Title of the screen that load selected the revisions. */ "Load" = "טעינה"; @@ -4228,18 +4135,12 @@ translators: Block name. %s: The localized block name */ /* Text displayed while loading the activity feed for a site */ "Loading Backups..." = "טוען גיבויים..."; -/* Phrase to show when the user has searched for GIFs and they are being loaded. */ -"Loading GIFs..." = "טוען קובצי GIF..."; - /* Menus label text displayed while menus are loading */ "Loading Menus..." = "טוען תפריטים..."; /* Text displayed while loading site People. */ "Loading People..." = "טוען אנשים..."; -/* Phrase to show when the user has searched for images and they are being loaded. */ -"Loading Photos..." = "טוען תמונות..."; - /* Text displayed while loading plans details */ "Loading Plan..." = "טוען תוכנית..."; @@ -4300,8 +4201,7 @@ translators: Block name. %s: The localized block name */ /* Local Services site intent topic */ "Local Services" = "שירותים מקומיים"; -/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. - Title of the Local Changes Badge */ +/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. */ "Local changes" = "שינויים מקומיים"; /* Title for alert when a generic error happened when trying to find the location of the device */ @@ -4465,7 +4365,6 @@ translators: Block name. %s: The localized block name */ "Max Video Upload Size" = "הגודל המקסימלי להעלאת וידאו"; /* Accessibility label for the Me button in My Site. - Label for the post author filter. This filter shows posts only authored by the current user. Me page title Noun. Title. Links to the Me screen. The accessibility value of the me tab. @@ -4473,9 +4372,7 @@ translators: Block name. %s: The localized block name */ "Me" = "אני"; /* Noun. Title. Links to the blog's Media library. - Tab bar title for the Media tab in Media Picker The menu item to select during a guided tour. - Title for Media Library section of the app. Title for the media section in site settings screen Title label for the media settings section in the app settings */ "Media" = "מדיה"; @@ -4487,13 +4384,6 @@ translators: Block name. %s: The localized block name */ /* Label for size of media cache in the app. */ "Media Cache Size" = "גודל מטמון של פריט מדיה"; -/* Title for alert when access to media capture is not granted */ -"Media Capture" = "לכידת מדיה"; - -/* Title for alert when a generic error happened when loading media - Title for alert when access to the media library is not granted by the user */ -"Media Library" = "ספריית מדיה"; - /* Title for action sheet with media options. */ "Media Options" = "אפשרויות מדיה"; @@ -4516,9 +4406,6 @@ translators: Block name. %s: The localized block name */ /* translators: %s: block title e.g: \"Paragraph\". */ "Media options" = "אפשרויות מדיה"; -/* Alert title when there is issues loading an asset to preview. */ -"Media preview failed." = "תצוגה מקדימה של פריט מדיה נכשלה."; - /* Alert displayed to the user when multiple media items have uploaded successfully. */ "Media uploaded (%ld files)" = "פרטי המדיה הועלו (%ld קבצים)"; @@ -4556,9 +4443,6 @@ translators: Block name. %s: The localized block name */ Label for the share message field on the post settings. */ "Message" = "הודעה"; -/* Title of section containing image / video metadata such as size and file type */ -"Metadata" = "מטא-נתונים"; - /* Option to select the Microsft Outlook app when logging in with magic links */ "Microsoft Outlook" = "Microsoft Outlook"; @@ -4578,13 +4462,11 @@ translators: Block name. %s: The localized block name */ "Months and Years" = "חודשים ושנים"; /* Accessibility label for more button in dashboard quick start card. - Accessibility label for the More button in Page List. Accessibility label for the More button in Post List (compact view). Accessibility label for the More button on formatting toolbar. Accessibility label for the More button on Reader Cell Accessibility label for the More button on Reader's post details - Action button to display more available options - Label for the more post button. Tapping displays an action sheet with post options. */ + Action button to display more available options */ "More" = "עוד"; /* Action button to display more available options @@ -4642,15 +4524,8 @@ translators: Block name. %s: The localized block name */ /* Screen reader text for button that will move the menu item */ "Move menu item" = "להעביר פריט תפריט"; -/* Label for a button that moves a page to the draft folder - Label for an option that moves a post to the draft folder */ -"Move to Draft" = "העברה לטיוטות"; - -/* Label for a button that moves a page to the trash folder - Label for a option that moves a post to the trash folder - Title for button on the comment details page that moves the comment to trash when tapped. +/* Title for button on the comment details page that moves the comment to trash when tapped. Trash option in the trash confirmation alert. - Trash option in the trash page confirmation alert. Trashes the comment */ "Move to Trash" = "העברה לפח"; @@ -4682,7 +4557,8 @@ translators: Block name. %s: The localized block name */ Title of My Site tab */ "My Site" = "אתר שלי"; -/* Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ +/* Title for site picker screen. + Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ "My Sites" = "האתרים שלי"; /* Siri Suggestion to open My Sites */ @@ -4932,9 +4808,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title for the view when there aren't any Activities to display in the Activity Log for a given filter. */ "No matching events found." = "לא נמצאו אירועים תואמים."; -/* Message displayed when no results are returned from a media library search. Should match Calypso. - Phrase to show when the user search for images but there are no result to show. - Phrase to show when the user searches for GIFs but there are no result to show. */ +/* Message displayed when no results are returned from a media library search. Should match Calypso. */ "No media matching your search" = "לא נמצאו פרטי מדיה שמתאימים לחיפוש שלך"; /* Menus text shown when no menus were available for loading the Menus editor. */ @@ -4952,8 +4826,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Displayed in the Notifications Tab as a title, when there are no notifications */ "No notifications yet" = "אין עדיין התראות"; -/* Displayed when the user is searching the pages list and there are no matching pages - Text displayed when there's no matching with the text search */ +/* Text displayed when there's no matching with the text search */ "No pages matching your search" = "לא נמצאו עמודים שמתאימים לחיפוש שלך"; /* Text displayed when search for plugins returns no results */ @@ -4974,9 +4847,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Message shown whent the reader finds no posts for the chosen tag */ "No posts have been made recently with this tag." = "לא נוצרו פוסטים בזמן האחרון עם תגית זו."; -/* Displayed when the user is searching the posts list and there are no matching posts */ -"No posts matching your search" = "לא נמצאו פוסטים שמתאימים לחיפוש שלך"; - /* Accessibility value for a Stats' Posting Activity Month if there are no posts. */ "No posts." = "אין פוסטים."; @@ -5077,9 +4947,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* A message title */ "Nothing liked yet" = "עדיין אין לייקים"; -/* Default message for empty media picker */ -"Nothing to show" = "אין מה לראות כאן"; - /* Notifications Details Accessibility Identifier */ "Notification Details Table" = "טבלת פרטי הודעות"; @@ -5139,7 +5006,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Alert dismissal title Button title. Acknowledges a prompt. Button title. An acknowledgement of the message displayed in a prompt. - Confirmation of action Default action Dismisses the alert Marks all notifications as read. @@ -5201,9 +5067,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* No comment provided by engineer. */ "Only show excerpt" = "להציג את התקציר בלבד"; -/* Message telling the user that they've only enabled limited photo library permissions for the app. */ -"Only the selected photos you've given access to are available." = "רק התמונות הנבחרות שאליהן סיפקת גישה יהיו זמינות."; - /* An informal exclaimation that means `something went wrong`. Title for the view when there's an error loading a comment. Title for the view when there's an error loading Activity Log @@ -5238,9 +5101,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title of a button that opens the apps settings in the system Settings.app */ "Open Settings" = "פתיחת הגדרות"; -/* Editor button to swich the media picker from quick mode to full picker */ -"Open full media picker" = "פתיחה של בוחר המדיה המלא"; - /* No comment provided by engineer. */ "Open in Safari" = "פתח בדפדפן"; @@ -5280,6 +5140,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Divider on initial auth view separating auth options. */ "Or" = "או"; +/* Instruction text for other forms of two-factor auth methods. */ +"Or choose another form of authentication." = "לחלופין, יש לבחור אמצעי אימות אחר."; + /* Label for button to log in using site address. Underscores _..._ denote underline. */ "Or log in by _entering your site address_." = "לחלופין, ניתן להתחבר באמצעות _הזנת הכתובת של האתר שלך_."; @@ -5338,15 +5201,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title shown when selecting a post type of Page from the Share Extension. */ "Page" = "עמוד"; -/* Prompts the user that a restored page was moved to the drafts list. */ -"Page Restored to Drafts" = "עמוד שוחזר למצב 'טיוטה'"; - -/* Prompts the user that a restored page was moved to the published list. */ -"Page Restored to Published" = "עמוד שוחזר למצב 'פורסם'"; - -/* Prompts the user that a restored page was moved to the scheduled list. */ -"Page Restored to Scheduled" = "עמוד שוחזר למצב 'מתוזמן לפרסום'"; - /* Name of the button to open the page settings The title of the Page Settings screen. */ "Page Settings" = "הגדרות עמוד"; @@ -5363,9 +5217,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of notification displayed when a page has failed to upload. */ "Page failed to upload" = "ההעלאה של העמוד נכשלה"; -/* A short message explaining that a page was moved to the trash bin. */ -"Page moved to trash." = "עמוד הועבר לפח."; - /* Title of notification displayed when a page has been successfully saved as a draft. */ "Page pending review" = "העמוד ממתין לביקורת"; @@ -5437,8 +5288,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title of pending Comments filter. */ "Pending" = "ממתין לאישור"; -/* Name for the status of a post pending review. - Title of the Pending Review Badge */ +/* Name for the status of a post pending review. */ "Pending review" = "ממתין לאישור"; /* Noun. Title of the people management feature. @@ -5467,12 +5317,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Photography site intent topic */ "Photography" = "צילום"; -/* Tab bar title for the Photos tab in Media Picker */ -"Photos" = "תמונות"; - -/* Subtitle for placeholder in Free Photos. The company name 'Pexels' should always be written as it is. */ -"Photos provided by Pexels" = "התמונות מובאות בחסות Pexels"; - /* Title for selecting a new username in the site creation flow. */ "Pick username" = "בחירת שם משתמש"; @@ -5565,7 +5409,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Please enter the password for your WordPress.com account to log in with your Apple ID." = "עליך להזין את הסיסמה לחשבון שלך ב-WordPress.com כדי להתחבר עם ה-Apple ID שלך."; /* Instruction text on the two-factor screen. */ -"Please enter the verification code from your authenticator app, or tap the link below to receive a code via SMS." = "יש להזין את קוד האימות מאפליקציית האימות או להקיש על הקישור מתחת כדי לקבל קוד באמצעות הודעת טקסט (SMS)."; +"Please enter the verification code from your authenticator app." = "יש להזין את קוד האימות מאפליקציית האימות."; /* Popup message to ask for user credentials (fields shown below). */ "Please enter your credentials" = "הזינו את הפרטים"; @@ -5660,15 +5504,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ The post formats available for the post. Should be the same as in core WP. */ "Post Format" = "פורמט"; -/* Prompts the user that a restored post was moved to the drafts list. */ -"Post Restored to Drafts" = "פוסט שוחזר למצב \"טיוטה\""; - -/* Prompts the user that a restored post was moved to the published list. */ -"Post Restored to Published" = "פוסט שוחזר למצב \"פורסם\""; - -/* Prompts the user that a restored post was moved to the scheduled list. */ -"Post Restored to Scheduled" = "פוסט שוחזר למצב \"מתוזמן לפרסום\""; - /* Name of the button to open the post settings The title of the Post Settings screen. */ "Post Settings" = "הגדרות פוסט"; @@ -5688,9 +5523,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of notification displayed when a post has failed to upload. */ "Post failed to upload" = "העלאת הפוסט נכשלה"; -/* A short message explaining that a post was moved to the trash bin. */ -"Post moved to trash." = "פוסט הועבר לפח."; - /* Title of notification displayed when a post has been successfully saved as a draft. */ "Post pending review" = "הפוסט ממתין לביקורת"; @@ -5749,9 +5581,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Period Stats 'Posts and Pages' header */ "Posts and Pages" = "פוסטים ועמודים"; -/* Title of the Posts Page Badge */ -"Posts page" = "עמוד הפוסטים"; - /* Message informing the user that their static homepage for posts was set successfully */ "Posts page successfully updated" = "עמוד הפוסטים עודכן בהצלחה"; @@ -5764,9 +5593,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* A message explaining the Posts I Like feature in the reader */ "Posts that you like will appear here." = "כאן יופיעו פוסטים שסימנת ב'לייק'."; -/* Subtitle for placeholder in Tenor picker. `The company name 'Tenor' should always be written as it is. */ -"Powered by Tenor" = "מופעל על ידי Tenor"; - /* Browse premium themes selection title */ "Premium" = "Premium"; @@ -5785,18 +5611,12 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title for screen to preview a static content. */ "Preview" = "תצוגה מקדימה"; -/* Action for Media Picker to preview the selected media items. The argument in the string represents the number of elements (as numeric digits) selected */ -"Preview %@" = "תצוגה מקדימה של %@"; - /* Title for web preview device switching button */ "Preview Device" = "תצוגה מקדימה של המכשיר"; /* Title on display preview error */ "Preview Unavailable" = "תצוגה מקדימה לא זמינה"; -/* Accessibility label for media item preview for user's viewing an item in their media library */ -"Preview media" = "תצוגה מקדימה של מדיה"; - /* No comment provided by engineer. */ "Preview page" = "תצוגה מקדימה של העמוד"; @@ -5843,8 +5663,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Privacy notice for California users" = "הצהרת פרטיות למשתמשים מקליפורניה"; /* Name for the status of a post that is marked private. - Privacy setting for posts set to 'Private'. Should be the same as in core WP. - Title of the Private Badge */ + Privacy setting for posts set to 'Private'. Should be the same as in core WP. */ "Private" = "פרטי"; /* No comment provided by engineer. */ @@ -5894,12 +5713,10 @@ translators: %s: Select control button label e.g. \"Button width\" */ Label for the publish date button. */ "Publish Date" = "תאריך פרסום"; -/* A short phrase indicating a post is due to be immedately published. - Label for a button that moves a page to the published folder, publishing with the current date/time. */ +/* A short phrase indicating a post is due to be immedately published. */ "Publish Immediately" = "פרסום מיידי"; /* Label for a button that publishes the post - Label for an option that moves a publishes a post immediately Title of button allowing the user to immediately publish the post they are editing. */ "Publish Now" = "לפרסם כעת"; @@ -5917,8 +5734,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Name for the status of a published post. Period Stats 'Published' header - Title of the published filter. This filter shows a list of posts that the user has published. - Title of the published header in search list. */ + Title of the published filter. This filter shows a list of posts that the user has published. */ "Published" = "פורסם"; /* Precedes the name of the blog just posted on */ @@ -6060,8 +5876,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of the completion screen of the Blogging Reminders Settings screen when the reminders are removed. */ "Reminders removed" = "התזכורות הוסרו"; -/* Add asset to media picker list - Alert button to confirm a plugin to be removed +/* Alert button to confirm a plugin to be removed Button label when removing a blog Label for the Remove Feature Image icon. Tapping will show a confirmation screen for removing the feature image from the post. Remove Action @@ -6214,9 +6029,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of secondary button on alert prompting verify their accounts while attempting to publish */ "Resend" = "שליחה מחדש"; -/* Title of the reset button */ -"Reset" = "אתחול"; - /* Accessibility label for the reset activity type button */ "Reset Activity Type filter" = "לאפס את המסנן של סוג הפעילות"; @@ -6271,12 +6083,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ Button title, displayed when media has failed to upload. Allows the user to try the upload again. Button title. Retries uploading a post. If a user taps the button with this label, the action that evinced this error view will be retried. - Label for a button that attempts to re-upload a page that previously failed to upload. - Label for the retry post upload button. Tapping attempts to upload the post again. Opens the media library . Retries the upload of a user's post. Retry updating User's Role - Retry uploading the post. Retry. Action Retry. Verb – retry a failed media upload. The Jetpack view button title used when an error occurred @@ -6288,9 +6097,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Button title that triggers a scan */ "Retry Scan" = "לנסות לסרוק שוב"; -/* User action to retry media upload. */ -"Retry Upload" = "נסה להעלות שוב"; - /* User action to retry all failed media uploads. */ "Retry all" = "ניסיון חוזר של הכול"; @@ -6388,9 +6194,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Accessibility label for the 'Save Post' button when a post has been saved. */ "Saved Post" = "הפוסט נשמר"; -/* Text displayed in HUD when a media item's metadata (title, etc) is saved successfully. */ -"Saved!" = "נשמר!"; - /* Accessibility hint for the 'Save Post' button. */ "Saves this post for later." = "שומר את הפוסט הנוכחי למועד מאוחר יותר."; @@ -6401,7 +6204,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Saving post…" = "שומר פוסט..."; /* Menus save button title while it is saving a Menu. - Text displayed in HUD while a media item's metadata (title, etc) is being saved. Text displayed in HUD while a post is being saved as a draft. */ "Saving..." = "שומר..."; @@ -6492,21 +6294,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* No comment provided by engineer. */ "Search or type URL" = "לחפש או להקליד כתובת URL"; -/* Text displayed when the search controller will be presented */ -"Search pages" = "חיפוש עמודים"; - -/* Text displayed when the search controller will be presented */ -"Search posts" = "חיפוש פוסטים"; - /* No comment provided by engineer. */ "Search settings" = "הגדרות חיפוש"; -/* Title for placeholder in Tenor picker */ -"Search to find GIFs to add to your Media Library!" = "חיפוש קובצי GIF שניתן להוסיף לספריית מדיה שלך!"; - -/* Title for placeholder in Free Photos */ -"Search to find free photos to add to your Media Library!" = "חיפוש של תמונות להוספה לספריית המדיה שלך בחינם!"; - /* Menus search bar placeholder text. */ "Search..." = "חיפוש..."; @@ -6577,9 +6367,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Register Domain - Domain contact information field placeholder for Country */ "Select Country" = "בחירת מדינה"; -/* Title of button that allows the user to select more photos to access within the app */ -"Select More" = "לבחור אפשרויות נוספות"; - /* Blog Picker's Title */ "Select Site" = "בחירת אתר"; @@ -6601,9 +6388,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Register domain - Title for the Choose domain button of Suggested domains screen */ "Select domain" = "לבחור דומיין"; -/* Accessibility hint for actions when displaying media items. */ -"Select media." = "בחירת מדיה."; - /* Accessibility label for selecting paragraph style button on formatting toolbar. */ "Select paragraph style" = "בחירת סגנון פסקה"; @@ -6707,19 +6491,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label for connected service in Publicize stat. */ "Service" = "שירות"; -/* Label for a button that opens the Set Parent options view controller - Navigation title displayed on the navigation bar */ +/* Navigation title displayed on the navigation bar */ "Set Parent" = "הגדרת הורה"; /* No comment provided by engineer. */ "Set as Featured Image" = "להגדיר כתמונה מרכזית"; -/* Label for a button that sets the selected page as the site's Homepage */ -"Set as Homepage" = "להגדיר כעמוד הבית"; - -/* Label for a button that sets the selected page as the site's Posts page */ -"Set as Posts Page" = "להגדיר כעמוד פוסטים"; - /* Notice confirming that an image has been set as the post's featured image. */ "Set as featured image" = "להגדיר כתמונה מרכזית"; @@ -6763,7 +6540,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Accessibility label for share buttons in nav bars Button label to share a post Button label to share a web page - Share the post. Shares the comment URL Spoken accessibility label Title for a button that allows the user to share their answer to the prompt. @@ -7149,8 +6925,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Name of setting configured when a site uses a static page as its homepage */ "Static Homepage" = "עמוד בית קבוע"; -/* Label for post stats option. Tapping displays statistics for a post. - Noun. Abbreviation of Statistics. Name of the Stats feature +/* Noun. Abbreviation of Statistics. Name of the Stats feature Noun. Abbv. of Statistics. Links to a blog's Stats screen. Stats 3D Touch Shortcut Stats window title @@ -7181,9 +6956,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label text that defines a post marked as sticky */ "Sticky" = "דביק"; -/* Accessibility label for a sticky post in the post list. */ -"Sticky." = "דביק."; - /* User action to stop upload. */ "Stop upload" = "לעצור העלאה"; @@ -7240,7 +7012,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ Theme Support action title */ "Support" = "תמיכה"; -/* Button used to switch site */ +/* Title for back button that leads to the site picker screen. */ "Switch Site" = "החלפת אתר"; /* Switches the Editor to HTML Mode */ @@ -7328,9 +7100,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label explaining why users might want to add tags. */ "Tags help tell readers what a post is about. Separate different tags with commas." = "התגיות יעזרו לקוראים להבין מה הנושא של הפוסט. להפריד תגיות שונות באמצעות פסיקים."; -/* Menu option for taking an image or video with the device's camera. */ -"Take Photo or Video" = "צילום תמונה או וידאו"; - /* No comment provided by engineer. */ "Take a Photo" = "לצלם תמונה"; @@ -7401,12 +7170,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Accessibility hint */ "Tap to select the previous period" = "יש להקיש כדי לבחור את התקופה הקודמת"; -/* Accessibility hint for button used to switch site */ -"Tap to switch to another site, or add a new site" = "יש להקיש כדי להחליף לאתר אחר או להוסיף אתר חדש"; - -/* Accessibility hint for media item preview for user's viewing an item in their media library */ -"Tap to view media in full screen" = "יש להקיש כדי להציג מדיה במסך מלא"; - /* Accessibility hint for a button that opens a new view with more details. */ "Tap to view more details." = "Tap to view more details."; @@ -7452,10 +7215,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* No comment provided by engineer. */ "Text formatting controls are located within the toolbar positioned above the keyboard while editing a text block" = "הפקדים לעיצוב טקסט נמצאים בתוך סרגל הכלים שממוקם מעל המקלדת כאשר עורכים בלוק טקסט"; -/* Button title - The button's title text to send a 2FA code via SMS text message. */ +/* Button title */ "Text me a code instead" = "שליחת קוד במקום"; +/* The button's title text to send a 2FA code via SMS text message. */ +"Text me a code via SMS" = "שלחו לי קוד ב-SMS"; + /* Message of alert when theme activation succeeds */ "Thanks for choosing %@ by %@" = "תודה שבחרת ב-%1$@ מאת %2$@"; @@ -7483,9 +7248,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Error message shown to a user who is trying to share to Facebook but does not have any available Facebook Pages. */ "The Facebook connection cannot find any Pages. Publicize cannot connect to Facebook Profiles, only published Pages." = "החיבור עם פייסבוק לא הצליח למצוא דפים. השיתוף האוטומטי לא יכול להתחבר אל פרופילים בפייסבוק, רק לדפים שפורסמו."; -/* Message shown when a GIF failed to load while trying to add it to the Media library. */ -"The GIF could not be added to the Media Library." = "לא ניתן להוסיף את קובץ GIF לספריית המדיה."; - /* Description shown when a user logs in with Google but no matching WordPress.com account is found */ "The Google account \"%@\" doesn't match any account on WordPress.com" = "חשבון ה-Google‏ \"%@\" לא תואם לשום חשבון ב-WordPress.com‏"; @@ -7613,7 +7375,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Error message shown when user attempts to remove the site owner. */ "The user you are trying to remove is the owner of this site. Please contact support for assistance." = "המשתמש שניסית להסיר רשום בתור הבעלים של האתר. כדאי לפנות לתמיכה לעזרה."; -/* Error message informing a user about an invalid password. */ +/* No comment provided by engineer. */ "The username or password stored in the app may be out of date. Please re-enter your password in the settings and try again." = "פג תוקפם של שם המשתמש והססמה השמורים באפליקציה. יש להזין מחדש את הססמה בהגדרות ולנסות שנית."; /* Message shown when a video failed to load while trying to add it to the Media library. */ @@ -7681,9 +7443,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* A short error message letting the user know about a problem displaying a post. */ "There was a problem displaying this post." = "אירעה בעיה בעת הצגת הפוסט."; -/* Error message displayed when the Media Library is unable to load a full sized preview of an item. */ -"There was a problem loading the media item." = "נתקלנו בבעיה בטעינת פריט מדיה זה."; - /* The loading view subtitle displayed when an error occurred */ "There was a problem loading your data, refresh your page to try again." = "הייתה בעיה בטעינת הנתונים שלך, יש לרענן את העמוד ולנסות שוב."; @@ -7696,9 +7455,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Explaining to the user there was an error trying to obtain the current location of the user. */ "There was a problem when trying to access your location. Please try again later." = "אירעה בעיה בניסיון לגשת למיקום שלך. יש לנסות שוב מאוחר יותר."; -/* Explaining to the user there was an generic error accesing media. */ -"There was a problem when trying to access your media. Please try again later." = "אירעה בעיה בניסיון לגשת למדיה שלך. כדאי לנסות שוב מאוחר יותר."; - /* Message for stories unknown error. */ "There was a problem with the Stories editor. If the problem persists you can contact us via the Me > Help & Support screen." = "אירעה בעיה בעורך הסטוריז. אם הבעיה נמשכת, אפשר ליצור איתנו קשר באמצעות המסך 'עזרה ותמיכה' תחת 'אני'."; @@ -7769,9 +7525,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* A description informing the user in order to proceed with this feature we will need camera permissions, and how to enable it. */ "This app needs permission to access the Camera to scan login codes, tap on the Open Settings button to enable it." = "לאפליקציה הזאת נדרשת הרשאת גישה למצלמה לצורך סריקת קודים של התחברות. יש להקיש על הכפתור 'לפתוח את ההגדרות' כדי לאפשר את הגישה."; -/* Explaining to the user why the app needs access to the device media library. */ -"This app needs permission to access your device media library in order to add photos and\/or video to your posts. Please change the privacy settings if you wish to allow this." = "אפליקציה זו זקוקה להרשאה כדי לגשת לספריית המדיה במכשיר שלך ולהוסיף תמונות ו\/או סרטוני וידאו לפוסטים שלך. יש לשנות את הגדרות הפרטיות אם ברצונך לאפשר זאת."; - /* No comment provided by engineer. */ "This color combination may be hard for people to read. Try using a brighter background color and\/or a darker text color." = "ייתכן ששילוב הצבעים הזה יהיה מאתגר לקריאה עבור אנשים מסוימים. אנחנו ממליצים לנסות צבע רקע בהיר יותר ו\/או צבע טקסט כהה יותר."; @@ -7881,6 +7634,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Description of a Quick Start Tour */ "Time to finish setting up your site! Our checklist walks you through the next steps." = "הגיע הזמן לסיים את הגדרת האתר שלך! רשימת המשימות שלנו תדריך אותך בשלבים הבאים."; +/* Error when the uses takes more than 1 minute to submit a security key. */ +"Time's up, but don't worry, your security is our priority. Please try again!" = "נגמר הזמן הקצוב, אבל לא לדאוג: האבטחה שלך בעדיפות ראשונה אצלנו. יש לנסות שוב!"; + /* WordPress.com Marketing Footer Text */ "Tips for getting the most out of WordPress.com." = "טיפים כדי להפיק את המיטב מ-WordPress.com."; @@ -8004,24 +7760,20 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title for the traffic section in site settings screen */ "Traffic" = "תעבורה"; +/* Describes a domain that was transferred from elsewhere to wordpress.com */ +"Transferred Domain" = "להעביר דומיין"; + /* translators: %s: block title e.g: \"Paragraph\". */ "Transform %s to" = "לשנות את %s אל"; /* No comment provided by engineer. */ "Transform block…" = "לשנות בלוק..."; -/* Accessibility label for trash button to delete items from the user's media library - Accessibility label for trash buttons in nav bars +/* Accessibility label for trash buttons in nav bars Trashes a comment Trashes the comment */ "Trash" = "לאשפה"; -/* Accessibility hint for trash button to delete items from the user's media library */ -"Trash selected media" = "להעביר את המדיה הנבחרת לפח"; - -/* Title of the trash page confirmation alert. */ -"Trash this page?" = "האם להעביר את העמוד לפח?"; - /* Title of the trash confirmation alert. */ "Trash this post?" = "האם להעביר את הפוסט לפח?"; @@ -8139,9 +7891,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Shown when a user logs in with Google but it subsequently fails to work as login to WordPress.com */ "Unable To Connect" = "אי אפשר להתחבר"; -/* An error message. */ -"Unable to Connect" = "אי אפשר להתחבר"; - /* Title for stories unknown error. */ "Unable to Create Stories Editor" = "לא ניתן ליצור את עורך הסטוריז"; @@ -8157,9 +7906,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* An error message shown when there is an issue creating new invite links. */ "Unable to create new invite links." = "לא ניתן ליצור קישורי הזמנה חדשים."; -/* Text displayed in HUD if there was an error attempting to delete a group of media items. */ -"Unable to delete all media items." = "לא ניתן למחוק את כל פריטי המדיה."; - /* Text displayed in HUD if there was an error attempting to delete a media item. */ "Unable to delete media item." = "לא ניתן למחוק פריט מדיה."; @@ -8223,12 +7969,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Message displayed when sharing a link to the downloadable backup fails. */ "Unable to share link" = "לא ניתן לשתף את הקישור"; -/* Message that appears when a user tries to trash a page while their device is offline. */ -"Unable to trash pages while offline. Please try again later." = "אין אפשרות להעביר עמודים לפח במצב לא מחובר. יש לנסות שוב מאוחר יותר."; - -/* Message that appears when a user tries to trash a post while their device is offline. */ -"Unable to trash posts while offline. Please try again later." = "אין אפשרות להעביר פוסטים לפח במצב לא מחובר. יש לנסות שוב מאוחר יותר."; - /* Notice title when turning site notifications off fails. */ "Unable to turn off site notifications" = "לא ניתן להשבית את ההודעות של האתר"; @@ -8301,8 +8041,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ Button title. Reverts the previous notification operation Revert an operation Revert enabling notification after successfully subcribing to the comments for the post. - The title of an 'undo' button. Tapping the button moves a trashed page out of the trash folder. - The title of an 'undo' button. Tapping the button moves a trashed post out of the trash folder. Undo action */ "Undo" = "ביטול פעולה אחרונה"; @@ -8345,9 +8083,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title for Unknown HTML Editor */ "Unknown HTML" = "HTML לא מוכר"; -/* Label to use when creation date from media asset is not know. */ -"Unknown creation date" = "תאריך יצירה לא ידוע"; - /* No comment provided by engineer. */ "Unknown error" = "שגיאה לא ידועה"; @@ -8513,6 +8248,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of a row displayed on the debug screen used to configure the sandbox store use in the App. */ "Use Sandbox Store" = "להשתמש בחנות של Sandbox"; +/* The button's title text to use a security key. */ +"Use a security key" = "להשתמש במפתח אבטחה"; + /* Option to enable the block editor for new posts */ "Use block editor" = "להשתמש בעורך הבלוקים"; @@ -8588,15 +8326,10 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of an alert informing users that the video they are trying to select is not allowed. */ "Video not uploaded" = "הסרטון לא הועלה"; -/* Accessibility label for video thumbnails in the media collection view. The parameter is the creation date of the video. */ -"Video, %@" = "וידאו, %@"; - /* Period Stats 'Videos' header */ "Videos" = "סרטונים"; /* Button title. Displays a summary / sharing screen for a specific post. - Label for a button that opens the page when tapped. - Label for the view post button. Tapping displays the post as it appears on the web. Opens the post epilogue screen to allow sharing / viewing of a post. Theme View action title Verb. The screen title shown when viewing a post inside the app. */ @@ -8711,6 +8444,10 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Message shown on screen while waiting for Google to finish its signup process. */ "Waiting for Google to complete…" = "ממתין להשלמת הפעולה על ידי Google..."; +/* Text while the webauthn signature is being verified + Text while waiting for a security key challenge */ +"Waiting for security key" = "ממתין למפתח אבטחה"; + /* View title during the Google auth process. */ "Waiting..." = "ממתין..."; @@ -9082,6 +8819,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* An error message shown when a wpcom user provides the wrong password. */ "Whoops, something went wrong and we couldn't log you in. Please try again!" = "משהו השתבש ולא ניתן לבצע חיבור לחשבון. יש לנסות שוב!"; +/* Generic error on the 2FA screen */ +"Whoops, something went wrong. Please try again!" = "אופס, משהו השתבש. יש לנסות שוב!"; + +/* Error when the uses chooses an invalid security key on the 2FA screen. */ +"Whoops, that security key does not seem valid. Please try again with another one" = "אופס, נראה שיש לנו כאן מפתח אבטחה לא תקף. נא לנסות שוב עם מפתח אחר"; + /* Error message shown when an incorrect two factor code is provided. */ "Whoops, that's not a valid two-factor verification code. Double-check your code and try again!" = "הקוד לאימות דו-שלבי אינו תקף. יש לבדוק שוב את הקוד ולנסות שנית!"; @@ -9109,9 +8852,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Siri Suggestion to open Support */ "WordPress Help" = "עזרה של WordPress"; -/* Name for the WordPress Media Library */ -"WordPress Media" = "מדיה של WordPress"; - /* Accessibility label for selecting an image or video from the user's WordPress media library on formatting toolbar. */ "WordPress Media Library" = "ספריית המדיה של WordPress"; @@ -9426,9 +9166,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Share extension error dialog text. */ "Your account does not have permission to upload media to this site. The Site Administrator can change these permissions." = "החשבון שלך לא מורשה להעלות מדיה לאתר זה. מנהל המערכת של האתר יכול לשנות את ההרשאות האלו."; -/* Explaining to the user why the app needs access to the device media library. */ -"Your app is not authorized to access media library due to active restrictions such as parental controls. Please check your parental control settings in this device." = "לאפליקציה שלך אין הרשאות גישה לספריית המדיה עקב הגבלות פעילות, כגון בקרת הורים. יש לבדוק את ההגדרות של בקרת ההורים במכשיר זה."; - /* Title for the Jetpack Backup Complete message. */ "Your backup is now available for download" = "הגיבוי שלך כעת מוכן להורדה"; @@ -9447,9 +9184,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of the site address section in the Domains Dashboard. */ "Your free WordPress.com address is" = "הכתובת החינמית שלך ב-WordPress.com היא"; -/* Error message when picked media cannot be imported into stories. */ -"Your media could not be exported. If the problem persists you can contact us via the Me > Help & Support screen." = "לא ניתן היה לייצא את המדיה שלך. If the problem persists you can contact us via the Me > מסך 'עזרה ותמיכה'."; - /* Details about recently acquired domain on domain credit redemption success screen */ "Your new domain %@ is being set up. It may take up to 30 minutes for your domain to start working." = "כעת מתבצע תהליך ההגדרה של הדומיין החדש שלך, %@. יידרשו עד 30 שעות להתחלת הפעילות של הדומיין."; @@ -9573,8 +9307,38 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* This is the string we display when prompting the user to review the WordPress app */ "appRatings.wordpress.prompt" = "מה דעתך על WordPress?"; -/* Label displayed on audio media items. */ -"audio" = "אודיו"; +/* Option to enable the optimization of images when uploading. */ +"appSettings.media.imageOptimizationRow" = "מיטוב תמונות"; + +/* Indicates an image will use high quality when uploaded. */ +"appSettings.media.imageQuality.high" = "High"; + +/* Indicates an image will use low quality when uploaded. */ +"appSettings.media.imageQuality.low" = "נמוכים"; + +/* Indicates an image will use maximum quality when uploaded. */ +"appSettings.media.imageQuality.maximum" = "מקסימום"; + +/* Indicates an image will use medium quality when uploaded. */ +"appSettings.media.imageQuality.medium" = "בינוני"; + +/* The quality of image used when uploading */ +"appSettings.media.imageQuality.title" = "איכות"; + +/* Title for the image quality settings option. */ +"appSettings.media.imageQualityRow" = "איכות תמונה"; + +/* Message of an alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.message" = "מיטוב התמונות מכווץ כעת תמונות להעלאה מהירה יותר.\n\nThis option is enabled by default, but you can change it in the app settings at any time."; + +/* Title of an alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.title" = "Keep optimizing images?"; + +/* Title of button for turning off image optimization, displayed in the alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.turnOff" = "No, turn off"; + +/* Title of button for leaving on image optimization, displayed in the alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.turnOn" = "Yes, leave on"; /* translators: displays audio file extension. e.g. MP3 audio file */ "audio file" = "קובץ אודיו"; @@ -9685,7 +9449,44 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "blogHeader.actionCopyURL" = "להעתיק כתובת URL"; /* Context menu button title */ -"blogHeader.actionOpenInBrowser" = "לפתוח בדפדפן"; +"blogHeader.actionVisitSite" = "לבקר באתר"; + +/* Title for a button that, when tapped, shows more info about participating in Bloganuary. */ +"bloganuary.dashboard.card.button.learnMore" = "למידע נוסף"; + +/* Short description for the Bloganuary event, shown right below the title. */ +"bloganuary.dashboard.card.description" = "לאורך כל חודש ינואר יתקבלו מ-Bloganuary הצעות כתיבה יומיות בבלוג. זהו האתגר הקהילתי שלנו לביסוס הרגלי פרסום בבלוג לשנה החדשה."; + +/* Title for the Bloganuary dashboard card while Bloganuary is running. */ +"bloganuary.dashboard.card.runningTitle" = "Bloganuary כבר כאן!"; + +/* Title for the Bloganuary dashboard card. */ +"bloganuary.dashboard.card.title" = "Bloganuary מתקרב!"; + +/* Title of a button that calls the user to enable the Blogging Prompts feature. */ +"bloganuary.learnMore.modal.button.promptsDisabled" = "להפעיל הנחיות לפרסום בבלוג"; + +/* Title of a button that will dismiss the Bloganuary modal when tapped. +Note that the word 'go' here should have a closer meaning to 'start' rather than 'move forward'. */ +"bloganuary.learnMore.modal.button.promptsEnabled" = "קדימה!"; + +/* The second line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.description.second" = "מזמינים אותם לפרסם את תשובתך."; + +/* The third line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.description.third" = "אפשר לקרוא תשובות של בלוגרים אחרים כדי לקבל השראה וליצור חיבורים חדשים."; + +/* The first line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.descriptions.first" = "כך תגיע אליך מדי יום הנחיה חדשה שתיתן לך השראה."; + +/* An additional piece of information shown in case the user has the Blogging Prompts feature disabled. */ +"bloganuary.learnMore.modal.footer.addition" = "כדי להצטרף ל-Bloganuary עליך להפעיל את 'הנחיות פרסום בבלוג'."; + +/* An informative excerpt shown in a subtler tone. */ +"bloganuary.learnMore.modal.footer.text" = "במסגרת Bloganuary נזכיר לך בעזרת 'הנחיות פרסום בבלוג' יומיות לשלוח לנו את הנושאים שלך לחודש ינואר."; + +/* The headline text of the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.headline" = "אנו מזמינים אותך להצטרף לאתגר הכתיבה שלנו שיימשך חודש שלם"; /* Verb. Dismisses the blogging prompt notification. */ "bloggingPrompt.pushNotification.customActionDescription.dismiss" = "ביטול"; @@ -9714,6 +9515,12 @@ Example: Comment on Example: Reply to Pamela Nguyen */ "comment.header.subText.reply" = "לענות ל-%1$@"; +/* Error message informing a user about an invalid password. */ +"common.reEnterPasswordMessage" = "ייתכן ששם המשתמש או הסיסמה המאוחסנים באפליקציה אינם מעודכנים. יש להזין מחדש את הסיסמה בהגדרות ולנסות שוב."; + +/* An error message. */ +"common.unableToConnect" = "אי אפשר להתחבר"; + /* Footnote for the privacy compliance popover. */ "compliance.analytics.popover.footnote" = "קובצי Cookie אלו מאפשרים לנו לבצע אופטימיזציה של ביצועים על ידי איסוף מידע לגבי האופן שבו משתמשים יוצרים אינטראקציה עם אתרי האינטרנט שלנו."; @@ -9864,50 +9671,92 @@ Example: Reply to Pamela Nguyen */ /* Title for a menu action in the context menu on the Jetpack install card. */ "domain.dashboard.card.menu.hide" = "להסתיר את המידע"; -/* Domain Purchase Completion footer */ -"domain.purchase.preview.footer" = "יידרשו עד 30 דקות להתחלת הפעילות של הדומיין האישי."; +/* Search domain - Title for the Suggested domains screen */ +"domain.management.addDomain.search.title" = "לחפש דומיין"; -/* Domain Purchase Completion description (only for FREE domains). */ -"domain.purchase.preview.free.description" = "בשלב הבא, נעזור לך להכין את האתר לעיון."; +/* Domain management buy domain card button title */ +"domain.management.buy.card.button.title" = "לקבל דומיין"; -/* Domain Purchase Completion description (only for PAID domains). */ -"domain.purchase.preview.paid.description" = "שלחנו את הקבלה שלך באימייל. בשלב הבא, נעזור לך להכין את האתר לשימוש המבקרים."; +/* Domain management buy domain card subtitle */ +"domain.management.buy.card.subtitle" = "ניתן להוסיף אתר בהמשך."; -/* Reflects that site is live when domain purchase feature flag is ON. */ -"domain.purchase.preview.title" = "כל הכבוד, האתר שלך פורסם!"; +/* Domain management buy domain card title */ +"domain.management.buy.card.title" = "עליך רק לרכוש דומיין"; + +/* The expired label of the domain card in All Domains screen. */ +"domain.management.card.expired.label" = "התוקף פג"; + +/* The renews label of the domain card in All Domains screen. */ +"domain.management.card.renews.label" = "מתחדש"; + +/* The empty state button title in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.button.title" = "למצוא דומיין"; + +/* The empty state description in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.description" = "לאיתור הדומיין המושלם יש להקיש למטה."; + +/* The empty state title in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.title" = "עדיין אין לך דומיינים"; + +/* The empty state description in All Domains screen when an error occurs */ +"domain.management.error.empty.state.description" = "We encountered an error while loading your domains. אם הבעיה נמשכת, ניתן ליצור קשר עם התמיכה."; + +/* The empty state title in All Domains screen when an error occurs */ +"domain.management.error.empty.state.title" = "משהו השתבש"; + +/* The empty state button title in All Domains screen when an error occurs */ +"domain.management.error.state.button.title" = "יש לנסות שוב"; + +/* The empty state description in All Domains screen when the user is offline */ +"domain.management.offline.empty.state.description" = "עליך לבדוק את החיבור לרשת ולנסות שוב."; -/* Status of a domain in `Action Required` state */ -"domain.status.action.required" = "נדרשת פעולה"; +/* The empty state title in All Domains screen when the user is offline */ +"domain.management.offline.empty.state.title" = "אין חיבור אינטרנט"; -/* Status of a domain in `Active` state */ -"domain.status.active" = "פעיל"; +/* Domain management choose site card button title */ +"domain.management.purchase.footer" = "*כל התוכניות השנתיות בתשלום כוללות דומיין בחינם לשנה."; -/* Status of a domain in `Complete Setup` state */ -"domain.status.complete.setup" = "להשלים את ההגדרה"; +/* Domain management purchase domain screen title */ +"domain.management.purchase.subtitle" = "אל דאגה, ניתן בקלות להוסיף אתר בהמשך."; -/* Status of a domain in `Error` state */ -"domain.status.error" = "שגיאה"; +/* Domain management purchase domain screen title. */ +"domain.management.purchase.title" = "לבחור את אופן השימוש בדומיין שלך"; -/* Status of a domain in `Expired` state */ -"domain.status.expired" = "התוקף פג"; +/* The search bar title in All Domains screen. */ +"domain.management.search-bar.title" = "לחפש דומיינים"; -/* Status of a domain in `Expiring Soon` state */ -"domain.status.expiring.soon" = "התוקף יפוג בקרוב"; +/* The empty state description in All Domains screen when the are no domains matching the search criteria */ +"domain.management.search.empty.state.description" = "We couldn't find any domains that match your search for '%@'"; -/* Status of a domain in `Failed` state */ -"domain.status.failed" = "נכשל"; +/* The empty state title in All Domains screen when the are no domains matching the search criteria */ +"domain.management.search.empty.state.title" = "אין דומיינים תואמים."; -/* Status of a domain in `In Progress` state */ -"domain.status.in.progress" = "בביצוע"; +/* Domain management choose site card button title */ +"domain.management.site.card.button.title" = "לבחור אתר"; -/* Status of a domain in `Renew` state */ -"domain.status.renew" = "לחדש"; +/* Domain management choose site card subtitle */ +"domain.management.site.card.footer" = "דומיין חינם בשנה הראשונה*"; -/* Status of a domain in `Verify Email` state */ -"domain.status.verify.email" = "לאמת את האימייל"; +/* Domain management choose site card subtitle */ +"domain.management.site.card.subtitle" = "להשתמש בפריט באתר שכבר הקמת."; -/* Status of a domain in `Verifying` state */ -"domain.status.verifying" = "מאמת"; +/* Domain management choose site card title */ +"domain.management.site.card.title" = "אתר WordPress.com קיים"; + +/* Domain Management Screen Title */ +"domain.management.title" = "כל הדומיינים"; + +/* Domain Purchase Completion footer */ +"domain.purchase.preview.footer" = "יידרשו עד 30 דקות להתחלת הפעילות של הדומיין האישי."; + +/* Domain Purchase Completion description (only for FREE domains). */ +"domain.purchase.preview.free.description" = "בשלב הבא, נעזור לך להכין את האתר לעיון."; + +/* Domain Purchase Completion description (only for PAID domains). */ +"domain.purchase.preview.paid.description" = "שלחנו את הקבלה שלך באימייל. בשלב הבא, נעזור לך להכין את האתר לשימוש המבקרים."; + +/* Reflects that site is live when domain purchase feature flag is ON. */ +"domain.purchase.preview.title" = "כל הכבוד, האתר שלך פורסם!"; /* The 'Best Alternative' label under the domain name in 'Choose a domain' screen */ "domain.suggestions.row.best-alternative" = "החלופה הטובה ביותר"; @@ -9930,12 +9779,24 @@ Example: Reply to Pamela Nguyen */ /* The text to display for paid domains in 'Site Creation > Choose a domain' screen */ "domain.suggestions.row.yearly" = "לשנה"; +/* Title for the checkout screen. */ +"domains.checkout.title" = "קופה"; + /* Action shown in a bottom notice to dismiss it. */ "domains.failure.dismiss" = "ביטול"; /* Content show when the domain selection action fails. */ "domains.failure.title" = "מצטערים, לא ניתן לרכוש את הדומיין שברצונך להוסיף מהאפליקציה של Jetpack כעת."; +/* Title for the screen where the user can choose how to use the domain they're end up purchasing. */ +"domains.purchase.choice.title" = "לרכוש דומיין"; + +/* Back button title that navigates back to the search domains screen. */ +"domains.search.backButton.title" = "חיפוש"; + +/* Title of screen where user chooses a site to connect to their selected domain */ +"domains.sitePicker.title" = "לבחור אתר"; + /* No comment provided by engineer. */ "double-tap to change unit" = "יש להקיש פעמיים כדי לשנות את היחידה"; @@ -9953,6 +9814,15 @@ Example: Reply to Pamela Nguyen */ Site Address placeholder */ "example.com" = "example.com"; +/* Title for confirmation navigation bar button item */ +"externalMediaPicker.add" = "להוסיף"; + +/* Bottom toolbar title in the selection mode */ +"externalMediaPicker.toolbarSelectItemsPrompt" = "לבחור תמונות"; + +/* Bottom toolbar title in the selection mode */ +"externalMediaPicker.toolbarViewSelected" = "תצוגה נבחרה (%@)"; + /* Title of screen the displays the details of an advertisement campaign. */ "feature.blaze.campaignDetails.title" = "פרטי הקמפיין"; @@ -10052,9 +9922,6 @@ Example: Reply to Pamela Nguyen */ /* (placeholder) Help the user enter a URL into the field */ "http:\/\/my-site-address (URL)" = "http:\/\/my-site-address (URL)"; -/* Label displayed on image media items. */ -"image" = "תמונה"; - /* Sentence to justify why the app is asking permission from the user to use their camera. */ "infoplist.NSCameraUsageDescription" = "לצלם תמונות או סרטוני וידאו לשימוש בפוסטים שלך."; @@ -10355,6 +10222,9 @@ Example: Reply to Pamela Nguyen */ /* Indicating that referrer was marked as spam */ "marked as spam" = "סומן כ'זבל'"; +/* Products header text in Me Screen. */ +"me.products.header" = "מוצרים"; + /* Title of error prompt shown when a sync fails. */ "media.syncFailed" = "לא ניתן לסנכרן את פריטי המדיה"; @@ -10367,18 +10237,24 @@ Example: Reply to Pamela Nguyen */ /* Message of an alert informing users that the video they are trying to select is not allowed. */ "mediaExporter.videoLimitExceededError" = "להעלאת סרטון שאורכו מעל חמש דקות נדרשת תוכנית בתשלום."; -/* Verb. User action to dismiss error alert when failing to load media item. */ -"mediaItemTable.errorAlert.dismissButton" = "ביטול"; - /* Accessibility hint for add button to add items to the user's media library */ "mediaLibrary.addButtonAccessibilityHint" = "להוסיף פריט מדיה חדש"; /* Accessibility label for add button to add items to the user's media library */ "mediaLibrary.addButtonAccessibilityLabel" = "להוסיף"; +/* Button name in the more menu */ +"mediaLibrary.aspectRatioGrid" = "יחס גובה:רוחב:"; + +/* Context menu button */ +"mediaLibrary.buttonDelete" = "למחוק"; + /* Media screen navigation bar button Select title */ "mediaLibrary.buttonSelect" = "לבחור"; +/* Context menu button */ +"mediaLibrary.buttonShare" = "לשתף"; + /* Verb. Button title. Tapping cancels an action. */ "mediaLibrary.deleteConfirmationCancel" = "ביטול"; @@ -10400,6 +10276,21 @@ Example: Reply to Pamela Nguyen */ /* Text displayed in HUD after successfully deleting a media item */ "mediaLibrary.deletionSuccessMessage" = "הפריט נמחק!"; +/* The name of the media filter */ +"mediaLibrary.filterAll" = "הכל"; + +/* The name of the media filter */ +"mediaLibrary.filterAudio" = "אודיו"; + +/* The name of the media filter */ +"mediaLibrary.filterDocuments" = "מסמכים"; + +/* The name of the media filter */ +"mediaLibrary.filterImages" = "תמונות"; + +/* The name of the media filter */ +"mediaLibrary.filterVideos" = "סרטוני וידאו"; + /* User action to delete un-uploaded media. */ "mediaLibrary.retryOptionsAlert.delete" = "למחוק"; @@ -10412,6 +10303,12 @@ Example: Reply to Pamela Nguyen */ /* Message displayed when no results are returned from a media library search. Should match Calypso. */ "mediaLibrary.searchResultsEmptyTitle" = "לא נמצאו פרטי מדיה שמתאימים לחיפוש שלך"; +/* Text displayed in HUD if there was an error attempting to share a group of media items. */ +"mediaLibrary.sharingFailureMessage" = "Unable to share the selected items."; + +/* Button name in the more menu */ +"mediaLibrary.squareGrid" = "Square Grid"; + /* Media screen navigation title */ "mediaLibrary.title" = "מדיה"; @@ -10433,6 +10330,12 @@ Example: Reply to Pamela Nguyen */ /* The title of the button to dismiss the alert shown when the picked media cannot be imported into stories. */ "mediaPicker.failedMediaExportAlert.dismissButton" = "ביטול"; +/* Error message when picked media cannot be imported into stories. */ +"mediaPicker.failedMediaExportAlert.message" = "לא ניתן היה לייצא את המדיה שלך מסך 'עזרה ותמיכה'. אם הבעיה נמשכת, אפשר ליצור איתנו קשר באמצעות המסך 'עזרה ותמיכה' תחת 'אני'."; + +/* Error title when picked media cannot be imported into stories. */ +"mediaPicker.failedMediaExportAlert.title" = "ייצוא המדיה נכשל"; + /* Message for alert when access to camera is not granted */ "mediaPicker.noCameraAccessMessage" = "אפליקציה זו זקוקה להרשאה כדי לגשת למצלמה ולצלם מדיה חדשה, יש לשנות את הגדרות הפרטיות אם ברצונך לאפשר זאת."; @@ -10466,6 +10369,12 @@ Example: Reply to Pamela Nguyen */ /* The name of the action in the context menu */ "mediaPicker.takeVideo" = "צילום וידאו"; +/* Navigation title for media preview. Example: 1 of 3 */ +"mediaPreview.NofM" = "%1$@ מתוך %2$@"; + +/* Max image size in pixels (e.g. 300x300px) */ +"mediaSizeSlider.valueFormat" = "פיקסלים.⁦%1$d⁩%2$d"; + /* The description in the Delete WordPress screen */ "migration.deleteWordpress.description" = "נראה שהאפליקציה של WordPress עדיין מותקנת אצלך."; @@ -10478,9 +10387,6 @@ Example: Reply to Pamela Nguyen */ /* The title in the Delete WordPress screen */ "migration.deleteWordpress.title" = "כבר אין לך צורך באפליקציה של WordPress במכשיר לך"; -/* Primary button title in the migration done screen. */ -"migration.done.actions.primary.title" = "סיום"; - /* Footer for the migration done screen. */ "migration.done.footer" = "אנחנו ממליצים להסיר את ההתקנה של האפליקציה של WordPress כדי להימנע מהתנגשויות נתונים."; @@ -10490,6 +10396,9 @@ Example: Reply to Pamela Nguyen */ /* Primary description in the migration done screen. */ "migration.done.primaryDescription" = "העברנו את כל הנתונים וההגדרות שלך. השארנו הכול במקום."; +/* Secondary description (second paragraph) in the migration done screen. */ +"migration.done.secondaryDescription" = "It's time to continue your WordPress journey on the Jetpack app!"; + /* Title of the migration done screen. */ "migration.done.title" = "תודה שעברת אל Jetpack!"; @@ -10538,6 +10447,9 @@ Example: Reply to Pamela Nguyen */ /* The title in the migration welcome screen */ "migration.welcome.title" = "ברוך בואך אל Jetpack!"; +/* Primary button title in the migration done screen. */ +"migrationDone.actions.primaryTitle" = "קדימה"; + /* Description for the static screen displayed prompting users to switch the Jetpack app. */ "movedToJetpack.description" = "באפליקציה של Jetpack יש את כל הפונקציונליות של האפליקציה של WordPress ועכשיו גם גישה בלעדית לנתונים סטטיסטיים, ל-Reader, להודעות ועוד."; @@ -10613,6 +10525,30 @@ Example: Reply to Pamela Nguyen */ /* Message title for when a user has no sites. */ "mySite.noSites.title" = "אין לך עוד אתרים"; +/* Menu title for the add site option */ +"mySite.siteActions.addSite" = "הוספת אתר"; + +/* Button that reveals more site actions */ +"mySite.siteActions.button" = "פעולות באתר"; + +/* Accessibility hint for button used to show more site actions */ +"mySite.siteActions.hint" = "יש ללחוץ כדי להציג עוד פעולות באתר"; + +/* Menu title for the personalize home option */ +"mySite.siteActions.personalizeHome" = "להתאים אישית את הלשונית 'בית'"; + +/* Menu title for the change site icon option */ +"mySite.siteActions.siteIcon" = "לשנות את סמל אתר"; + +/* Menu title for the change site title option */ +"mySite.siteActions.siteTitle" = "לשנות את שם האתר"; + +/* Menu title for the switch site option */ +"mySite.siteActions.switchSite" = "להחליף אתר"; + +/* Menu title for the visit site option */ +"mySite.siteActions.visitSite" = "לבקר באתר"; + /* Dismiss button title. */ "noResultsViewController.dismissButton" = "ביטול"; @@ -10628,14 +10564,20 @@ Example: Reply to Pamela Nguyen */ /* This is one of the buttons we display when prompting the user for a review */ "notifications.appRatings.sendFeedback.yes.buttonTitle" = "לשלוח משוב"; -/* Word separating the current index from the total amount. I.e.: 7 of 9 */ -"of" = "מתוך"; +/* Badge for page cells */ +"pageList.badgeHomepage" = "עמוד הבית"; + +/* Badge for page cells */ +"pageList.badgeLocalChanges" = "שינויים מקומיים"; -/* Label displayed on media items that are not video, image, or audio. */ -"other" = "אחר"; +/* Badge for page cells */ +"pageList.badgePendingReview" = "ביקורת בהמתנה"; -/* Promote the page with Blaze. */ -"pages.blaze.actionTitle" = "לקדם עם Blaze"; +/* Badge for page cells */ +"pageList.badgePosts" = "פוסטים בכל עמוד"; + +/* Badge for page cells */ +"pageList.badgePrivate" = "פרטי"; /* Subtitle of the theme template homepage cell */ "pages.template.subtitle" = "עמוד הבית שלך משתמש בתבנית של ערכת עיצוב והוא ייפתח בעורך הדפדפן."; @@ -10643,12 +10585,45 @@ Example: Reply to Pamela Nguyen */ /* Title of the theme template homepage cell */ "pages.template.title" = "עמוד הבית"; +/* Message informing the user that their static homepage page was set successfully */ +"pages.updatePage.successTitle" = "העמוד עודכן בהצלחה."; + +/* Delete option in the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.actionTitle" = "למחוק לצמיתות"; + +/* Message of the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.alertMessage" = "האם ברצונך למחוק את העמוד לצמיתות?"; + +/* Title of the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.alertTitle" = "למחוק לצמיתות?"; + +/* Menu option for filtering posts by everyone */ +"pagesList.pagesByEveryone" = "Pages by everyone"; + +/* Menu option for filtering posts by me */ +"pagesList.pagesByMe" = "Pages by me"; + +/* Trash option in the trash page confirmation alert. */ +"pagesList.trash.actionTitle" = "להעביר לפח"; + +/* Message of the trash page confirmation alert. */ +"pagesList.trash.alertMessage" = "בחרת לשלוח עמוד זה לפח – האם ההחלטה סופית?"; + +/* Title of the trash page confirmation alert. */ +"pagesList.trash.alertTitle" = "האם להעביר את העמוד לפח?"; + +/* Cancels an Action */ +"pagesList.trash.cancel" = "ביטול"; + /* No comment provided by engineer. */ "password" = "סיסמה"; /* Section footer displayed below the list of toggles */ "personalizeHome.cardsSectionFooter" = "הכרטיסים עשויים להציג תוכן שונה בהתאם לפעילות באתר שלך. אנחנו עובדים על הוספה של כרטיסים ופקדים נוספים."; +/* Section header */ +"personalizeHome.cardsSectionHeader" = "להציג או להסתיר כרטיסים"; + /* Card title for the pesonalization menu */ "personalizeHome.dashboardCard.activityLog" = "פעילות אחרונה"; @@ -10679,6 +10654,42 @@ Example: Reply to Pamela Nguyen */ /* Register Domain - Domain contact information field Phone */ "phone number" = "מספר טלפון"; +/* Status mesasge for post cells */ +"post.deletingPostPermanentlyStatusMessage" = "מוחק פוסט"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.editedTimeAgo" = "Edited %@"; + +/* Status mesasge for post cells */ +"post.movingToTrashStatusMessage" = "מעביר לפח..."; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.publishedTimeAgo" = "פורסם %@"; + +/* Accessibility label for the post author in the post list. The parameter is the author name. For example, \"By Elsa.\" */ +"postList.a11y.authorChunkFormat" = "מאת %@."; + +/* Accessibility label for a post's excerpt in the post list. The parameter is the post excerpt. For example, \"Excerpt. This is the first paragraph.\" */ +"postList.a11y.exerptChunkFormat" = "תקציר. %@."; + +/* Accessibility label for a sticky post in the post list. */ +"postList.a11y.sticky" = "דביק."; + +/* Accessibility label for a post in the post list. The first placeholder is the post title. The second placeholder is the date. */ +"postList.a11y.titleAndDateChunkFormat" = "%1$@, %2$@."; + +/* Title for the 'Trash' post list row swipe action */ +"postList.swipeActionDelete" = "פח"; + +/* Title for the 'Delete' post list row swipe action */ +"postList.swipeActionDeletePermanently" = "למחוק"; + +/* Title for the 'Share' post list row swipe action */ +"postList.swipeActionShare" = "לשתף"; + +/* Title for the 'View' post list row swipe action */ +"postList.swipeActionView" = "להציג"; + /* User action to dismiss featured media options. */ "postSettings.featuredImageUploadActionSheet.dismiss" = "ביטול"; @@ -10697,9 +10708,84 @@ Example: Reply to Pamela Nguyen */ /* Button in Post Settings */ "postSettings.setFeaturedImageButton" = "קביעת תמונה מרכזית"; +/* Error message on post/page settings screen */ +"postSettings.updateFailedMessage" = "העדכון של הגדרות ההודעה נכשל."; + /* Promote the post with Blaze. */ "posts.blaze.actionTitle" = "לקדם עם Blaze"; +/* Label for the Post List option that cancels automatic uploading of a post. */ +"posts.cancelUpload.actionTitle" = "לבטל העלאה"; + +/* Label for post comments option. Tapping displays comments for a post. */ +"posts.comments.actionTitle" = "תגובות"; + +/* Label for the delete post option. Tapping permanently deletes a post. */ +"posts.delete.actionTitle" = "למחוק לצמיתות"; + +/* Label for an option that moves a post to the draft folder */ +"posts.draft.actionTitle" = "העברה לטיוטות"; + +/* Label for post duplicate option. Tapping creates a copy of the post. */ +"posts.duplicate.actionTitle" = "לשכפל"; + +/* Opens a submenu for page attributes. */ +"posts.pageAttributes.actionTitle" = "מאפייני עמוד"; + +/* Label for the preview post button. Tapping displays the post as it appears on the web. */ +"posts.preview.actionTitle" = "תצוגה מקדימה"; + +/* Label for an option that moves a publishes a post immediately */ +"posts.publish.actionTitle" = "לפרסם כעת"; + +/* Retry uploading the post. */ +"posts.retry.actionTitle" = "לנסות שוב"; + +/* Set the selected page as the homepage. */ +"posts.setHomepage.actionTitle" = "Set as homepage"; + +/* Set the parent page for the selected page. */ +"posts.setParent.actionTitle" = "הגדרת הורה"; + +/* Set the selected page as a posts page. */ +"posts.setPostsPage.actionTitle" = "להגדיר כעמוד פוסטים"; + +/* Set the selected page as a regular page. */ +"posts.setRegularPage.actionTitle" = "להגדיר כעמוד רגיל"; + +/* Label for post settings option. Tapping displays settings for a post. */ +"posts.settings.actionTitle" = "הגדרות"; + +/* Share the post. */ +"posts.share.actionTitle" = "לשתף"; + +/* Label for post stats option. Tapping displays statistics for a post. */ +"posts.stats.actionTitle" = "נתונים סטטיסטיים"; + +/* Label for a option that moves a post to the trash folder */ +"posts.trash.actionTitle" = "להעביר לפח"; + +/* Label for the view post button. Tapping displays the post as it appears on the web. */ +"posts.view.actionTitle" = "להציג"; + +/* A short message explaining that a page was deleted permanently. */ +"postsList.deletePage.message" = "Page deleted permanently"; + +/* A short message explaining that a post was deleted permanently. */ +"postsList.deletePost.message" = "Post deleted permanently"; + +/* A short message explaining that a page was moved to the trash bin. */ +"postsList.movePageToTrash.message" = "העמוד הועבר לפח"; + +/* A short message explaining that a post was moved to the trash bin. */ +"postsList.movePostToTrash.message" = "הפוסט הועבר לפח"; + +/* Menu option for filtering posts by everyone */ +"postsList.postsByEveryone" = "Posts by everyone"; + +/* Menu option for filtering posts by me */ +"postsList.postsByMe" = "Posts by me"; + /* Title for the button to subscribe to Jetpack Social on the remaining shares view */ "postsettings.social.remainingshares.subscribe" = "ניתן להירשם עכשיו לקבלת שיתופים נוספים"; @@ -10844,13 +10930,15 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Text for the 'Like' button on the reader post card cell. */ "reader.post.button.like" = "לייק"; -/* Accessibility hint for the like button on the reader post card cell - Accessibility hint for the liked button on the reader post card cell */ +/* Accessibility hint for the like button on the reader post card cell */ "reader.post.button.like.accessibility.hint" = "הוסיף לייק לפוסט."; /* Text for the 'Liked' button on the reader post card cell. */ "reader.post.button.liked" = "קיבל לייק"; +/* Accessibility hint for the liked button on the reader post card cell */ +"reader.post.button.liked.accessibility.hint" = "לביטול לייק לפוסט."; + /* Accessibility hint for the site header on the reader post card cell */ "reader.post.button.menu.accessibility.hint" = "פותח תפריט שכולל פעולות נוספות."; @@ -10914,6 +11002,12 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Title for the tooltip anchor. */ "readerDetail.tooltipAnchorTitle.accessibilityLabel" = "חדש"; +/* The button title for the transfer footer view in Register Domain screen */ +"register.domain.transfer.button.title" = "להעביר דומיין"; + +/* The title for the transfer footer view in Register Domain screen */ +"register.domain.transfer.title" = "Looking to transfer a domain you already own?"; + /* Information of what related post are and how they are presented */ "relatedPostsSettings.optionsFooter" = "פוסטים קשורים מציגים תוכן רלוונטי מהאתר שלך, מתחת לפוסטים שלך."; @@ -11013,6 +11107,21 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Accessibility hint for actions when displaying media items. */ "siteMedia.cellAccessibilityHint" = "יש לבחור מדיה."; +/* Accessibility hint for media item preview for user's viewing an item in their media library */ +"siteMediaItem.contentViewAccessibilityHint" = "יש להקיש כדי להציג מדיה במסך מלא"; + +/* Accessibility label for media item preview for user's viewing an item in their media library */ +"siteMediaItem.contentViewAccessibilityLabel" = "תצוגה מקדימה של מדיה"; + +/* Title for confirmation navigation bar button item */ +"siteMediaPicker.add" = "להוסיף"; + +/* Button selection media in media picker */ +"siteMediaPicker.deselect" = "ביטול בחירה"; + +/* Button selection media in media picker */ +"siteMediaPicker.select" = "לבחור"; + /* Media screen navigation title */ "siteMediaPicker.title" = "מדיה"; @@ -11020,7 +11129,7 @@ Example: given a notice format "Following %@" and empty site name, this will be "siteSettings.privacy.title" = "פרטיות"; /* Hint for users when hidden privacy setting is set */ -"siteVisibility.hidden.hint" = "האתר זמין לכולם, אבל מנועי חיפוש מתבקשים לא לאנדקס אותו."; +"siteVisibility.hidden.hint" = "האתר מוסתר ממבקרים, אשר רואים רק הודעת 'בקרוב' עד שהאתר יהיה מוכן לצפייה."; /* Text for privacy settings: Hidden */ "siteVisibility.hidden.title" = "מוסתר"; @@ -11181,6 +11290,12 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Dismiss the AlertView */ "stockPhotos.strings.dismiss" = "ביטול"; +/* Subtitle for placeholder in Free Photos. The company name 'Pexels' should always be written as it is. */ +"stockPhotos.subtitle" = "התמונות מובאות בחסות Pexels"; + +/* Title for placeholder in Free Photos */ +"stockPhotos.title" = "אפשר לחפש תמונות חינמיות ולהוסיף אותן לספריית המדיה שלך!"; + /* Section title for prominent suggestions */ "suggestions.section.prominent" = "בשיחה הזאת"; @@ -11328,6 +11443,9 @@ Example: given a notice format "Following %@" and empty site name, this will be /* View title for Help & Support page. */ "support.title" = "עזרה"; +/* Title for placeholder in Tenor picker */ +"tenor.welcomeMessage" = "אפשר לחפש קובצי GIF ולהוסיף אותם לספריית המדיה שלך!"; + /* Header of delete screen section listing things that will be deleted. */ "these items will be deleted:" = "פריטים אלה יימחקו:"; @@ -11343,9 +11461,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Displayed in the confirmation alert when marking unread notifications as read. */ "unread" = "לא נקרא"; -/* Label displayed on video media items. */ -"video" = "וידאו"; - /* Portion of a message for Jetpack users that have multisite WP installation, thus Restore is not available. This part is a link, colored with a different color. */ "visit our documentation page" = "היכנס לעמוד המסמכים שלנו"; diff --git a/WordPress/Resources/hr.lproj/Localizable.strings b/WordPress/Resources/hr.lproj/Localizable.strings index 1b6ab22258f0..7a0b4f7cb2ba 100644 --- a/WordPress/Resources/hr.lproj/Localizable.strings +++ b/WordPress/Resources/hr.lproj/Localizable.strings @@ -23,10 +23,6 @@ /* Lets a user know that a local draft does not have a title. */ "(no title)" = "(bez naslova)"; -/* Accessibility label for add button to add items to the user's media library - Remove asset from media picker list */ -"Add" = "Dodaj"; - /* The title on the add category screen */ "Add a Category" = "Dodaj Kategoriju"; @@ -122,10 +118,7 @@ Cancels an Action Cancels an alert. Cancels the mark all as read action. - Dismiss the post action sheet Editing GIF alert cancel action button. - Label for a cancel button - Label for the auto-upload cancelation button in the post list. Tapping will prevent the app from auto-uploading the post. Menus cancel button for deleting a menu. Menus cancel button within text bar while editing items. Menus: Cancel button title for canceling an edited menu item. @@ -189,10 +182,6 @@ /* No comment provided by engineer. */ "Connecting to WordPress.com" = "Spajanje sa WordPress.com"; -/* Copy the post url and paste anywhere in phone - Label for page copy link. Tapping copy the url of page */ -"Copy Link" = "Kopiraj Poveznicu"; - /* Period Stats 'Countries' header */ "Countries" = "Države"; @@ -223,7 +212,6 @@ /* Delete Delete button title for the warning shown to the user when he refuses to re-login when the authToken is missing. Title for button that permanently deletes a media item (photo / video) - Title for button that permanently deletes one or more media items (photos / videos) Title of a delete button Title of the trash confirmation alert. */ "Delete" = "Obriši"; @@ -254,7 +242,6 @@ /* Accessibility label for button to dismiss a bottom sheet Accessibility label for the transparent space above the login dialog which acts as a button to dismiss the dialog. Accessibility label for the transparent space above the signup dialog which acts as a button to dismiss the dialog. - Action to show on alert when view asset fails. Dismiss a view. Verb */ "Dismiss" = "Odbaci"; @@ -282,13 +269,9 @@ /* Name for the status of a draft post. */ "Draft" = "Skica"; -/* Accessibility label for edit button to enable multi selection mode in the user's media library - Edit the post. - Editing GIF alert default action button. +/* Editing GIF alert default action button. Edits a Comment Edits the comment - Label for a button that opens the Edit Page view controller - Label for the edit post button. Tapping displays the editor. User action to edit media details. Verb, edit a comment */ "Edit" = "Uredi"; @@ -453,7 +436,6 @@ "Manage" = "Upravljanje"; /* Accessibility label for the Me button in My Site. - Label for the post author filter. This filter shows posts only authored by the current user. Me page title Noun. Title. Links to the Me screen. The accessibility value of the me tab. @@ -461,17 +443,11 @@ "Me" = "Ja"; /* Noun. Title. Links to the blog's Media library. - Tab bar title for the Media tab in Media Picker The menu item to select during a guided tour. - Title for Media Library section of the app. Title for the media section in site settings screen Title label for the media settings section in the app settings */ "Media" = "Media"; -/* Title for alert when a generic error happened when loading media - Title for alert when access to the media library is not granted by the user */ -"Media Library" = "Zbirka medija"; - /* Medium image size. Should be the same as in core WP. */ "Medium" = "Srednje"; @@ -532,7 +508,6 @@ Alert dismissal title Button title. Acknowledges a prompt. Button title. An acknowledgement of the message displayed in a prompt. - Confirmation of action Default action Dismisses the alert Marks all notifications as read. @@ -595,16 +570,12 @@ Title of pending Comments filter. */ "Pending" = "Na čekanju"; -/* Name for the status of a post pending review. - Title of the Pending Review Badge */ +/* Name for the status of a post pending review. */ "Pending review" = "Čeka recenziju"; /* Label for date periods. */ "Period" = "Vremensko razdoblje"; -/* Tab bar title for the Photos tab in Media Picker */ -"Photos" = "Slike"; - /* Popup message to ask for user credentials (fields shown below). */ "Please enter your credentials" = "Molimo unesite vaše podatke"; @@ -645,8 +616,7 @@ "Privacy Policy" = "Pravila privatnosti"; /* Name for the status of a post that is marked private. - Privacy setting for posts set to 'Private'. Should be the same as in core WP. - Title of the Private Badge */ + Privacy setting for posts set to 'Private'. Should be the same as in core WP. */ "Private" = "Privatno"; /* Privacy setting for posts set to 'Public' (default). Should be the same as in core WP. */ @@ -660,14 +630,12 @@ Title for the publish settings view */ "Publish" = "Objavi"; -/* A short phrase indicating a post is due to be immedately published. - Label for a button that moves a page to the published folder, publishing with the current date/time. */ +/* A short phrase indicating a post is due to be immedately published. */ "Publish Immediately" = "Objavi Odmah"; /* Name for the status of a published post. Period Stats 'Published' header - Title of the published filter. This filter shows a list of posts that the user has published. - Title of the published header in search list. */ + Title of the published filter. This filter shows a list of posts that the user has published. */ "Published" = "Objavljeno"; /* Published on [date] */ @@ -687,8 +655,7 @@ The loading view button title displayed when an error occurred */ "Refresh" = "Osvježi"; -/* Add asset to media picker list - Alert button to confirm a plugin to be removed +/* Alert button to confirm a plugin to be removed Button label when removing a blog Label for the Remove Feature Image icon. Tapping will show a confirmation screen for removing the feature image from the post. Remove Action @@ -713,12 +680,9 @@ Button title, displayed when media has failed to upload. Allows the user to try the upload again. Button title. Retries uploading a post. If a user taps the button with this label, the action that evinced this error view will be retried. - Label for a button that attempts to re-upload a page that previously failed to upload. - Label for the retry post upload button. Tapping attempts to upload the post again. Opens the media library . Retries the upload of a user's post. Retry updating User's Role - Retry uploading the post. Retry. Action Retry. Verb – retry a failed media upload. The Jetpack view button title used when an error occurred @@ -746,11 +710,7 @@ /* Button shown if there are unsaved changes and the author is trying to move away from the post. */ "Save Draft" = "Snimi Skicu"; -/* Text displayed in HUD when a media item's metadata (title, etc) is saved successfully. */ -"Saved!" = "Spremljeno!"; - /* Menus save button title while it is saving a Menu. - Text displayed in HUD while a media item's metadata (title, etc) is being saved. Text displayed in HUD while a post is being saved as a draft. */ "Saving..." = "Snimam..."; @@ -791,7 +751,6 @@ /* Accessibility label for share buttons in nav bars Button label to share a post Button label to share a web page - Share the post. Shares the comment URL Spoken accessibility label Title for a button that allows the user to share their answer to the prompt. @@ -824,8 +783,7 @@ Title of spam Comments filter. */ "Spam" = "Spam"; -/* Label for post stats option. Tapping displays statistics for a post. - Noun. Abbreviation of Statistics. Name of the Stats feature +/* Noun. Abbreviation of Statistics. Name of the Stats feature Noun. Abbv. of Statistics. Links to a blog's Stats screen. Stats 3D Touch Shortcut Stats window title @@ -990,8 +948,6 @@ "Version " = "Inačica:"; /* Button title. Displays a summary / sharing screen for a specific post. - Label for a button that opens the page when tapped. - Label for the view post button. Tapping displays the post as it appears on the web. Opens the post epilogue screen to allow sharing / viewing of a post. Theme View action title Verb. The screen title shown when viewing a post inside the app. */ @@ -1083,9 +1039,6 @@ /* translators: displays audio file extension. e.g. MP3 audio file */ "audio file" = "Zvučna datoteka"; -/* Label displayed on image media items. */ -"image" = "slika"; - /* This text is used when the user is configuring the iOS widget to suggest them to select the site to configure the widget for */ "ios-widget.gpCwrM" = "Select Site"; diff --git a/WordPress/Resources/hu.lproj/Localizable.strings b/WordPress/Resources/hu.lproj/Localizable.strings index 67ca1780e679..60cb0ac66986 100644 --- a/WordPress/Resources/hu.lproj/Localizable.strings +++ b/WordPress/Resources/hu.lproj/Localizable.strings @@ -12,10 +12,6 @@ /* No comment provided by engineer. */ "Activity Logs" = "Tevékenységi napló"; -/* Accessibility label for add button to add items to the user's media library - Remove asset from media picker list */ -"Add" = "Hozzáadás"; - /* Title for the advanced section in site settings screen */ "Advanced" = "Haladó"; @@ -79,10 +75,7 @@ Cancels an Action Cancels an alert. Cancels the mark all as read action. - Dismiss the post action sheet Editing GIF alert cancel action button. - Label for a cancel button - Label for the auto-upload cancelation button in the post list. Tapping will prevent the app from auto-uploading the post. Menus cancel button for deleting a menu. Menus cancel button within text bar while editing items. Menus: Cancel button title for canceling an edited menu item. @@ -149,10 +142,6 @@ /* No comment provided by engineer. */ "Connecting to WordPress.com" = "Kapcsolódás a WordPress.com-hoz"; -/* Copy the post url and paste anywhere in phone - Label for page copy link. Tapping copy the url of page */ -"Copy Link" = "Hivatkozás másolás"; - /* No comment provided by engineer. */ "Couldn't Connect" = "Nem sikerült kapcsolódni"; @@ -179,7 +168,6 @@ /* Delete Delete button title for the warning shown to the user when he refuses to re-login when the authToken is missing. Title for button that permanently deletes a media item (photo / video) - Title for button that permanently deletes one or more media items (photos / videos) Title of a delete button Title of the trash confirmation alert. */ "Delete" = "Törlés"; @@ -198,7 +186,6 @@ /* Accessibility label for button to dismiss a bottom sheet Accessibility label for the transparent space above the login dialog which acts as a button to dismiss the dialog. Accessibility label for the transparent space above the signup dialog which acts as a button to dismiss the dialog. - Action to show on alert when view asset fails. Dismiss a view. Verb */ "Dismiss" = "Elutasítás"; @@ -223,13 +210,9 @@ /* Name for the status of a draft post. */ "Draft" = "Vázlat"; -/* Accessibility label for edit button to enable multi selection mode in the user's media library - Edit the post. - Editing GIF alert default action button. +/* Editing GIF alert default action button. Edits a Comment Edits the comment - Label for a button that opens the Edit Page view controller - Label for the edit post button. Tapping displays the editor. User action to edit media details. Verb, edit a comment */ "Edit" = "Szerkesztés"; @@ -417,7 +400,6 @@ "Manage" = "Kezelés"; /* Accessibility label for the Me button in My Site. - Label for the post author filter. This filter shows posts only authored by the current user. Me page title Noun. Title. Links to the Me screen. The accessibility value of the me tab. @@ -425,9 +407,7 @@ "Me" = "Én"; /* Noun. Title. Links to the blog's Media library. - Tab bar title for the Media tab in Media Picker The menu item to select during a guided tour. - Title for Media Library section of the app. Title for the media section in site settings screen Title label for the media settings section in the app settings */ "Media" = "Média"; @@ -494,7 +474,6 @@ Alert dismissal title Button title. Acknowledges a prompt. Button title. An acknowledgement of the message displayed in a prompt. - Confirmation of action Default action Dismisses the alert Marks all notifications as read. @@ -554,8 +533,7 @@ Title of pending Comments filter. */ "Pending" = "Függőben Lévő"; -/* Name for the status of a post pending review. - Title of the Pending Review Badge */ +/* Name for the status of a post pending review. */ "Pending review" = "Ellenőrzésre vár"; /* No comment provided by engineer. */ @@ -606,8 +584,7 @@ "Privacy Policy" = "Adatvédelmi irányelvek"; /* Name for the status of a post that is marked private. - Privacy setting for posts set to 'Private'. Should be the same as in core WP. - Title of the Private Badge */ + Privacy setting for posts set to 'Private'. Should be the same as in core WP. */ "Private" = "Magánjellegű"; /* Privacy setting for posts set to 'Public' (default). Should be the same as in core WP. */ @@ -621,14 +598,12 @@ Title for the publish settings view */ "Publish" = "Közzététel"; -/* A short phrase indicating a post is due to be immedately published. - Label for a button that moves a page to the published folder, publishing with the current date/time. */ +/* A short phrase indicating a post is due to be immedately published. */ "Publish Immediately" = "Azonnali közzététel"; /* Name for the status of a published post. Period Stats 'Published' header - Title of the published filter. This filter shows a list of posts that the user has published. - Title of the published header in search list. */ + Title of the published filter. This filter shows a list of posts that the user has published. */ "Published" = "Közzétéve"; /* Published on [date] */ @@ -653,8 +628,7 @@ /* Period Stats 'Referrers' header */ "Referrers" = "Hivatkozó oldalak"; -/* Add asset to media picker list - Alert button to confirm a plugin to be removed +/* Alert button to confirm a plugin to be removed Button label when removing a blog Label for the Remove Feature Image icon. Tapping will show a confirmation screen for removing the feature image from the post. Remove Action @@ -682,12 +656,9 @@ Button title, displayed when media has failed to upload. Allows the user to try the upload again. Button title. Retries uploading a post. If a user taps the button with this label, the action that evinced this error view will be retried. - Label for a button that attempts to re-upload a page that previously failed to upload. - Label for the retry post upload button. Tapping attempts to upload the post again. Opens the media library . Retries the upload of a user's post. Retry updating User's Role - Retry uploading the post. Retry. Action Retry. Verb – retry a failed media upload. The Jetpack view button title used when an error occurred @@ -713,7 +684,6 @@ "Save Draft" = "Vázlat mentése"; /* Menus save button title while it is saving a Menu. - Text displayed in HUD while a media item's metadata (title, etc) is being saved. Text displayed in HUD while a post is being saved as a draft. */ "Saving..." = "Mentés..."; @@ -742,7 +712,6 @@ /* Accessibility label for share buttons in nav bars Button label to share a post Button label to share a web page - Share the post. Shares the comment URL Spoken accessibility label Title for a button that allows the user to share their answer to the prompt. @@ -824,8 +793,7 @@ Title of spam Comments filter. */ "Spam" = "Spam"; -/* Label for post stats option. Tapping displays statistics for a post. - Noun. Abbreviation of Statistics. Name of the Stats feature +/* Noun. Abbreviation of Statistics. Name of the Stats feature Noun. Abbv. of Statistics. Links to a blog's Stats screen. Stats 3D Touch Shortcut Stats window title @@ -873,7 +841,7 @@ /* No comment provided by engineer. */ "The site at %@ uses WordPress %@. We recommend to update to the latest version, or at least %@" = "A %1$@ webhelyen WordPress %2$@ van használatban. Azt ajánljuk, hogy frissítsünk a legutóbbi verzióra, vagy legalább erre: %3$@"; -/* Error message informing a user about an invalid password. */ +/* No comment provided by engineer. */ "The username or password stored in the app may be out of date. Please re-enter your password in the settings and try again." = "Az alkalmazásban tárolt felhasználónév és a jelszó lehetséges, hogy elavult. Adjuk meg újra a jelszót a beállításokban, és próbáljuk újra."; /* Accessibility label for web page preview title @@ -960,8 +928,6 @@ "Version " = "Verzió"; /* Button title. Displays a summary / sharing screen for a specific post. - Label for a button that opens the page when tapped. - Label for the view post button. Tapping displays the post as it appears on the web. Opens the post epilogue screen to allow sharing / viewing of a post. Theme View action title Verb. The screen title shown when viewing a post inside the app. */ @@ -1026,9 +992,6 @@ /* (placeholder) Help the user enter a URL into the field */ "http:\/\/my-site-address (URL)" = "http:\/\/azoldalamcíme (URL)"; -/* Label displayed on image media items. */ -"image" = "kép"; - /* This text is used when the user is configuring the iOS widget to suggest them to select the site to configure the widget for */ "ios-widget.gpCwrM" = "Select Site"; diff --git a/WordPress/Resources/id.lproj/Localizable.strings b/WordPress/Resources/id.lproj/Localizable.strings index 5c5d62a22939..31c94dac31a8 100644 --- a/WordPress/Resources/id.lproj/Localizable.strings +++ b/WordPress/Resources/id.lproj/Localizable.strings @@ -1,4 +1,4 @@ -/* Translation-Revision-Date: 2023-10-18 12:54:08+0000 */ +/* Translation-Revision-Date: 2024-01-04 09:54:09+0000 */ /* Plural-Forms: nplurals=2; plural=n > 1; */ /* Generator: GlotPress/4.0.0-alpha.11 */ /* Language: id */ @@ -152,9 +152,6 @@ /* Difference label for Period Overview stat, indicating change from previous period. Ex: +99.9K (5%) */ "%@%@ (%@%%)" = "%1$@%2$@ (%3$@%%)"; -/* Accessibility label for a post in the post list. The parameters are the title, and date respectively. For example, \"Let it Go, 1 hour ago.\" */ -"%@, %@." = "%1$@, %2$@."; - /* Accessibility value for a Stats' Posting Activity Month if the user selected a day with posts. The first parameter is day (e.g. November 2019). The second parameter is the number of posts. */ "%@. %d posts." = "%1$@. %2$d pos."; @@ -210,9 +207,6 @@ /* Age between dates over one year. */ "%d years" = "%d tahun"; -/* Max image size in pixels (e.g. 300x300px) */ -"%dx%dpx" = "%1$dx%2$dpx"; - /* One menu area available in the theme */ "%i menu area in this theme" = "%i area menu di tema ini"; @@ -271,6 +265,9 @@ translators: Block name. %s: The localized block name */ /* translators: %s: social link name e.g: \"Instagram\". */ "%s social icon" = "Ikon media sosial %s"; +/* translators: displayed right after the classic block is converted to blocks. %s: The localized classic block name */ +"'%s' block converted to blocks" = "Blok '%s' dikonversi menjadi blok"; + /* translators: Missing block alert title. %s: The localized block name */ "'%s' is not fully-supported" = "'%s' tidak sepenuhnya didukung"; @@ -478,13 +475,6 @@ translators: Block name. %s: The localized block name */ /* Label for the Activity Type filter button when there are more than 1 activity type selected */ "Activity Type (%1$d)" = "Jenis Aktivitas (%1$d)"; -/* Accessibility label for add button to add items to the user's media library - Remove asset from media picker list */ -"Add" = "Tambah"; - -/* Action for Media Picker to indicate selection of media. The argument in the string represents the number of elements (as numeric digits) selected */ -"Add %@" = "Tambahkan %@"; - /* No comment provided by engineer. */ "Add Block After" = "Tambahkan Blok Setelah"; @@ -578,9 +568,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Add menu item to children" = "Tambahkan item menu ke turunan"; -/* Accessibility hint for add button to add items to the user's media library */ -"Add new media" = "Tambah media baru"; - /* Screen reader text for Menus button that adds a new menu to a site. */ "Add new menu" = "Tambahkan menu baru"; @@ -643,9 +630,6 @@ translators: Block name. %s: The localized block name */ /* Option to select the Airmail app when logging in with magic links */ "Airmail" = "Airmail"; -/* Description of albums in the photo libraries */ -"Albums" = "Album"; - /* Image alignment option title. Title of the screen for choosing an image's alignment. */ "Alignment" = "Perataan"; @@ -659,6 +643,9 @@ translators: Block name. %s: The localized block name */ Title of the drafts filter. This filter shows a list of draft posts. */ "All" = "Semua"; +/* Information about redeeming domain credit on site dashboard. */ +"All WordPress.com annual plans include a custom domain name. Register your free domain now." = "Semua paket tahunan WordPress.com menyertakan nama domain kustom. Daftarkan domain gratis Anda sekarang."; + /* Footer of the free domain registration section for a paid plan. Information about redeeming domain credit on site dashboard. */ "All WordPress.com plans include a custom domain name. Register your free premium domain now." = "Semua paket WordPress.com menyertakan nama domain kustom. Daftarkan domain premium gratis Anda sekarang."; @@ -724,10 +711,13 @@ translators: Block name. %s: The localized block name */ "Alt Text" = "Teks Alt"; /* No comment provided by engineer. */ -"Alternatively, you can detach and edit these blocks separately by tapping “Detach patterns”." = "Atau, blok-blok ini bisa dipisahkan lalu disunting secara tersendiri dengan mengetuk \"Pisahkan pola\"."; +"Alternatively, you can convert the content to blocks." = "Anda dapat mengonversi konten menjadi blok."; /* No comment provided by engineer. */ -"Alternatively, you can detach and edit this block separately by tapping “Detach pattern”." = "Atau, blok ini bisa dipisahkan lalu disunting secara tersendiri dengan mengetuk \"Pisahkan pola\"."; +"Alternatively, you can detach and edit this block separately by tapping “Detach”." = "Atau, blok ini bisa dipisahkan lalu disunting secara tersendiri dengan mengetuk \"Pisahkan\"."; + +/* translators: Alternative option included in a warning related to having blocks deeply nested. */ +"Alternatively, you can flatten the content by ungrouping the block." = "Atau, Anda dapat meratakan konten dengan membatalkan pengelompokan blok."; /* Instruction text to explain to help users type their password instead of using magic link login option. */ "Alternatively, you may enter the password for this account." = "Atau, Anda bisa memasukkan kata sandi untuk akun ini."; @@ -876,15 +866,9 @@ translators: Block name. %s: The localized block name */ /* Message prompting the user to confirm that they want to disconnect Jetpack from the site. */ "Are you sure you want to disconnect Jetpack from the site?" = "Apakah Anda yakin ingin memutuskan sambungan Jetpack dari situs?"; -/* Message prompting the user to confirm that they want to permanently delete a group of media items. */ -"Are you sure you want to permanently delete these items?" = "Anda yakin ingin menghapus item ini secara permanen?"; - /* Message prompting the user to confirm that they want to permanently delete a media item. Should match Calypso. */ "Are you sure you want to permanently delete this item?" = "Anda yakin ingin menghapus item ini secara permanen?"; -/* Message of the confirmation alert when deleting a page from the trash. */ -"Are you sure you want to permanently delete this page?" = "Anda yakin ingin menghapus halaman ini secara permanen?"; - /* Message of the confirmation alert when deleting a post from the trash. */ "Are you sure you want to permanently delete this post?" = "Anda yakin ingin menghapus pos ini secara permanen?"; @@ -916,9 +900,6 @@ translators: Block name. %s: The localized block name */ /* Title of message shown when user taps submit for review. */ "Are you sure you want to submit for review?" = "Anda yakin ingin mengirimnya untuk diulas?"; -/* Message of the trash page confirmation alert. */ -"Are you sure you want to trash this page?" = "Apakah Anda ingin membuang halaman ini?"; - /* Message of the trash confirmation alert. */ "Are you sure you want to trash this post?" = "Anda yakin ingin membuang pos ini?"; @@ -959,9 +940,6 @@ translators: Block name. %s: The localized block name */ /* translators: accessibility text. Empty Audio caption. */ "Audio caption. Empty" = "Keterangan audio. Kosong"; -/* Accessibility label for audio items in the media collection view. The parameter is the creation date of the audio. */ -"Audio, %@" = "Audio, %@"; - /* No comment provided by engineer. */ "Authenticating" = "Mengotentikasi"; @@ -1172,10 +1150,7 @@ translators: Block name. %s: The localized block name */ "Blocks menu" = "Menu blok"; /* translators: Warning related to having blocks deeply nested. %d: The deepest nesting level. */ -"Blocks nested deeper than %d levels may not render properly in the mobile editor. For this reason, we recommend flattening the content by ungrouping the block or editing the block using the web editor." = "Blok bertingkat yang kedalamannya melebihi %d level mungkin tidak dapat ditampilkan dengan benar di editor seluler. Karenanya, kami menyarankan Anda untuk meratakan konten dengan membatalkan pengelompokan blok atau mengedit blok melalui editor web."; - -/* translators: Warning related to having blocks deeply nested. %d: The deepest nesting level. */ -"Blocks nested deeper than %d levels may not render properly in the mobile editor. For this reason, we recommend flattening the content by ungrouping the block or editing the block using your web browser." = "Blok bertingkat yang kedalamannya melebihi %d level mungkin tidak dapat ditampilkan dengan benar di editor seluler. Karenanya, kami menyarankan Anda untuk meratakan konten dengan membatalkan pengelompokan blok atau mengedit blok melalui browser web."; +"Blocks nested deeper than %d levels may not render properly in the mobile editor." = "Blok bertingkat yang kedalamannya melebihi %d level mungkin tidak dapat ditampilkan dengan benar di editor seluler."; /* Title of a button that displays the WordPress.com blog */ "Blog" = "Blog"; @@ -1251,10 +1226,7 @@ translators: Block name. %s: The localized block name */ "Button position" = "Posisi tombol"; /* Label for the post author in the post detail. */ -"By " = "Oleh"; - -/* Accessibility label for the post author in the post list. The parameter is the author name. For example, \"By Elsa.\" */ -"By %@." = "Oleh %@."; +"By " = "Oleh "; /* Legal disclaimer for logging in. The underscores _..._ denote underline. */ "By continuing, you agree to our _Terms of Service_." = "Dengan melanjutkan, Anda menyetujui _Ketentuan Layanan_ kami."; @@ -1275,8 +1247,7 @@ translators: Block name. %s: The localized block name */ /* Label for size of media while it's being calculated. */ "Calculating..." = "Menghitung..."; -/* Accessibility label for taking an image or video with the camera on formatting toolbar. - Accessibility label for the camera tile in the collection view */ +/* Accessibility label for taking an image or video with the camera on formatting toolbar. */ "Camera" = "Kamera"; /* Title of an alert informing the user the camera permission for the app is disabled and its needed to proceed */ @@ -1327,10 +1298,7 @@ translators: Block name. %s: The localized block name */ Cancels an Action Cancels an alert. Cancels the mark all as read action. - Dismiss the post action sheet Editing GIF alert cancel action button. - Label for a cancel button - Label for the auto-upload cancelation button in the post list. Tapping will prevent the app from auto-uploading the post. Menus cancel button for deleting a menu. Menus cancel button within text bar while editing items. Menus: Cancel button title for canceling an edited menu item. @@ -1347,10 +1315,6 @@ translators: Block name. %s: The localized block name */ Verb. Title for Jetpack Restore cancel button. */ "Cancel" = "Batal"; -/* Label for the Post List option that cancels automatic uploading of a post. - Media Library option to cancel an in-progress or failed upload. */ -"Cancel Upload" = "Batalkan Unggahan"; - /* Menus alert button title to cancel discarding changes and not createa a new menu. Menus alert button title to cancel discarding changes and not select a new menu Menus alert button title to cancel discarding changes and not select a new menu location */ @@ -1409,9 +1373,6 @@ translators: Block name. %s: The localized block name */ Main title */ "Change Password" = "Ganti Kata Sandi"; -/* Title of button that takes user to the system Settings section for the app */ -"Change Settings" = "Ubah Pengaturan"; - /* Change Username title. */ "Change Username" = "Ubah Nama Pengguna"; @@ -1551,9 +1512,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Choose file" = "Pilih berkas"; -/* Menu option for selecting media from the device's photo library. */ -"Choose from My Device" = "Pilih dari Perangkat Saya"; - /* Explanatory text for Homepage Settings homepage type selection. */ "Choose from a homepage that displays your latest posts (classic blog) or a fixed \/ static page." = "Pilih dari halaman beranda yang menampilkan pos terbaru Anda (blog klasik) atau halaman tetap\/statis."; @@ -1754,9 +1712,6 @@ translators: Block name. %s: The localized block name */ /* Community & Non-Profit site intent topic */ "Community & Non-Profit" = "Komunitas & Non-Profit"; -/* Accessibility indication that the current Post List style is currently Compact. */ -"Compact" = "Praktis"; - /* The action is completed */ "Completed" = "Selesai"; @@ -1942,10 +1897,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Copied block" = "Blok disalin"; -/* Copy the post url and paste anywhere in phone - Label for page copy link. Tapping copy the url of page */ -"Copy Link" = "Salin Taut"; - /* Copy link to oomment button title */ "Copy Link to Comment" = "Salin Tautan ke Komentar"; @@ -2054,9 +2005,6 @@ translators: Block name. %s: The localized block name */ /* Error title displayed when unable to close user account. */ "Couldn’t close account automatically" = "Tidak dapat menutup akun secara otomatis"; -/* Message to show while media data source is finding the number of items available. */ -"Counting media items..." = "Menghitung item media..."; - /* Period Stats 'Countries' header */ "Countries" = "Negara"; @@ -2307,7 +2255,6 @@ translators: Block name. %s: The localized block name */ /* Delete Delete button title for the warning shown to the user when he refuses to re-login when the authToken is missing. Title for button that permanently deletes a media item (photo / video) - Title for button that permanently deletes one or more media items (photos / videos) Title of a delete button Title of the trash confirmation alert. */ "Delete" = "Hapus"; @@ -2315,15 +2262,11 @@ translators: Block name. %s: The localized block name */ /* Menus confirmation button for deleting a menu. */ "Delete Menu" = "Hapus Menu"; -/* Delete option in the confirmation alert when deleting a page from the trash. - Delete option in the confirmation alert when deleting a post from the trash. - Label for a button permanently deletes a page. - Label for the delete post option. Tapping permanently deletes a post. +/* Delete option in the confirmation alert when deleting a post from the trash. Title for button on the comment details page that deletes the comment when tapped. */ "Delete Permanently" = "Hapus Permanen"; -/* Title of the confirmation alert when deleting a page from the trash. - Title of the confirmation alert when deleting a post from the trash. */ +/* Title of the confirmation alert when deleting a post from the trash. */ "Delete Permanently?" = "Hapus Permanen?"; /* Button label for deleting the current site @@ -2442,7 +2385,6 @@ translators: Block name. %s: The localized block name */ /* Accessibility label for button to dismiss a bottom sheet Accessibility label for the transparent space above the login dialog which acts as a button to dismiss the dialog. Accessibility label for the transparent space above the signup dialog which acts as a button to dismiss the dialog. - Action to show on alert when view asset fails. Dismiss a view. Verb */ "Dismiss" = "Hentikan"; @@ -2460,12 +2402,6 @@ translators: Block name. %s: The localized block name */ User's Display Name */ "Display Name" = "Nama Tampilan"; -/* Accessibility label for other media items in the media collection view. The parameter is the creation date of the document. */ -"Document, %@" = "Dokumen, %@"; - -/* Accessibility label for other media items in the media collection view. The parameter is the filename file. */ -"Document: %@" = "Dokumen: %@"; - /* Message shown when all tours have been completed */ "Doesn't it feel good to cross things off a list?" = "Lega rasanya saat satu pekerjaan terselesaikan!"; @@ -2626,8 +2562,7 @@ translators: Block name. %s: The localized block name */ /* Description of a Quick Start Tour */ "Draft and publish a post." = "Buat draf dan publikasikan artikel."; -/* Title of the drafts filter. This filter shows a list of draft posts. - Title of the drafts header in search list. */ +/* Title of the drafts filter. This filter shows a list of draft posts. */ "Drafts" = "Konsep"; /* No comment provided by engineer. */ @@ -2639,10 +2574,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Drag to adjust focal point" = "Seret untuk menyetel titik fokus"; -/* Label for page duplicate option. Tapping creates a copy of the page. - Label for post duplicate option. Tapping creates a copy of the post. */ -"Duplicate" = "Duplikat"; - /* No comment provided by engineer. */ "Duplicate block" = "Duplikasi blok"; @@ -2655,13 +2586,9 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Each block has its own settings. To find them, tap on a block. Its settings will appear on the toolbar at the bottom of the screen." = "Setiap blok memiliki pengaturannya sendiri. Untuk menemukannya, ketuk pada blok. Pengaturannya akan muncul di toolbar di bagian bawah layar."; -/* Accessibility label for edit button to enable multi selection mode in the user's media library - Edit the post. - Editing GIF alert default action button. +/* Editing GIF alert default action button. Edits a Comment Edits the comment - Label for a button that opens the Edit Page view controller - Label for the edit post button. Tapping displays the editor. User action to edit media details. Verb, edit a comment */ "Edit" = "Sunting"; @@ -2669,9 +2596,6 @@ translators: Block name. %s: The localized block name */ /* Title for the edit more button section */ "Edit \"More\" button" = "Sunting tombol \"Lagi\""; -/* Button that displays the media editor to the user */ -"Edit %@" = "Sunting %@"; - /* Blocklist Keyword Edition Title */ "Edit Blocklist Word" = "Edit Daftar Blokir Kata"; @@ -2864,9 +2788,6 @@ translators: Block name. %s: The localized block name */ /* Secondary message shown when there are no domains that match the user entered text. */ "Enter different words above and we'll look for an address that matches it." = "Masukkan kata yang berbeda dengan yang di atas dan kami akan mencari alamat yang cocok dengan kata tersebut."; -/* Accessibility hint for edit button to enable multi selection mode in the user's media library */ -"Enter edit mode to enable multi select to delete" = "Aktifkan mode edit untuk mengaktifkan fitur hapus banyak objek yang dipilih"; - /* (placeholder) Help enter WordPress password Placeholder of a field to type a password to protect the post. */ "Enter password" = "Masukkan kata sandi"; @@ -3022,9 +2943,6 @@ translators: Block name. %s: The localized block name */ /* Short title telling the user they will receive a blogging reminder every day of the week. */ "Every day at %@" = "Setiap hari pada %@"; -/* Label for the post author filter. This filter shows posts for all users on the blog. */ -"Everyone" = "Setiap Orang"; - /* Example story title description */ "Example story title" = "Contoh judul cerita"; @@ -3034,9 +2952,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Excerpt length (words)" = "Panjang kutipan (kata)"; -/* Accessibility label for a post's excerpt in the post list. The parameter is the post excerpt. For example, \"Excerpt. This is the first paragraph.\" */ -"Excerpt. %@." = "Kutipan. %@."; - /* Should be the same as the text displayed if the user clicks the (i) in Calypso. */ "Excerpts are optional hand-crafted summaries of your content." = "Kutipan merupakan ringkasan pilihan opsional dari konten Anda."; @@ -3046,8 +2961,7 @@ translators: Block name. %s: The localized block name */ /* Accessibility Label for the exit full screen button on the full screen comment reply mode */ "Exit Full Screen" = "Keluar dari Layar Penuh"; -/* Accessibility indication that the current Post List style is currently Expanded. - Screen reader text to represent the expanded state of a UI control */ +/* Screen reader text to represent the expanded state of a UI control */ "Expanded" = "Diperluas"; /* Accessibility hint */ @@ -3097,9 +3011,6 @@ translators: Block name. %s: The localized block name */ The action failed */ "Failed" = "Gagal"; -/* Error title when picked media cannot be imported into stories. */ -"Failed Media Export" = "Ekspor Media Gagal"; - /* Message for mark all as read success notice */ "Failed marking Notifications as read" = "Gagal menandai Pemberitahuan sebagai sudah dibaca"; @@ -3301,6 +3212,12 @@ translators: Block name. %s: The localized block name */ /* An example tag used in the login prologue screens. */ "Football" = "Sepakbola"; +/* translators: Recommendation included in a warning related to having blocks deeply nested. */ +"For this reason, we recommend editing the block using the web editor." = "Karenanya, kami menyarankan Anda untuk mengedit blok melalui editor web."; + +/* translators: Recommendation included in a warning related to having blocks deeply nested. */ +"For this reason, we recommend editing the block using your web browser." = "Karenanya, kami menyarankan Anda untuk mengedit blok melalui browser web."; + /* Register Domain - Domain contact information section header description */ "For your convenience, we have pre-filled your WordPress.com contact information. Please review to be sure it’s the correct information you want to use for this domain." = "Demi kenyamanan Anda, kami telah mengisi informasi kontak WordPress.com Anda terlebih dahulu. Harap tinjau untuk memastikan informasi yang ingin Anda gunakan untuk domain ini sudah benar."; @@ -3618,8 +3535,7 @@ translators: Block name. %s: The localized block name */ "Home" = "Beranda"; /* Title for setting which shows the current page assigned as a site's homepage - Title for the homepage section in site settings screen - Title of the Homepage Badge */ + Title for the homepage section in site settings screen */ "Homepage" = "Beranda"; /* Label for Homepage Settings site settings section @@ -3716,9 +3632,6 @@ translators: Block name. %s: The localized block name */ /* Hint for image title on image settings. */ "Image title" = "Judul gambar"; -/* Accessibility label for image thumbnails in the media collection view. The parameter is the creation date of the image. */ -"Image, %@" = "Gambar, %@"; - /* Undated post time label */ "Immediately" = "Segera"; @@ -4204,9 +4117,6 @@ translators: Block name. %s: The localized block name */ Settings: Comments Approval settings */ "Links in comments" = "Tautan dalam komentar"; -/* The accessibility label for the list style button in the Post List. */ -"List style" = "Buat daftar gaya"; - /* Title of the screen that load selected the revisions. */ "Load" = "Memuat"; @@ -4222,18 +4132,12 @@ translators: Block name. %s: The localized block name */ /* Text displayed while loading the activity feed for a site */ "Loading Backups..." = "Memuat Cadangan…"; -/* Phrase to show when the user has searched for GIFs and they are being loaded. */ -"Loading GIFs..." = "Memuat GIF..."; - /* Menus label text displayed while menus are loading */ "Loading Menus..." = "Memuat Menu..."; /* Text displayed while loading site People. */ "Loading People..." = "Memuat Orang-orang..."; -/* Phrase to show when the user has searched for images and they are being loaded. */ -"Loading Photos..." = "Memuat Foto..."; - /* Text displayed while loading plans details */ "Loading Plan..." = "Memuat Paket..."; @@ -4294,8 +4198,7 @@ translators: Block name. %s: The localized block name */ /* Local Services site intent topic */ "Local Services" = "Layanan lokal"; -/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. - Title of the Local Changes Badge */ +/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. */ "Local changes" = "Perubahan lokal"; /* Title for alert when a generic error happened when trying to find the location of the device */ @@ -4459,7 +4362,6 @@ translators: Block name. %s: The localized block name */ "Max Video Upload Size" = "Ukuran Unggahan Video Maksimal"; /* Accessibility label for the Me button in My Site. - Label for the post author filter. This filter shows posts only authored by the current user. Me page title Noun. Title. Links to the Me screen. The accessibility value of the me tab. @@ -4467,9 +4369,7 @@ translators: Block name. %s: The localized block name */ "Me" = "Saya"; /* Noun. Title. Links to the blog's Media library. - Tab bar title for the Media tab in Media Picker The menu item to select during a guided tour. - Title for Media Library section of the app. Title for the media section in site settings screen Title label for the media settings section in the app settings */ "Media" = "Media"; @@ -4481,13 +4381,6 @@ translators: Block name. %s: The localized block name */ /* Label for size of media cache in the app. */ "Media Cache Size" = "Ukuran Cache Media"; -/* Title for alert when access to media capture is not granted */ -"Media Capture" = "Tangkapan Media"; - -/* Title for alert when a generic error happened when loading media - Title for alert when access to the media library is not granted by the user */ -"Media Library" = "Pustaka Media"; - /* Title for action sheet with media options. */ "Media Options" = "Opsi Media"; @@ -4510,9 +4403,6 @@ translators: Block name. %s: The localized block name */ /* translators: %s: block title e.g: \"Paragraph\". */ "Media options" = "Pilihan media"; -/* Alert title when there is issues loading an asset to preview. */ -"Media preview failed." = "Pratinjau media gagal."; - /* Alert displayed to the user when multiple media items have uploaded successfully. */ "Media uploaded (%ld files)" = "Media diunggah (%ld file)"; @@ -4550,9 +4440,6 @@ translators: Block name. %s: The localized block name */ Label for the share message field on the post settings. */ "Message" = "Pesan"; -/* Title of section containing image / video metadata such as size and file type */ -"Metadata" = "Metadata"; - /* Option to select the Microsft Outlook app when logging in with magic links */ "Microsoft Outlook" = "Microsoft Outlook"; @@ -4572,13 +4459,11 @@ translators: Block name. %s: The localized block name */ "Months and Years" = "Bulan dan Tahun"; /* Accessibility label for more button in dashboard quick start card. - Accessibility label for the More button in Page List. Accessibility label for the More button in Post List (compact view). Accessibility label for the More button on formatting toolbar. Accessibility label for the More button on Reader Cell Accessibility label for the More button on Reader's post details - Action button to display more available options - Label for the more post button. Tapping displays an action sheet with post options. */ + Action button to display more available options */ "More" = "Lainnya"; /* Action button to display more available options @@ -4636,15 +4521,8 @@ translators: Block name. %s: The localized block name */ /* Screen reader text for button that will move the menu item */ "Move menu item" = "Pindahkan item menu"; -/* Label for a button that moves a page to the draft folder - Label for an option that moves a post to the draft folder */ -"Move to Draft" = "Pindahkan ke Draf"; - -/* Label for a button that moves a page to the trash folder - Label for a option that moves a post to the trash folder - Title for button on the comment details page that moves the comment to trash when tapped. +/* Title for button on the comment details page that moves the comment to trash when tapped. Trash option in the trash confirmation alert. - Trash option in the trash page confirmation alert. Trashes the comment */ "Move to Trash" = "Pindahkan ke Sampah"; @@ -4676,7 +4554,8 @@ translators: Block name. %s: The localized block name */ Title of My Site tab */ "My Site" = "Situs Saya"; -/* Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ +/* Title for site picker screen. + Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ "My Sites" = "Situs Saya"; /* Siri Suggestion to open My Sites */ @@ -4926,9 +4805,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title for the view when there aren't any Activities to display in the Activity Log for a given filter. */ "No matching events found." = "Tidak ditemukan acara yang cocok."; -/* Message displayed when no results are returned from a media library search. Should match Calypso. - Phrase to show when the user search for images but there are no result to show. - Phrase to show when the user searches for GIFs but there are no result to show. */ +/* Message displayed when no results are returned from a media library search. Should match Calypso. */ "No media matching your search" = "Tidak ada media yang cocok dengan pencarian Anda"; /* Menus text shown when no menus were available for loading the Menus editor. */ @@ -4946,8 +4823,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Displayed in the Notifications Tab as a title, when there are no notifications */ "No notifications yet" = "Belum ada notifikasi."; -/* Displayed when the user is searching the pages list and there are no matching pages - Text displayed when there's no matching with the text search */ +/* Text displayed when there's no matching with the text search */ "No pages matching your search" = "Tidak ada halaman yang cocok dengan pencarian Anda"; /* Text displayed when search for plugins returns no results */ @@ -4968,9 +4844,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Message shown whent the reader finds no posts for the chosen tag */ "No posts have been made recently with this tag." = "Tidak ada pos yang baru saja dibuat dengan tag ini."; -/* Displayed when the user is searching the posts list and there are no matching posts */ -"No posts matching your search" = "Tidak ada pos yang cocok dengan pencarian Anda"; - /* Accessibility value for a Stats' Posting Activity Month if there are no posts. */ "No posts." = "Tidak ada pos."; @@ -5071,9 +4944,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* A message title */ "Nothing liked yet" = "Belum ada yang menyukai"; -/* Default message for empty media picker */ -"Nothing to show" = "Tidak ada yang bisa ditampilkan"; - /* Notifications Details Accessibility Identifier */ "Notification Details Table" = "Tabel Detail Pemberitahuan"; @@ -5133,7 +5003,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Alert dismissal title Button title. Acknowledges a prompt. Button title. An acknowledgement of the message displayed in a prompt. - Confirmation of action Default action Dismisses the alert Marks all notifications as read. @@ -5195,9 +5064,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* No comment provided by engineer. */ "Only show excerpt" = "Hanya tampilkan kutipan"; -/* Message telling the user that they've only enabled limited photo library permissions for the app. */ -"Only the selected photos you've given access to are available." = "Hanya tersedia foto-foto yang mana Anda diizinkan untuk mengaksesnya."; - /* An informal exclaimation that means `something went wrong`. Title for the view when there's an error loading a comment. Title for the view when there's an error loading Activity Log @@ -5232,9 +5098,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title of a button that opens the apps settings in the system Settings.app */ "Open Settings" = "Buka Pengaturan"; -/* Editor button to swich the media picker from quick mode to full picker */ -"Open full media picker" = "Buka pemilih media penuh"; - /* No comment provided by engineer. */ "Open in Safari" = "Buka di Safari"; @@ -5274,6 +5137,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Divider on initial auth view separating auth options. */ "Or" = "Atau"; +/* Instruction text for other forms of two-factor auth methods. */ +"Or choose another form of authentication." = "Atau pilih bentuk autentikasi lainnya."; + /* Label for button to log in using site address. Underscores _..._ denote underline. */ "Or log in by _entering your site address_." = "Atau login dengan _memasukkan alamat situs Anda_."; @@ -5332,15 +5198,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title shown when selecting a post type of Page from the Share Extension. */ "Page" = "Halaman"; -/* Prompts the user that a restored page was moved to the drafts list. */ -"Page Restored to Drafts" = "Halaman Dipulihkan ke Draf"; - -/* Prompts the user that a restored page was moved to the published list. */ -"Page Restored to Published" = "Laman Dipulihkan ke Dipublikasikan"; - -/* Prompts the user that a restored page was moved to the scheduled list. */ -"Page Restored to Scheduled" = "Halaman Dipulihkan ke Dijadwalkan"; - /* Name of the button to open the page settings The title of the Page Settings screen. */ "Page Settings" = "Pengaturan Halaman"; @@ -5357,9 +5214,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of notification displayed when a page has failed to upload. */ "Page failed to upload" = "Halaman gagal diunggah"; -/* A short message explaining that a page was moved to the trash bin. */ -"Page moved to trash." = "Halaman dipindahkan ke sampah."; - /* Title of notification displayed when a page has been successfully saved as a draft. */ "Page pending review" = "Halaman menunggu peninjauan"; @@ -5431,8 +5285,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title of pending Comments filter. */ "Pending" = "Tertunda"; -/* Name for the status of a post pending review. - Title of the Pending Review Badge */ +/* Name for the status of a post pending review. */ "Pending review" = "Menunggu ulasan"; /* Noun. Title of the people management feature. @@ -5461,12 +5314,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Photography site intent topic */ "Photography" = "Fotografi"; -/* Tab bar title for the Photos tab in Media Picker */ -"Photos" = "Foto"; - -/* Subtitle for placeholder in Free Photos. The company name 'Pexels' should always be written as it is. */ -"Photos provided by Pexels" = "Foto disediakan oleh Pexels"; - /* Title for selecting a new username in the site creation flow. */ "Pick username" = "Pilih nama pengguna"; @@ -5559,7 +5406,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Please enter the password for your WordPress.com account to log in with your Apple ID." = "Masukkan kata sandi akun WordPress.com Anda untuk login dengan ID Apple."; /* Instruction text on the two-factor screen. */ -"Please enter the verification code from your authenticator app, or tap the link below to receive a code via SMS." = "Masukkan kode verifikasi dari aplikasi pengautentikasi atau klik tautan berikut untuk mendapat kode via SMS."; +"Please enter the verification code from your authenticator app." = "Masukkan kode verifikasi dari aplikasi pengautentikasi."; /* Popup message to ask for user credentials (fields shown below). */ "Please enter your credentials" = "Masukkan kredensial Anda"; @@ -5654,15 +5501,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ The post formats available for the post. Should be the same as in core WP. */ "Post Format" = "Format Tulisan"; -/* Prompts the user that a restored post was moved to the drafts list. */ -"Post Restored to Drafts" = "Pos Dipulihkan ke Draf"; - -/* Prompts the user that a restored post was moved to the published list. */ -"Post Restored to Published" = "Pos Dipulihkan ke Dipublikasikan"; - -/* Prompts the user that a restored post was moved to the scheduled list. */ -"Post Restored to Scheduled" = "Pos Dipulihkan ke Dijadwalkan"; - /* Name of the button to open the post settings The title of the Post Settings screen. */ "Post Settings" = "Pengaturan Pos"; @@ -5682,9 +5520,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of notification displayed when a post has failed to upload. */ "Post failed to upload" = "Pos gagal diunggah"; -/* A short message explaining that a post was moved to the trash bin. */ -"Post moved to trash." = "Pos dipindahkan ke sampah."; - /* Title of notification displayed when a post has been successfully saved as a draft. */ "Post pending review" = "Pos menunggu peninjauan"; @@ -5743,9 +5578,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Period Stats 'Posts and Pages' header */ "Posts and Pages" = "Pos dan Halaman"; -/* Title of the Posts Page Badge */ -"Posts page" = "Halaman pos"; - /* Message informing the user that their static homepage for posts was set successfully */ "Posts page successfully updated" = "Halaman pos berhasil diperbarui"; @@ -5758,9 +5590,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* A message explaining the Posts I Like feature in the reader */ "Posts that you like will appear here." = "Postingan yang Anda sukai akan muncul di sini."; -/* Subtitle for placeholder in Tenor picker. `The company name 'Tenor' should always be written as it is. */ -"Powered by Tenor" = "Didukung oleh Tenor"; - /* Browse premium themes selection title */ "Premium" = "Premium"; @@ -5779,18 +5608,12 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title for screen to preview a static content. */ "Preview" = "Pratampil"; -/* Action for Media Picker to preview the selected media items. The argument in the string represents the number of elements (as numeric digits) selected */ -"Preview %@" = "Pratinjau %@"; - /* Title for web preview device switching button */ "Preview Device" = "Pratinjau Perangkat"; /* Title on display preview error */ "Preview Unavailable" = "Pratinjau Tidak Tersedia"; -/* Accessibility label for media item preview for user's viewing an item in their media library */ -"Preview media" = "Pratinjau media"; - /* No comment provided by engineer. */ "Preview page" = "Pratinjau halaman"; @@ -5837,8 +5660,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Privacy notice for California users" = "Pemberitahuan privasi untuk pengguna di California"; /* Name for the status of a post that is marked private. - Privacy setting for posts set to 'Private'. Should be the same as in core WP. - Title of the Private Badge */ + Privacy setting for posts set to 'Private'. Should be the same as in core WP. */ "Private" = "Privat"; /* No comment provided by engineer. */ @@ -5888,12 +5710,10 @@ translators: %s: Select control button label e.g. \"Button width\" */ Label for the publish date button. */ "Publish Date" = "Tanggal Publikasi"; -/* A short phrase indicating a post is due to be immedately published. - Label for a button that moves a page to the published folder, publishing with the current date/time. */ +/* A short phrase indicating a post is due to be immedately published. */ "Publish Immediately" = "Terbitkan Segera"; /* Label for a button that publishes the post - Label for an option that moves a publishes a post immediately Title of button allowing the user to immediately publish the post they are editing. */ "Publish Now" = "Publikasikan Sekarang"; @@ -5911,8 +5731,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Name for the status of a published post. Period Stats 'Published' header - Title of the published filter. This filter shows a list of posts that the user has published. - Title of the published header in search list. */ + Title of the published filter. This filter shows a list of posts that the user has published. */ "Published" = "Dipublikasikan"; /* Precedes the name of the blog just posted on */ @@ -6054,8 +5873,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of the completion screen of the Blogging Reminders Settings screen when the reminders are removed. */ "Reminders removed" = "Pengingat dihapus"; -/* Add asset to media picker list - Alert button to confirm a plugin to be removed +/* Alert button to confirm a plugin to be removed Button label when removing a blog Label for the Remove Feature Image icon. Tapping will show a confirmation screen for removing the feature image from the post. Remove Action @@ -6208,9 +6026,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of secondary button on alert prompting verify their accounts while attempting to publish */ "Resend" = "Kirim ulang"; -/* Title of the reset button */ -"Reset" = "Reset"; - /* Accessibility label for the reset activity type button */ "Reset Activity Type filter" = "Atur ulang penyaringan Jenis Aktivitas"; @@ -6265,12 +6080,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ Button title, displayed when media has failed to upload. Allows the user to try the upload again. Button title. Retries uploading a post. If a user taps the button with this label, the action that evinced this error view will be retried. - Label for a button that attempts to re-upload a page that previously failed to upload. - Label for the retry post upload button. Tapping attempts to upload the post again. Opens the media library . Retries the upload of a user's post. Retry updating User's Role - Retry uploading the post. Retry. Action Retry. Verb – retry a failed media upload. The Jetpack view button title used when an error occurred @@ -6282,9 +6094,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Button title that triggers a scan */ "Retry Scan" = "Pindai Kembali"; -/* User action to retry media upload. */ -"Retry Upload" = "Coba Unggah Lagi"; - /* User action to retry all failed media uploads. */ "Retry all" = "Coba lagi semuanya"; @@ -6382,9 +6191,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Accessibility label for the 'Save Post' button when a post has been saved. */ "Saved Post" = "Pos yang Disimpan"; -/* Text displayed in HUD when a media item's metadata (title, etc) is saved successfully. */ -"Saved!" = "Tersimpan!"; - /* Accessibility hint for the 'Save Post' button. */ "Saves this post for later." = "Menyimpan pos ini untuk nanti."; @@ -6395,7 +6201,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Saving post…" = "Menyimpan pos…"; /* Menus save button title while it is saving a Menu. - Text displayed in HUD while a media item's metadata (title, etc) is being saved. Text displayed in HUD while a post is being saved as a draft. */ "Saving..." = "Menyimpan..."; @@ -6486,21 +6291,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* No comment provided by engineer. */ "Search or type URL" = "Cari atau ketik URL"; -/* Text displayed when the search controller will be presented */ -"Search pages" = "Cari halaman"; - -/* Text displayed when the search controller will be presented */ -"Search posts" = "Cari pos"; - /* No comment provided by engineer. */ "Search settings" = "Pengaturan pencarian"; -/* Title for placeholder in Tenor picker */ -"Search to find GIFs to add to your Media Library!" = "Cari GIF untuk ditambahkan ke Pustaka Media Anda!"; - -/* Title for placeholder in Free Photos */ -"Search to find free photos to add to your Media Library!" = "Cari foto gratis untuk ditambahkan ke Pustaka Media Anda!"; - /* Menus search bar placeholder text. */ "Search..." = "Pencarian..."; @@ -6571,9 +6364,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Register Domain - Domain contact information field placeholder for Country */ "Select Country" = "Pilih Negara"; -/* Title of button that allows the user to select more photos to access within the app */ -"Select More" = "Pilih Lainnya"; - /* Blog Picker's Title */ "Select Site" = "Pilih Situs"; @@ -6595,9 +6385,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Register domain - Title for the Choose domain button of Suggested domains screen */ "Select domain" = "Pilih domain"; -/* Accessibility hint for actions when displaying media items. */ -"Select media." = "Pilih media."; - /* Accessibility label for selecting paragraph style button on formatting toolbar. */ "Select paragraph style" = "Pilih gaya paragraf"; @@ -6701,19 +6488,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label for connected service in Publicize stat. */ "Service" = "Layanan"; -/* Label for a button that opens the Set Parent options view controller - Navigation title displayed on the navigation bar */ +/* Navigation title displayed on the navigation bar */ "Set Parent" = "Atur Induk"; /* No comment provided by engineer. */ "Set as Featured Image" = "Jadikan Gambar Unggulan "; -/* Label for a button that sets the selected page as the site's Homepage */ -"Set as Homepage" = "Atur sebagai Beranda"; - -/* Label for a button that sets the selected page as the site's Posts page */ -"Set as Posts Page" = "Atur sebagai Halaman Pos"; - /* Notice confirming that an image has been set as the post's featured image. */ "Set as featured image" = "Atur sebagai gambar andalan"; @@ -6757,7 +6537,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Accessibility label for share buttons in nav bars Button label to share a post Button label to share a web page - Share the post. Shares the comment URL Spoken accessibility label Title for a button that allows the user to share their answer to the prompt. @@ -7143,8 +6922,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Name of setting configured when a site uses a static page as its homepage */ "Static Homepage" = "Beranda Statis"; -/* Label for post stats option. Tapping displays statistics for a post. - Noun. Abbreviation of Statistics. Name of the Stats feature +/* Noun. Abbreviation of Statistics. Name of the Stats feature Noun. Abbv. of Statistics. Links to a blog's Stats screen. Stats 3D Touch Shortcut Stats window title @@ -7175,9 +6953,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label text that defines a post marked as sticky */ "Sticky" = "Lekat"; -/* Accessibility label for a sticky post in the post list. */ -"Sticky." = "Melekat."; - /* User action to stop upload. */ "Stop upload" = "Berhenti mengunggah"; @@ -7234,7 +7009,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ Theme Support action title */ "Support" = "Dukungan"; -/* Button used to switch site */ +/* Title for back button that leads to the site picker screen. */ "Switch Site" = "Pindah Situs"; /* Switches the Editor to HTML Mode */ @@ -7322,9 +7097,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label explaining why users might want to add tags. */ "Tags help tell readers what a post is about. Separate different tags with commas." = "Tag membantu memberi tahu para pembaca tentang isi sebuah pos. Pisahkan tag yang berbeda dengan koma."; -/* Menu option for taking an image or video with the device's camera. */ -"Take Photo or Video" = "Memotret atau Merekam Video"; - /* No comment provided by engineer. */ "Take a Photo" = "Ambil Foto"; @@ -7395,12 +7167,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Accessibility hint */ "Tap to select the previous period" = "Ketuk untuk memilih periode sebelumnya"; -/* Accessibility hint for button used to switch site */ -"Tap to switch to another site, or add a new site" = "Ketuk untuk beralih ke situs lain, atau menambahkan situs baru"; - -/* Accessibility hint for media item preview for user's viewing an item in their media library */ -"Tap to view media in full screen" = "Ketuk untuk melihat media dalam layar penuh"; - /* Accessibility hint for a button that opens a new view with more details. */ "Tap to view more details." = "Ketuk untuk melihat detail selengkapnya."; @@ -7446,10 +7212,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* No comment provided by engineer. */ "Text formatting controls are located within the toolbar positioned above the keyboard while editing a text block" = "Kontrol pemformatan teks terletak di dalam bilah peralatan yang ada di atas keyboard saat menyunting blok teks"; -/* Button title - The button's title text to send a 2FA code via SMS text message. */ +/* Button title */ "Text me a code instead" = "Kirimi saya kode saja"; +/* The button's title text to send a 2FA code via SMS text message. */ +"Text me a code via SMS" = "Kirimkan kode melalui SMS"; + /* Message of alert when theme activation succeeds */ "Thanks for choosing %@ by %@" = "Terima kasih telah memilih %1$@ dari %2$@"; @@ -7477,9 +7245,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Error message shown to a user who is trying to share to Facebook but does not have any available Facebook Pages. */ "The Facebook connection cannot find any Pages. Publicize cannot connect to Facebook Profiles, only published Pages." = "Koneksi Facebook tidak dapat menemukan Halaman apa pun. Publikasikan tidak dapat terhubung ke Profil Facebook, hanya Halaman yang dipublikasikan."; -/* Message shown when a GIF failed to load while trying to add it to the Media library. */ -"The GIF could not be added to the Media Library." = "GIF tidak bisa ditambahkan ke Pustaka Media."; - /* Description shown when a user logs in with Google but no matching WordPress.com account is found */ "The Google account \"%@\" doesn't match any account on WordPress.com" = "Akun Google \"%@\" tidak cocok dengan akun apa pun di WordPress.com"; @@ -7607,7 +7372,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Error message shown when user attempts to remove the site owner. */ "The user you are trying to remove is the owner of this site. Please contact support for assistance." = "Pengguna yang ingin Anda hapus adalah pemilik situs ini. Silakan hubungi dukungan untuk mendapatkan bantuan."; -/* Error message informing a user about an invalid password. */ +/* No comment provided by engineer. */ "The username or password stored in the app may be out of date. Please re-enter your password in the settings and try again." = "Nama pengguna atau kata sandi yang tersimpan di app mungkin sudah tak berlaku. Silahkan isi ulang kata sandi di pengaturan dan coba lagi."; /* Message shown when a video failed to load while trying to add it to the Media library. */ @@ -7675,9 +7440,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* A short error message letting the user know about a problem displaying a post. */ "There was a problem displaying this post." = "Ada masalah saat menampilkan pos ini."; -/* Error message displayed when the Media Library is unable to load a full sized preview of an item. */ -"There was a problem loading the media item." = "Terjadi masalah saat memuat item media."; - /* The loading view subtitle displayed when an error occurred */ "There was a problem loading your data, refresh your page to try again." = "Terjadi masalah saat memuat data Anda, segarkan halaman untuk mencoba lagi."; @@ -7690,9 +7452,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Explaining to the user there was an error trying to obtain the current location of the user. */ "There was a problem when trying to access your location. Please try again later." = "Ada masalah saat mencoba mengakses lokasi Anda. Harap coba lagi nanti."; -/* Explaining to the user there was an generic error accesing media. */ -"There was a problem when trying to access your media. Please try again later." = "Ada masalah saat mencoba mengakses media Anda. Harap coba lagi nanti."; - /* Message for stories unknown error. */ "There was a problem with the Stories editor. If the problem persists you can contact us via the Me > Help & Support screen." = "Terjadi masalah dengan editor Cerita. Jika masalah berlanjut, Anda dapat menghubungi kami melalui layar Saya > Bantuan & Dukungan."; @@ -7763,9 +7522,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* A description informing the user in order to proceed with this feature we will need camera permissions, and how to enable it. */ "This app needs permission to access the Camera to scan login codes, tap on the Open Settings button to enable it." = "Aplikasi ini memerlukan akses ke Kamera untuk memindai kode login. Ketuk tombol Buka Pengaturan untuk mengaktifkannya."; -/* Explaining to the user why the app needs access to the device media library. */ -"This app needs permission to access your device media library in order to add photos and\/or video to your posts. Please change the privacy settings if you wish to allow this." = "Aplikasi ini memerlukan izin untuk mengakses pustaka perangkat Anda untuk menambahkan foto dan\/atau video ke pos Anda. Harap ubah pengaturan privasi jika Anda ingin mengizinkan ini."; - /* No comment provided by engineer. */ "This color combination may be hard for people to read. Try using a brighter background color and\/or a darker text color." = "Kombinasi warna ini mungkin sulit dibaca. Coba gunakan warna latar belakang yang lebih terang dan\/atau warna teks yang lebih gelap."; @@ -7875,6 +7631,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Description of a Quick Start Tour */ "Time to finish setting up your site! Our checklist walks you through the next steps." = "Waktunya menyelesaikan penyiapan situs Anda! Daftar periksa kami memberikan panduan bagi Anda untuk langkah selanjutnya."; +/* Error when the uses takes more than 1 minute to submit a security key. */ +"Time's up, but don't worry, your security is our priority. Please try again!" = "Waktu habis, tetapi jangan khawatir, keamanan Anda menjadi prioritas kami. Coba lagi!"; + /* WordPress.com Marketing Footer Text */ "Tips for getting the most out of WordPress.com." = "Kiat untuk mendapat maksimal dari WordPress.com."; @@ -8004,18 +7763,11 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* No comment provided by engineer. */ "Transform block…" = "Mengubah blok..."; -/* Accessibility label for trash button to delete items from the user's media library - Accessibility label for trash buttons in nav bars +/* Accessibility label for trash buttons in nav bars Trashes a comment Trashes the comment */ "Trash" = "Buang"; -/* Accessibility hint for trash button to delete items from the user's media library */ -"Trash selected media" = "Media terpilih di tempat sampah"; - -/* Title of the trash page confirmation alert. */ -"Trash this page?" = "Hapus halaman ini?"; - /* Title of the trash confirmation alert. */ "Trash this post?" = "Buang pos ini?"; @@ -8133,9 +7885,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Shown when a user logs in with Google but it subsequently fails to work as login to WordPress.com */ "Unable To Connect" = "Tidak Dapat Terhubung"; -/* An error message. */ -"Unable to Connect" = "Tidak Dapat Terhubung"; - /* Title for stories unknown error. */ "Unable to Create Stories Editor" = "Tidak dapat Membuat Editor Cerita"; @@ -8151,9 +7900,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* An error message shown when there is an issue creating new invite links. */ "Unable to create new invite links." = "Tidak dapat membuat tautan undangan baru."; -/* Text displayed in HUD if there was an error attempting to delete a group of media items. */ -"Unable to delete all media items." = "Tidak dapat menghapus semua item media."; - /* Text displayed in HUD if there was an error attempting to delete a media item. */ "Unable to delete media item." = "Tidak dapat menghapus item media."; @@ -8217,12 +7963,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Message displayed when sharing a link to the downloadable backup fails. */ "Unable to share link" = "Tidak dapat berbagi tautan"; -/* Message that appears when a user tries to trash a page while their device is offline. */ -"Unable to trash pages while offline. Please try again later." = "Tidak dapat menghapus halaman saat offline. Silakan coba lagi nanti."; - -/* Message that appears when a user tries to trash a post while their device is offline. */ -"Unable to trash posts while offline. Please try again later." = "Tidak dapat membuang pos saat offline. Silakan coba lagi nanti."; - /* Notice title when turning site notifications off fails. */ "Unable to turn off site notifications" = "Tidak dapat menonaktifkan pemberitahuan situs"; @@ -8295,8 +8035,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ Button title. Reverts the previous notification operation Revert an operation Revert enabling notification after successfully subcribing to the comments for the post. - The title of an 'undo' button. Tapping the button moves a trashed page out of the trash folder. - The title of an 'undo' button. Tapping the button moves a trashed post out of the trash folder. Undo action */ "Undo" = "Urungkan"; @@ -8339,9 +8077,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title for Unknown HTML Editor */ "Unknown HTML" = "HTML Tidak Dikenal"; -/* Label to use when creation date from media asset is not know. */ -"Unknown creation date" = "Tanggal pembuatan tidak diketahui"; - /* No comment provided by engineer. */ "Unknown error" = "Galat tak diketahui"; @@ -8507,6 +8242,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of a row displayed on the debug screen used to configure the sandbox store use in the App. */ "Use Sandbox Store" = "Gunakan Toko Sandbox"; +/* The button's title text to use a security key. */ +"Use a security key" = "Gunakan kunci keamanan"; + /* Option to enable the block editor for new posts */ "Use block editor" = "Gunakan penyunting blok"; @@ -8582,15 +8320,10 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of an alert informing users that the video they are trying to select is not allowed. */ "Video not uploaded" = "Video tidak terunggah"; -/* Accessibility label for video thumbnails in the media collection view. The parameter is the creation date of the video. */ -"Video, %@" = "Video, %@"; - /* Period Stats 'Videos' header */ "Videos" = "Video"; /* Button title. Displays a summary / sharing screen for a specific post. - Label for a button that opens the page when tapped. - Label for the view post button. Tapping displays the post as it appears on the web. Opens the post epilogue screen to allow sharing / viewing of a post. Theme View action title Verb. The screen title shown when viewing a post inside the app. */ @@ -8705,6 +8438,10 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Message shown on screen while waiting for Google to finish its signup process. */ "Waiting for Google to complete…" = "Menunggu Google untuk menyelesaikan…"; +/* Text while the webauthn signature is being verified + Text while waiting for a security key challenge */ +"Waiting for security key" = "Menunggu kunci keamanan"; + /* View title during the Google auth process. */ "Waiting..." = "Menunggu..."; @@ -9079,6 +8816,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* An error message shown when a wpcom user provides the wrong password. */ "Whoops, something went wrong and we couldn't log you in. Please try again!" = "Ups, terjadi kesalahan dan Anda tidak dapat login. Coba lagi!"; +/* Generic error on the 2FA screen */ +"Whoops, something went wrong. Please try again!" = "Ups, sepertinya ada yang salah. Coba lagi!"; + +/* Error when the uses chooses an invalid security key on the 2FA screen. */ +"Whoops, that security key does not seem valid. Please try again with another one" = "Ups, tampaknya kunci keamanan tidak valid. Silakan coba lagi dengan kunci lain"; + /* Error message shown when an incorrect two factor code is provided. */ "Whoops, that's not a valid two-factor verification code. Double-check your code and try again!" = "Ups, kode verifikasi dua faktor tersebut tidak valid. Periksa kembali kode Anda kemudian coba lagi!"; @@ -9106,9 +8849,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Siri Suggestion to open Support */ "WordPress Help" = "Bantuan WordPress"; -/* Name for the WordPress Media Library */ -"WordPress Media" = "Media WordPress"; - /* Accessibility label for selecting an image or video from the user's WordPress media library on formatting toolbar. */ "WordPress Media Library" = "Pustaka Media WordPress"; @@ -9423,9 +9163,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Share extension error dialog text. */ "Your account does not have permission to upload media to this site. The Site Administrator can change these permissions." = "Akun Anda tidak memiliki izin untuk mengunggah media ke situs ini. Administrator Situs dapat mengubah izin ini."; -/* Explaining to the user why the app needs access to the device media library. */ -"Your app is not authorized to access media library due to active restrictions such as parental controls. Please check your parental control settings in this device." = "Aplikasi Anda tidak memiliki izin akses pustaka media karena adanya pembatasan aktif seperti kontrol orang tua. Harap periksa pengaturan kontrol orang tua di perangkat ini."; - /* Title for the Jetpack Backup Complete message. */ "Your backup is now available for download" = "Cadangan Anda sudah dapat diunduh"; @@ -9444,9 +9181,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of the site address section in the Domains Dashboard. */ "Your free WordPress.com address is" = "Alamat WordPress.com gratis Anda adalah"; -/* Error message when picked media cannot be imported into stories. */ -"Your media could not be exported. If the problem persists you can contact us via the Me > Help & Support screen." = "Media Anda tidak dapat diekspor. If the problem persists you can contact us via the Me > layar Bantuan & Dukungan."; - /* Details about recently acquired domain on domain credit redemption success screen */ "Your new domain %@ is being set up. It may take up to 30 minutes for your domain to start working." = "Domain baru Anda %@ sedang disiapkan. Perlu waktu hingga 30 menit agar domain anda mulai berfungsi."; @@ -9570,8 +9304,38 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* This is the string we display when prompting the user to review the WordPress app */ "appRatings.wordpress.prompt" = "Apa pendapat Anda tentang WordPress?"; -/* Label displayed on audio media items. */ -"audio" = "audio"; +/* Option to enable the optimization of images when uploading. */ +"appSettings.media.imageOptimizationRow" = "Optimalkan Gambar"; + +/* Indicates an image will use high quality when uploaded. */ +"appSettings.media.imageQuality.high" = "Tinggi"; + +/* Indicates an image will use low quality when uploaded. */ +"appSettings.media.imageQuality.low" = "Rendah"; + +/* Indicates an image will use maximum quality when uploaded. */ +"appSettings.media.imageQuality.maximum" = "Maksimum"; + +/* Indicates an image will use medium quality when uploaded. */ +"appSettings.media.imageQuality.medium" = "Sedang"; + +/* The quality of image used when uploading */ +"appSettings.media.imageQuality.title" = "Kualitas"; + +/* Title for the image quality settings option. */ +"appSettings.media.imageQualityRow" = "Kualitas Gambar"; + +/* Message of an alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.message" = "Pengoptimalan gambar mengecilkan gambar agar bisa diunggah lebih cepat.\n\nPilihan ini diaktifkan sesuai dengan pengaturan asal, tetapi Anda dapat mengubahnya di pengaturan aplikasi kapan saja."; + +/* Title of an alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.title" = "Teruskan pengoptimalan gambar?"; + +/* Title of button for turning off image optimization, displayed in the alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.turnOff" = "Tidak, matikan"; + +/* Title of button for leaving on image optimization, displayed in the alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.turnOn" = "Ya, biarkan"; /* translators: displays audio file extension. e.g. MP3 audio file */ "audio file" = "file audio"; @@ -9685,7 +9449,41 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "blogHeader.actionCopyURL" = "Salin URL"; /* Context menu button title */ -"blogHeader.actionOpenInBrowser" = "Buka di Browser"; +"blogHeader.actionVisitSite" = "Kunjungi situs"; + +/* Title for a button that, when tapped, shows more info about participating in Bloganuary. */ +"bloganuary.dashboard.card.button.learnMore" = "Baca selengkapnya"; + +/* Title for the Bloganuary dashboard card while Bloganuary is running. */ +"bloganuary.dashboard.card.runningTitle" = "Bloganuary telah hadir!"; + +/* Title for the Bloganuary dashboard card. */ +"bloganuary.dashboard.card.title" = "Bloganuary segera hadir!"; + +/* Title of a button that calls the user to enable the Blogging Prompts feature. */ +"bloganuary.learnMore.modal.button.promptsDisabled" = "Nyalakan prompt blogging"; + +/* Title of a button that will dismiss the Bloganuary modal when tapped. +Note that the word 'go' here should have a closer meaning to 'start' rather than 'move forward'. */ +"bloganuary.learnMore.modal.button.promptsEnabled" = "Ayo mulai!"; + +/* The second line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.description.second" = "Poskan tanggapan Anda."; + +/* The third line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.description.third" = "Baca tanggapan dari blogger lain untuk mendapatkan inspirasi dan menjalin koneksi baru."; + +/* The first line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.descriptions.first" = "Dapatkan prompt baru untuk menginspirasi Anda setiap hari."; + +/* An additional piece of information shown in case the user has the Blogging Prompts feature disabled. */ +"bloganuary.learnMore.modal.footer.addition" = "Untuk mengikuti Bloganuary, Anda perlu mengaktifkan Prompt Blogging."; + +/* An informative excerpt shown in a subtler tone. */ +"bloganuary.learnMore.modal.footer.text" = "Bloganuary akan menggunakan Prompt Blogging Harian untuk mengirimkan topik bulan Januari kepada Anda."; + +/* The headline text of the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.headline" = "Ikuti tantangan menulis sebulan penuh dari kami"; /* Verb. Dismisses the blogging prompt notification. */ "bloggingPrompt.pushNotification.customActionDescription.dismiss" = "Tutup"; @@ -9714,6 +9512,12 @@ Example: Comment on Example: Reply to Pamela Nguyen */ "comment.header.subText.reply" = "Balasan untuk %1$@"; +/* Error message informing a user about an invalid password. */ +"common.reEnterPasswordMessage" = "Nama pengguna atau kata sandi yang disimpan di dalam aplikasi ini mungkin sudah usang. Masukkan kembali kata sandi Anda dalam pengaturan dan coba lagi."; + +/* An error message. */ +"common.unableToConnect" = "Tidak Dapat Terhubung"; + /* Footnote for the privacy compliance popover. */ "compliance.analytics.popover.footnote" = "Cookie ini memungkinkan kami untuk mengoptimalkan performa dengan mengumpulkan informasi tentang cara pengguna berinteraksi dengan situs kami."; @@ -9861,50 +9665,92 @@ Example: Reply to Pamela Nguyen */ /* Title for a menu action in the context menu on the Jetpack install card. */ "domain.dashboard.card.menu.hide" = "Tutup"; -/* Domain Purchase Completion footer */ -"domain.purchase.preview.footer" = "Diperlukan 30 menit agar domain khusus Anda dapat berfungsi."; +/* Search domain - Title for the Suggested domains screen */ +"domain.management.addDomain.search.title" = "Cari domain"; -/* Domain Purchase Completion description (only for FREE domains). */ -"domain.purchase.preview.free.description" = "Selanjutnya, kami akan membantu Anda agar mudah ditemukan."; +/* Domain management buy domain card button title */ +"domain.management.buy.card.button.title" = "Dapatkan Domain"; -/* Domain Purchase Completion description (only for PAID domains). */ -"domain.purchase.preview.paid.description" = "Kami telah mengirim e-mail resi Anda. Selanjutnya, kami akan membantu Anda tersedia untuk siapa pun."; +/* Domain management buy domain card subtitle */ +"domain.management.buy.card.subtitle" = "Tambahkan situs nanti."; -/* Reflects that site is live when domain purchase feature flag is ON. */ -"domain.purchase.preview.title" = "Selamat, situs Anda sudah aktif!"; +/* Domain management buy domain card title */ +"domain.management.buy.card.title" = "Beli domain saja"; + +/* The expired label of the domain card in All Domains screen. */ +"domain.management.card.expired.label" = "Kedaluwarsa"; + +/* The renews label of the domain card in All Domains screen. */ +"domain.management.card.renews.label" = "Perpanjangan"; + +/* The empty state button title in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.button.title" = "Cari domain"; + +/* The empty state description in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.description" = "Ketuk di bawah untuk mencari domain ideal."; + +/* The empty state title in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.title" = "Anda belum memiliki domain"; + +/* The empty state description in All Domains screen when an error occurs */ +"domain.management.error.empty.state.description" = "Terjadi error saat memuat domain Anda. Jika masalah berlanjut, hubungi staf dukungan kami."; + +/* The empty state title in All Domains screen when an error occurs */ +"domain.management.error.empty.state.title" = "Terjadi kesalahan"; + +/* The empty state button title in All Domains screen when an error occurs */ +"domain.management.error.state.button.title" = "Coba lagi"; + +/* The empty state description in All Domains screen when the user is offline */ +"domain.management.offline.empty.state.description" = "Periksa koneksi jaringan Anda dan coba lagi."; + +/* The empty state title in All Domains screen when the user is offline */ +"domain.management.offline.empty.state.title" = "Tidak ada koneksi internet"; + +/* Domain management choose site card button title */ +"domain.management.purchase.footer" = "*Domain gratis selama setahun dalam semua paket tahunan berbayar"; + +/* Domain management purchase domain screen title */ +"domain.management.purchase.subtitle" = "Jangan khawatir. Anda bisa dengan mudah menambahkan situs di lain waktu."; -/* Status of a domain in `Action Required` state */ -"domain.status.action.required" = "Tindakan Diperlukan"; +/* Domain management purchase domain screen title. */ +"domain.management.purchase.title" = "Pilih cara menggunakan domain"; -/* Status of a domain in `Active` state */ -"domain.status.active" = "Aktif"; +/* The search bar title in All Domains screen. */ +"domain.management.search-bar.title" = "Cari domain"; -/* Status of a domain in `Complete Setup` state */ -"domain.status.complete.setup" = "Selesaikan Penyiapan"; +/* The empty state description in All Domains screen when the are no domains matching the search criteria */ +"domain.management.search.empty.state.description" = "Kami tidak dapat menemukan domain yang sesuai dengan pencarian Anda untuk '%@'."; -/* Status of a domain in `Error` state */ -"domain.status.error" = "Error"; +/* The empty state title in All Domains screen when the are no domains matching the search criteria */ +"domain.management.search.empty.state.title" = "Tidak ada domain yang sesuai."; -/* Status of a domain in `Expired` state */ -"domain.status.expired" = "Kedaluwarsa"; +/* Domain management choose site card button title */ +"domain.management.site.card.button.title" = "Pilih Situs"; -/* Status of a domain in `Expiring Soon` state */ -"domain.status.expiring.soon" = "Segera Berakhir"; +/* Domain management choose site card subtitle */ +"domain.management.site.card.footer" = "Domain gratis untuk tahun pertama*"; -/* Status of a domain in `Failed` state */ -"domain.status.failed" = "Gagal"; +/* Domain management choose site card subtitle */ +"domain.management.site.card.subtitle" = "Gunakan dengan situs yang sudah Anda buat."; -/* Status of a domain in `In Progress` state */ -"domain.status.in.progress" = "Sedang Berlangsung"; +/* Domain management choose site card title */ +"domain.management.site.card.title" = "Situs WordPress.com yang sudah ada"; -/* Status of a domain in `Renew` state */ -"domain.status.renew" = "Perpanjang"; +/* Domain Management Screen Title */ +"domain.management.title" = "Semua Domain"; -/* Status of a domain in `Verify Email` state */ -"domain.status.verify.email" = "Verifikasi email"; +/* Domain Purchase Completion footer */ +"domain.purchase.preview.footer" = "Diperlukan 30 menit agar domain khusus Anda dapat berfungsi."; + +/* Domain Purchase Completion description (only for FREE domains). */ +"domain.purchase.preview.free.description" = "Selanjutnya, kami akan membantu Anda agar mudah ditemukan."; -/* Status of a domain in `Verifying` state */ -"domain.status.verifying" = "Memverifikasi"; +/* Domain Purchase Completion description (only for PAID domains). */ +"domain.purchase.preview.paid.description" = "Kami telah mengirim e-mail resi Anda. Selanjutnya, kami akan membantu Anda tersedia untuk siapa pun."; + +/* Reflects that site is live when domain purchase feature flag is ON. */ +"domain.purchase.preview.title" = "Selamat, situs Anda sudah aktif!"; /* The 'Best Alternative' label under the domain name in 'Choose a domain' screen */ "domain.suggestions.row.best-alternative" = "Alternatif Terbaik"; @@ -9927,12 +9773,24 @@ Example: Reply to Pamela Nguyen */ /* The text to display for paid domains in 'Site Creation > Choose a domain' screen */ "domain.suggestions.row.yearly" = "per tahun"; +/* Title for the checkout screen. */ +"domains.checkout.title" = "Checkout"; + /* Action shown in a bottom notice to dismiss it. */ "domains.failure.dismiss" = "Abaikan"; /* Content show when the domain selection action fails. */ "domains.failure.title" = "Maaf, saat ini domain yang Anda coba tambahkan tidak dapat dibeli di aplikasi Jetpack."; +/* Title for the screen where the user can choose how to use the domain they're end up purchasing. */ +"domains.purchase.choice.title" = "Beli Domain"; + +/* Back button title that navigates back to the search domains screen. */ +"domains.search.backButton.title" = "Pencarian"; + +/* Title of screen where user chooses a site to connect to their selected domain */ +"domains.sitePicker.title" = "Pilih Situs"; + /* No comment provided by engineer. */ "double-tap to change unit" = "ketuk dua kali untuk mengubah unit"; @@ -9950,6 +9808,15 @@ Example: Reply to Pamela Nguyen */ Site Address placeholder */ "example.com" = "contoh.com"; +/* Title for confirmation navigation bar button item */ +"externalMediaPicker.add" = "Tambah"; + +/* Bottom toolbar title in the selection mode */ +"externalMediaPicker.toolbarSelectItemsPrompt" = "Pilih Gambar"; + +/* Bottom toolbar title in the selection mode */ +"externalMediaPicker.toolbarViewSelected" = "Tampilan Dipilih (%@)"; + /* Title of screen the displays the details of an advertisement campaign. */ "feature.blaze.campaignDetails.title" = "Detail Kampanye"; @@ -10049,9 +9916,6 @@ Example: Reply to Pamela Nguyen */ /* (placeholder) Help the user enter a URL into the field */ "http:\/\/my-site-address (URL)" = "http:\/\/alamat-situs-saya (URL)"; -/* Label displayed on image media items. */ -"image" = "gambar"; - /* Sentence to justify why the app is asking permission from the user to use their camera. */ "infoplist.NSCameraUsageDescription" = "Untuk mengambil foto atau video agar dapat digunakan di artikel Anda."; @@ -10352,6 +10216,9 @@ Example: Reply to Pamela Nguyen */ /* Indicating that referrer was marked as spam */ "marked as spam" = "ditandai sebagai spam"; +/* Products header text in Me Screen. */ +"me.products.header" = "Produk"; + /* Title of error prompt shown when a sync fails. */ "media.syncFailed" = "Tidak dapat menyinkronkan media"; @@ -10364,18 +10231,24 @@ Example: Reply to Pamela Nguyen */ /* Message of an alert informing users that the video they are trying to select is not allowed. */ "mediaExporter.videoLimitExceededError" = "Diperlukan paket berbayar untuk mengunggah video berdurasi lebih dari 5 menit."; -/* Verb. User action to dismiss error alert when failing to load media item. */ -"mediaItemTable.errorAlert.dismissButton" = "Tutup"; - /* Accessibility hint for add button to add items to the user's media library */ "mediaLibrary.addButtonAccessibilityHint" = "Tambah media baru"; /* Accessibility label for add button to add items to the user's media library */ "mediaLibrary.addButtonAccessibilityLabel" = "Tambah"; +/* Button name in the more menu */ +"mediaLibrary.aspectRatioGrid" = "Kolom Rasio Aspek"; + +/* Context menu button */ +"mediaLibrary.buttonDelete" = "Hapus"; + /* Media screen navigation bar button Select title */ "mediaLibrary.buttonSelect" = "Pilih"; +/* Context menu button */ +"mediaLibrary.buttonShare" = "Bagikan"; + /* Verb. Button title. Tapping cancels an action. */ "mediaLibrary.deleteConfirmationCancel" = "Batal"; @@ -10397,6 +10270,21 @@ Example: Reply to Pamela Nguyen */ /* Text displayed in HUD after successfully deleting a media item */ "mediaLibrary.deletionSuccessMessage" = "Dihapus!"; +/* The name of the media filter */ +"mediaLibrary.filterAll" = "Semua"; + +/* The name of the media filter */ +"mediaLibrary.filterAudio" = "Audio"; + +/* The name of the media filter */ +"mediaLibrary.filterDocuments" = "Dokumen"; + +/* The name of the media filter */ +"mediaLibrary.filterImages" = "Gambar"; + +/* The name of the media filter */ +"mediaLibrary.filterVideos" = "Video"; + /* User action to delete un-uploaded media. */ "mediaLibrary.retryOptionsAlert.delete" = "Hapus"; @@ -10409,6 +10297,12 @@ Example: Reply to Pamela Nguyen */ /* Message displayed when no results are returned from a media library search. Should match Calypso. */ "mediaLibrary.searchResultsEmptyTitle" = "Tidak ada media yang cocok dengan pencarian Anda"; +/* Text displayed in HUD if there was an error attempting to share a group of media items. */ +"mediaLibrary.sharingFailureMessage" = "Tidak dapat membagikan item yang dipilih."; + +/* Button name in the more menu */ +"mediaLibrary.squareGrid" = "Kisi Persegi"; + /* Media screen navigation title */ "mediaLibrary.title" = "Media"; @@ -10430,6 +10324,12 @@ Example: Reply to Pamela Nguyen */ /* The title of the button to dismiss the alert shown when the picked media cannot be imported into stories. */ "mediaPicker.failedMediaExportAlert.dismissButton" = "Tutup"; +/* Error message when picked media cannot be imported into stories. */ +"mediaPicker.failedMediaExportAlert.message" = "Media tidak dapat diekspor. Jika masalah berlanjut, Anda dapat menghubungi kami melalui layar Saya > Bantuan & Dukungan."; + +/* Error title when picked media cannot be imported into stories. */ +"mediaPicker.failedMediaExportAlert.title" = "Ekspor Media Gagal"; + /* Message for alert when access to camera is not granted */ "mediaPicker.noCameraAccessMessage" = "Aplikasi ini memerlukan izin untuk mengakses Kamera untuk menangkap media baru, harap ganti pengaturan privasi jika Anda ingin mengizinkan."; @@ -10463,6 +10363,12 @@ Example: Reply to Pamela Nguyen */ /* The name of the action in the context menu */ "mediaPicker.takeVideo" = "Ambil Video"; +/* Navigation title for media preview. Example: 1 of 3 */ +"mediaPreview.NofM" = "%1$@ dari %2$@"; + +/* Max image size in pixels (e.g. 300x300px) */ +"mediaSizeSlider.valueFormat" = "%1$d × %2$d piksel"; + /* The description in the Delete WordPress screen */ "migration.deleteWordpress.description" = "Aplikasi WordPress sepertinya masih terinstal."; @@ -10475,9 +10381,6 @@ Example: Reply to Pamela Nguyen */ /* The title in the Delete WordPress screen */ "migration.deleteWordpress.title" = "Anda tidak lagi membutuhkan aplikasi WordPress di perangkat Anda"; -/* Primary button title in the migration done screen. */ -"migration.done.actions.primary.title" = "Selesai"; - /* Footer for the migration done screen. */ "migration.done.footer" = "Kami sarankan untuk menghapus instalan aplikasi WordPress di perangkat Anda guna mencegah terjadinya konflik data."; @@ -10487,6 +10390,9 @@ Example: Reply to Pamela Nguyen */ /* Primary description in the migration done screen. */ "migration.done.primaryDescription" = "Kami telah mentransfer semua data dan pengaturan Anda. Semua percis seperti terakhir Anda tinggalkan."; +/* Secondary description (second paragraph) in the migration done screen. */ +"migration.done.secondaryDescription" = "Waktunya untuk melanjutkan perjalanan WordPress Anda di aplikasi Jetpack!"; + /* Title of the migration done screen. */ "migration.done.title" = "Terima kasih telah beralih ke Jetpack!"; @@ -10535,6 +10441,9 @@ Example: Reply to Pamela Nguyen */ /* The title in the migration welcome screen */ "migration.welcome.title" = "Selamat datang di Jetpack!"; +/* Primary button title in the migration done screen. */ +"migrationDone.actions.primaryTitle" = "Ayo"; + /* Description for the static screen displayed prompting users to switch the Jetpack app. */ "movedToJetpack.description" = "Aplikasi Jetpack memiliki semua fungsionalitas aplikasi WordPress, dan sekarang memiliki akses eksklusif ke Statistik, Pembaca, Notifikasi, dan lainnya."; @@ -10610,6 +10519,30 @@ Example: Reply to Pamela Nguyen */ /* Message title for when a user has no sites. */ "mySite.noSites.title" = "Anda belum memiliki situs"; +/* Menu title for the add site option */ +"mySite.siteActions.addSite" = "Tambahkan situs"; + +/* Button that reveals more site actions */ +"mySite.siteActions.button" = "Tindakan di Situs"; + +/* Accessibility hint for button used to show more site actions */ +"mySite.siteActions.hint" = "Ketuk untuk menunjukkan tindakan lain di situs"; + +/* Menu title for the personalize home option */ +"mySite.siteActions.personalizeHome" = "Personalisasikan beranda"; + +/* Menu title for the change site icon option */ +"mySite.siteActions.siteIcon" = "Ubah ikon situs"; + +/* Menu title for the change site title option */ +"mySite.siteActions.siteTitle" = "Ubah judul situs"; + +/* Menu title for the switch site option */ +"mySite.siteActions.switchSite" = "Alihkan sistus"; + +/* Menu title for the visit site option */ +"mySite.siteActions.visitSite" = "Kunjungi situs"; + /* Dismiss button title. */ "noResultsViewController.dismissButton" = "Tutup"; @@ -10625,14 +10558,20 @@ Example: Reply to Pamela Nguyen */ /* This is one of the buttons we display when prompting the user for a review */ "notifications.appRatings.sendFeedback.yes.buttonTitle" = "Beri masukan"; -/* Word separating the current index from the total amount. I.e.: 7 of 9 */ -"of" = "dari"; +/* Badge for page cells */ +"pageList.badgeHomepage" = "Beranda"; + +/* Badge for page cells */ +"pageList.badgeLocalChanges" = "Perubahan lokal"; -/* Label displayed on media items that are not video, image, or audio. */ -"other" = "lainnya"; +/* Badge for page cells */ +"pageList.badgePendingReview" = "Menunggu peninjauan"; -/* Promote the page with Blaze. */ -"pages.blaze.actionTitle" = "Promosikan dengan Blaze"; +/* Badge for page cells */ +"pageList.badgePosts" = "Halaman pos"; + +/* Badge for page cells */ +"pageList.badgePrivate" = "Pribadi"; /* Subtitle of the theme template homepage cell */ "pages.template.subtitle" = "Beranda Anda menggunakan templat Tema dan akan dibuka di editor web."; @@ -10640,6 +10579,36 @@ Example: Reply to Pamela Nguyen */ /* Title of the theme template homepage cell */ "pages.template.title" = "Beranda"; +/* Message informing the user that their static homepage page was set successfully */ +"pages.updatePage.successTitle" = "Halaman berhasil diperbarui."; + +/* Delete option in the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.actionTitle" = "Hapus Secara Permanen"; + +/* Message of the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.alertMessage" = "Anda yakin ingin menghapus halaman ini secara permanen?"; + +/* Title of the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.alertTitle" = "Hapus Permanen?"; + +/* Menu option for filtering posts by everyone */ +"pagesList.pagesByEveryone" = "Halaman oleh setiap orang"; + +/* Menu option for filtering posts by me */ +"pagesList.pagesByMe" = "Halaman oleh saya"; + +/* Trash option in the trash page confirmation alert. */ +"pagesList.trash.actionTitle" = "Pindahkan ke Tempat Sampah"; + +/* Message of the trash page confirmation alert. */ +"pagesList.trash.alertMessage" = "Apakah Anda ingin membuang halaman ini?"; + +/* Title of the trash page confirmation alert. */ +"pagesList.trash.alertTitle" = "Hapus halaman ini?"; + +/* Cancels an Action */ +"pagesList.trash.cancel" = "Batal"; + /* No comment provided by engineer. */ "password" = "Kata sandi"; @@ -10679,6 +10648,51 @@ Example: Reply to Pamela Nguyen */ /* Register Domain - Domain contact information field Phone */ "phone number" = "nomor telepon"; +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.createdTimeAgo" = "Dibuat %@"; + +/* Status mesasge for post cells */ +"post.deletingPostPermanentlyStatusMessage" = "Menghapus pos..."; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.editedTimeAgo" = "Diedit %@"; + +/* Status mesasge for post cells */ +"post.movingToTrashStatusMessage" = "Memindahkan ke tempat sampah..."; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.publishedTimeAgo" = "Dipublikasikan %@"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.scheduledForDate" = "Terjadwal %@"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.trashedTimeAgo" = "Dibuang %@"; + +/* Accessibility label for the post author in the post list. The parameter is the author name. For example, \"By Elsa.\" */ +"postList.a11y.authorChunkFormat" = "Oleh %@"; + +/* Accessibility label for a post's excerpt in the post list. The parameter is the post excerpt. For example, \"Excerpt. This is the first paragraph.\" */ +"postList.a11y.exerptChunkFormat" = "Kutipan %@"; + +/* Accessibility label for a sticky post in the post list. */ +"postList.a11y.sticky" = "Melekat"; + +/* Accessibility label for a post in the post list. The first placeholder is the post title. The second placeholder is the date. */ +"postList.a11y.titleAndDateChunkFormat" = "%1$@, %2$@."; + +/* Title for the 'Trash' post list row swipe action */ +"postList.swipeActionDelete" = "Tempat Sampah"; + +/* Title for the 'Delete' post list row swipe action */ +"postList.swipeActionDeletePermanently" = "Hapus"; + +/* Title for the 'Share' post list row swipe action */ +"postList.swipeActionShare" = "Bagikan"; + +/* Title for the 'View' post list row swipe action */ +"postList.swipeActionView" = "Lihat"; + /* User action to dismiss featured media options. */ "postSettings.featuredImageUploadActionSheet.dismiss" = "Tutup"; @@ -10697,9 +10711,84 @@ Example: Reply to Pamela Nguyen */ /* Button in Post Settings */ "postSettings.setFeaturedImageButton" = "Atur sebagai Gambar Andalan"; +/* Error message on post/page settings screen */ +"postSettings.updateFailedMessage" = "Gagal memperbarui pengaturan pos"; + /* Promote the post with Blaze. */ "posts.blaze.actionTitle" = "Promosikan dengan Blaze"; +/* Label for the Post List option that cancels automatic uploading of a post. */ +"posts.cancelUpload.actionTitle" = "Batalkan unggahan"; + +/* Label for post comments option. Tapping displays comments for a post. */ +"posts.comments.actionTitle" = "Komentar"; + +/* Label for the delete post option. Tapping permanently deletes a post. */ +"posts.delete.actionTitle" = "Hapus secara permanen"; + +/* Label for an option that moves a post to the draft folder */ +"posts.draft.actionTitle" = "Pindahkan ke draf"; + +/* Label for post duplicate option. Tapping creates a copy of the post. */ +"posts.duplicate.actionTitle" = "Duplikasi"; + +/* Opens a submenu for page attributes. */ +"posts.pageAttributes.actionTitle" = "Atribut halaman"; + +/* Label for the preview post button. Tapping displays the post as it appears on the web. */ +"posts.preview.actionTitle" = "Pratinjau"; + +/* Label for an option that moves a publishes a post immediately */ +"posts.publish.actionTitle" = "Publikasikan sekarang"; + +/* Retry uploading the post. */ +"posts.retry.actionTitle" = "Coba ulang"; + +/* Set the selected page as the homepage. */ +"posts.setHomepage.actionTitle" = "Atur sebagai beranda"; + +/* Set the parent page for the selected page. */ +"posts.setParent.actionTitle" = "Atur induk"; + +/* Set the selected page as a posts page. */ +"posts.setPostsPage.actionTitle" = "Atur sebagai halaman pos"; + +/* Set the selected page as a regular page. */ +"posts.setRegularPage.actionTitle" = "Tetapkan sebagai halaman biasa"; + +/* Label for post settings option. Tapping displays settings for a post. */ +"posts.settings.actionTitle" = "Pengaturan"; + +/* Share the post. */ +"posts.share.actionTitle" = "Bagikan"; + +/* Label for post stats option. Tapping displays statistics for a post. */ +"posts.stats.actionTitle" = "Statistik"; + +/* Label for a option that moves a post to the trash folder */ +"posts.trash.actionTitle" = "Pindahkan ke tempat sampah"; + +/* Label for the view post button. Tapping displays the post as it appears on the web. */ +"posts.view.actionTitle" = "Lihat"; + +/* A short message explaining that a page was deleted permanently. */ +"postsList.deletePage.message" = "Halaman yang dihapus permanen"; + +/* A short message explaining that a post was deleted permanently. */ +"postsList.deletePost.message" = "Pos yang dihapus permanen"; + +/* A short message explaining that a page was moved to the trash bin. */ +"postsList.movePageToTrash.message" = "Halaman dipindahkan ke tempat sampah"; + +/* A short message explaining that a post was moved to the trash bin. */ +"postsList.movePostToTrash.message" = "Pos dipindahkan ke tempat sampah"; + +/* Menu option for filtering posts by everyone */ +"postsList.postsByEveryone" = "Pos oleh setiap orang"; + +/* Menu option for filtering posts by me */ +"postsList.postsByMe" = "Pos oleh saya"; + /* Title for the button to subscribe to Jetpack Social on the remaining shares view */ "postsettings.social.remainingshares.subscribe" = "Langganan sekarang untuk kesempatan berbagi lebih banyak"; @@ -10844,13 +10933,15 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Text for the 'Like' button on the reader post card cell. */ "reader.post.button.like" = "Suka"; -/* Accessibility hint for the like button on the reader post card cell - Accessibility hint for the liked button on the reader post card cell */ +/* Accessibility hint for the like button on the reader post card cell */ "reader.post.button.like.accessibility.hint" = "Menyukai pos."; /* Text for the 'Liked' button on the reader post card cell. */ "reader.post.button.liked" = "Disukai"; +/* Accessibility hint for the liked button on the reader post card cell */ +"reader.post.button.liked.accessibility.hint" = "Tidak menyukai pos."; + /* Accessibility hint for the site header on the reader post card cell */ "reader.post.button.menu.accessibility.hint" = "Membuka menu dengan tindakan lain."; @@ -10914,6 +11005,12 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Title for the tooltip anchor. */ "readerDetail.tooltipAnchorTitle.accessibilityLabel" = "Baru"; +/* The button title for the transfer footer view in Register Domain screen */ +"register.domain.transfer.button.title" = "Transfer domain"; + +/* The title for the transfer footer view in Register Domain screen */ +"register.domain.transfer.title" = "Ingin mentransfer domain Anda?"; + /* Information of what related post are and how they are presented */ "relatedPostsSettings.optionsFooter" = "Pos Terkait menampilkan konten yang relevan dari situs Anda di bawah pos Anda."; @@ -10926,6 +11023,9 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Text for related post cell preview */ "relatedPostsSettings.preview2.details" = "dalam \"Aplikasi\""; +/* Text for related post cell preview */ +"relatedPostsSettings.preview2.title" = "Aplikasi WordPress for Android Kini Tampil Beda"; + /* Text for related post cell preview */ "relatedPostsSettings.preview3.details" = "dalam \"Upgrade\""; @@ -11010,6 +11110,21 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Accessibility hint for actions when displaying media items. */ "siteMedia.cellAccessibilityHint" = "Pilih media."; +/* Accessibility hint for media item preview for user's viewing an item in their media library */ +"siteMediaItem.contentViewAccessibilityHint" = "Ketuk untuk melihat media dalam layar penuh"; + +/* Accessibility label for media item preview for user's viewing an item in their media library */ +"siteMediaItem.contentViewAccessibilityLabel" = "Pratinjau media"; + +/* Title for confirmation navigation bar button item */ +"siteMediaPicker.add" = "Tambah"; + +/* Button selection media in media picker */ +"siteMediaPicker.deselect" = "Batalkan Pilihan"; + +/* Button selection media in media picker */ +"siteMediaPicker.select" = "Pilih"; + /* Media screen navigation title */ "siteMediaPicker.title" = "Media"; @@ -11017,7 +11132,7 @@ Example: given a notice format "Following %@" and empty site name, this will be "siteSettings.privacy.title" = "Privasi"; /* Hint for users when hidden privacy setting is set */ -"siteVisibility.hidden.hint" = "Situs Anda dapat dilihat oleh semua orang, tetapi Anda meminta mesin pencari untuk tidak mengindeks situs Anda."; +"siteVisibility.hidden.hint" = "Situs Anda belum dapat dilihat pengunjung dan masih bertuliskan “Akan Segera Hadir”."; /* Text for privacy settings: Hidden */ "siteVisibility.hidden.title" = "Tersembunyi"; @@ -11178,6 +11293,12 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Dismiss the AlertView */ "stockPhotos.strings.dismiss" = "Tutup"; +/* Subtitle for placeholder in Free Photos. The company name 'Pexels' should always be written as it is. */ +"stockPhotos.subtitle" = "Foto disediakan oleh Pexels"; + +/* Title for placeholder in Free Photos */ +"stockPhotos.title" = "Cari foto gratis untuk ditambahkan ke Pustaka Media Anda!"; + /* Section title for prominent suggestions */ "suggestions.section.prominent" = "Dalam percakapan ini"; @@ -11325,6 +11446,9 @@ Example: given a notice format "Following %@" and empty site name, this will be /* View title for Help & Support page. */ "support.title" = "Bantuan"; +/* Title for placeholder in Tenor picker */ +"tenor.welcomeMessage" = "Cari GIF untuk ditambahkan ke Pustaka Media Anda!"; + /* Header of delete screen section listing things that will be deleted. */ "these items will be deleted:" = "item berikut akan dihapus:"; @@ -11340,9 +11464,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Displayed in the confirmation alert when marking unread notifications as read. */ "unread" = "belum dibaca"; -/* Label displayed on video media items. */ -"video" = "video"; - /* Portion of a message for Jetpack users that have multisite WP installation, thus Restore is not available. This part is a link, colored with a different color. */ "visit our documentation page" = "kunjungi halaman dokumentasi kami"; diff --git a/WordPress/Resources/is.lproj/Localizable.strings b/WordPress/Resources/is.lproj/Localizable.strings index f043238d0cc6..3ba79578486b 100644 --- a/WordPress/Resources/is.lproj/Localizable.strings +++ b/WordPress/Resources/is.lproj/Localizable.strings @@ -54,9 +54,6 @@ /* Age between dates over one year. */ "%d years" = "%d ár"; -/* Max image size in pixels (e.g. 300x300px) */ -"%dx%dpx" = "%1$dx%2$dpx"; - /* One menu area available in the theme */ "%i menu area in this theme" = "%i valmyndarsvæði í þessu þema"; @@ -112,10 +109,6 @@ /* No comment provided by engineer. */ "Activity Logs" = "Aðgerðaskrá"; -/* Accessibility label for add button to add items to the user's media library - Remove asset from media picker list */ -"Add" = "Bæta við"; - /* The title on the add category screen */ "Add a Category" = "Bæta við flokki"; @@ -141,9 +134,6 @@ /* Title for the advanced section in site settings screen */ "Advanced" = "Ítarlegra"; -/* Description of albums in the photo libraries */ -"Albums" = "Albúm"; - /* Image alignment option title. Title of the screen for choosing an image's alignment. */ "Alignment" = "Jöfnun"; @@ -236,9 +226,6 @@ /* Menus confirmation text for confirming if a user wants to delete a menu. */ "Are you sure you want to delete the menu?" = "Ertu viss um að þú viljir eyða valmyndinni?"; -/* Message prompting the user to confirm that they want to permanently delete a group of media items. */ -"Are you sure you want to permanently delete these items?" = "Ertu viss um að þú viljir eyða þessum hlutum varanlega?"; - /* Message prompting the user to confirm that they want to permanently delete a media item. Should match Calypso. */ "Are you sure you want to permanently delete this item?" = "Ertu viss um að þú viljir eyða þessum hlut varanlega?"; @@ -321,8 +308,7 @@ /* Label for size of media while it's being calculated. */ "Calculating..." = "Reikna..."; -/* Accessibility label for taking an image or video with the camera on formatting toolbar. - Accessibility label for the camera tile in the collection view */ +/* Accessibility label for taking an image or video with the camera on formatting toolbar. */ "Camera" = "Myndavél"; /* Title of an alert letting the user know */ @@ -364,10 +350,7 @@ Cancels an Action Cancels an alert. Cancels the mark all as read action. - Dismiss the post action sheet Editing GIF alert cancel action button. - Label for a cancel button - Label for the auto-upload cancelation button in the post list. Tapping will prevent the app from auto-uploading the post. Menus cancel button for deleting a menu. Menus cancel button within text bar while editing items. Menus: Cancel button title for canceling an edited menu item. @@ -607,9 +590,6 @@ /* No comment provided by engineer. */ "Couldn't Connect" = "Gat ekkki tengst"; -/* Message to show while media data source is finding the number of items available. */ -"Counting media items..." = "Tel fjölda skráa..."; - /* Period Stats 'Countries' header */ "Countries" = "Lönd"; @@ -673,7 +653,6 @@ /* Delete Delete button title for the warning shown to the user when he refuses to re-login when the authToken is missing. Title for button that permanently deletes a media item (photo / video) - Title for button that permanently deletes one or more media items (photos / videos) Title of a delete button Title of the trash confirmation alert. */ "Delete" = "Eyða"; @@ -681,10 +660,7 @@ /* Menus confirmation button for deleting a menu. */ "Delete Menu" = "Eyða valmynd"; -/* Delete option in the confirmation alert when deleting a page from the trash. - Delete option in the confirmation alert when deleting a post from the trash. - Label for a button permanently deletes a page. - Label for the delete post option. Tapping permanently deletes a post. +/* Delete option in the confirmation alert when deleting a post from the trash. Title for button on the comment details page that deletes the comment when tapped. */ "Delete Permanently" = "Eyða varanlega"; @@ -758,7 +734,6 @@ /* Accessibility label for button to dismiss a bottom sheet Accessibility label for the transparent space above the login dialog which acts as a button to dismiss the dialog. Accessibility label for the transparent space above the signup dialog which acts as a button to dismiss the dialog. - Action to show on alert when view asset fails. Dismiss a view. Verb */ "Dismiss" = "Loka"; @@ -791,13 +766,9 @@ /* Name for the status of a draft post. */ "Draft" = "Drög"; -/* Accessibility label for edit button to enable multi selection mode in the user's media library - Edit the post. - Editing GIF alert default action button. +/* Editing GIF alert default action button. Edits a Comment Edits the comment - Label for a button that opens the Edit Page view controller - Label for the edit post button. Tapping displays the editor. User action to edit media details. Verb, edit a comment */ "Edit" = "Breyta"; @@ -886,9 +857,6 @@ /* Title of error dialog when removing a site owner fails. */ "Error removing %@" = "Villa við að fjarlægja %@"; -/* Label for the post author filter. This filter shows posts for all users on the blog. */ -"Everyone" = "Allir"; - /* Label for the excerpt field. Should be the same as WP core. */ "Excerpt" = "Útdráttur"; @@ -1097,9 +1065,6 @@ /* Hint for image title on image settings. */ "Image title" = "Titill myndar"; -/* Accessibility label for image thumbnails in the media collection view. The parameter is the creation date of the image. */ -"Image, %@" = "Mynd, %@"; - /* Undated post time label */ "Immediately" = "Strax"; @@ -1344,7 +1309,6 @@ "Max Image Upload Size" = "Hámarksstærð myndar til upphleðslu"; /* Accessibility label for the Me button in My Site. - Label for the post author filter. This filter shows posts only authored by the current user. Me page title Noun. Title. Links to the Me screen. The accessibility value of the me tab. @@ -1352,9 +1316,7 @@ "Me" = "Ég"; /* Noun. Title. Links to the blog's Media library. - Tab bar title for the Media tab in Media Picker The menu item to select during a guided tour. - Title for Media Library section of the app. Title for the media section in site settings screen Title label for the media settings section in the app settings */ "Media" = "Skrár"; @@ -1362,13 +1324,6 @@ /* Label for size of media cache in the app. */ "Media Cache Size" = "Stærð skyndiminni fyrir skrár"; -/* Title for alert when access to media capture is not granted */ -"Media Capture" = "Aðgengi að margmiðlunarefni"; - -/* Title for alert when a generic error happened when loading media - Title for alert when access to the media library is not granted by the user */ -"Media Library" = "Margmiðlunarsafn"; - /* Title for action sheet with media options. */ "Media Options" = "Valkostir skráa"; @@ -1379,9 +1334,6 @@ /* Message to indicate progress of uploading media to server */ "Media Uploading" = "Hleð upp"; -/* Alert title when there is issues loading an asset to preview. */ -"Media preview failed." = "Ekki tókst að forskoða skrá."; - /* Medium image size. Should be the same as in core WP. */ "Medium" = "Miðlungi gott"; @@ -1401,9 +1353,6 @@ Label for the share message field on the post settings. */ "Message" = "Skilaboð"; -/* Title of section containing image / video metadata such as size and file type */ -"Metadata" = "Lýsigögn"; - /* Title of Months stats filter. */ "Months" = "Mánuðir"; @@ -1411,28 +1360,19 @@ "Months and Years" = "Máðuðir og ár"; /* Accessibility label for more button in dashboard quick start card. - Accessibility label for the More button in Page List. Accessibility label for the More button in Post List (compact view). Accessibility label for the More button on formatting toolbar. Accessibility label for the More button on Reader Cell Accessibility label for the More button on Reader's post details - Action button to display more available options - Label for the more post button. Tapping displays an action sheet with post options. */ + Action button to display more available options */ "More" = "Meira"; /* Action button to display more available options Label for the More Options area in post settings. Should use the same translation as core WP. */ "More Options" = "Fleiri stillingar"; -/* Label for a button that moves a page to the draft folder - Label for an option that moves a post to the draft folder */ -"Move to Draft" = "Flytja í drög"; - -/* Label for a button that moves a page to the trash folder - Label for a option that moves a post to the trash folder - Title for button on the comment details page that moves the comment to trash when tapped. +/* Title for button on the comment details page that moves the comment to trash when tapped. Trash option in the trash confirmation alert. - Trash option in the trash page confirmation alert. Trashes the comment */ "Move to Trash" = "Flytja í rusl"; @@ -1440,7 +1380,8 @@ My Profile view title */ "My Profile" = "Minn prófíll"; -/* Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ +/* Title for site picker screen. + Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ "My Sites" = "Vefirnir mínir"; /* 'Need help?' button label, links off to the WP for iOS FAQ. */ @@ -1576,7 +1517,6 @@ Alert dismissal title Button title. Acknowledges a prompt. Button title. An acknowledgement of the message displayed in a prompt. - Confirmation of action Default action Dismisses the alert Marks all notifications as read. @@ -1681,18 +1621,6 @@ Title shown when selecting a post type of Page from the Share Extension. */ "Page" = "Síða"; -/* Prompts the user that a restored page was moved to the drafts list. */ -"Page Restored to Drafts" = "Síða endurvakin sem drög"; - -/* Prompts the user that a restored page was moved to the published list. */ -"Page Restored to Published" = "Síða endurvakin sem birt"; - -/* Prompts the user that a restored page was moved to the scheduled list. */ -"Page Restored to Scheduled" = "Síða endurvakin sem tímasett"; - -/* A short message explaining that a page was moved to the trash bin. */ -"Page moved to trash." = "Síðu hent í ruslið."; - /* Noun. Title. Links to the blog's Pages screen. The item to select during a guided tour. This is the section title @@ -1725,8 +1653,7 @@ Title of pending Comments filter. */ "Pending" = "Í bið"; -/* Name for the status of a post pending review. - Title of the Pending Review Badge */ +/* Name for the status of a post pending review. */ "Pending review" = "Bíður yfirlestrar"; /* Noun. Title of the people management feature. @@ -1790,18 +1717,6 @@ The post formats available for the post. Should be the same as in core WP. */ "Post Format" = "Færslusnið"; -/* Prompts the user that a restored post was moved to the drafts list. */ -"Post Restored to Drafts" = "Færsla endurvakin sem drög"; - -/* Prompts the user that a restored post was moved to the published list. */ -"Post Restored to Published" = "Færsla endurvakin sem birt"; - -/* Prompts the user that a restored post was moved to the scheduled list. */ -"Post Restored to Scheduled" = "Færsla endurvakin sem tímasett"; - -/* A short message explaining that a post was moved to the trash bin. */ -"Post moved to trash." = "Færslu hent í ruslið."; - /* Insights 'Posting Activity' header Title for stats Posting Activity view. */ "Posting Activity" = "Færsluvirkni"; @@ -1846,8 +1761,7 @@ "Privacy Policy" = "Persónuverndarstefna"; /* Name for the status of a post that is marked private. - Privacy setting for posts set to 'Private'. Should be the same as in core WP. - Title of the Private Badge */ + Privacy setting for posts set to 'Private'. Should be the same as in core WP. */ "Private" = "Einkamál"; /* Message for the warning shown to the user when he refuses to re-login when the authToken is missing. */ @@ -1867,19 +1781,16 @@ Title for the publish settings view */ "Publish" = "Birta"; -/* A short phrase indicating a post is due to be immedately published. - Label for a button that moves a page to the published folder, publishing with the current date/time. */ +/* A short phrase indicating a post is due to be immedately published. */ "Publish Immediately" = "Birta strax"; /* Label for a button that publishes the post - Label for an option that moves a publishes a post immediately Title of button allowing the user to immediately publish the post they are editing. */ "Publish Now" = "Birta núna"; /* Name for the status of a published post. Period Stats 'Published' header - Title of the published filter. This filter shows a list of posts that the user has published. - Title of the published header in search list. */ + Title of the published filter. This filter shows a list of posts that the user has published. */ "Published" = "Birt"; /* Precedes the name of the blog just posted on */ @@ -1950,8 +1861,7 @@ /* Label for selecting the related posts options */ "Related Posts" = "Tengdar greinar"; -/* Add asset to media picker list - Alert button to confirm a plugin to be removed +/* Alert button to confirm a plugin to be removed Button label when removing a blog Label for the Remove Feature Image icon. Tapping will show a confirmation screen for removing the feature image from the post. Remove Action @@ -2004,9 +1914,6 @@ /* Setting: WordPress.com Surveys */ "Research" = "Rannsóknir"; -/* Title of the reset button */ -"Reset" = "Endurstilla"; - /* Screen title. Resize and crop an image. */ "Resize & Crop" = "Minnka & skera til"; @@ -2024,12 +1931,9 @@ Button title, displayed when media has failed to upload. Allows the user to try the upload again. Button title. Retries uploading a post. If a user taps the button with this label, the action that evinced this error view will be retried. - Label for a button that attempts to re-upload a page that previously failed to upload. - Label for the retry post upload button. Tapping attempts to upload the post again. Opens the media library . Retries the upload of a user's post. Retry updating User's Role - Retry uploading the post. Retry. Action Retry. Verb – retry a failed media upload. The Jetpack view button title used when an error occurred @@ -2038,9 +1942,6 @@ User action to retry media upload. */ "Retry" = "Reyna aftur"; -/* User action to retry media upload. */ -"Retry Upload" = "Reyna upphal aftur"; - /* No comment provided by engineer. */ "Retry?" = "Reyna aftur?"; @@ -2077,11 +1978,7 @@ /* Title of button allowing users to change the status of the post they are currently editing to Draft. */ "Save as Draft" = "Vista sem drög"; -/* Text displayed in HUD when a media item's metadata (title, etc) is saved successfully. */ -"Saved!" = "Vistað!"; - /* Menus save button title while it is saving a Menu. - Text displayed in HUD while a media item's metadata (title, etc) is being saved. Text displayed in HUD while a post is being saved as a draft. */ "Saving..." = "Vista..."; @@ -2167,7 +2064,6 @@ /* Accessibility label for share buttons in nav bars Button label to share a post Button label to share a web page - Share the post. Shares the comment URL Spoken accessibility label Title for a button that allows the user to share their answer to the prompt. @@ -2326,8 +2222,7 @@ Title of Start Over settings page */ "Start Over" = "Byrja upp á nýtt"; -/* Label for post stats option. Tapping displays statistics for a post. - Noun. Abbreviation of Statistics. Name of the Stats feature +/* Noun. Abbreviation of Statistics. Name of the Stats feature Noun. Abbv. of Statistics. Links to a blog's Stats screen. Stats 3D Touch Shortcut Stats window title @@ -2360,7 +2255,7 @@ Theme Support action title */ "Support" = "Aðstoð"; -/* Button used to switch site */ +/* Title for back button that leads to the site picker screen. */ "Switch Site" = "Skipta um vef"; /* Menu item label for linking a specific tag. @@ -2473,7 +2368,7 @@ /* Error message shown when user attempts to remove the site owner. */ "The user you are trying to remove is the owner of this site. Please contact support for assistance." = "Notandinn sem þú ert að reyna að fjarlægja er eigandi vefsins. Vinsamlegast hafðu samband við þjónustuborð fyrir aðstoð."; -/* Error message informing a user about an invalid password. */ +/* No comment provided by engineer. */ "The username or password stored in the app may be out of date. Please re-enter your password in the settings and try again." = "Notandanafnið eða lykilorðið sem geymt er af forritinu gæti verið útrunnið. Vinsamlegast sláðu aftur inn lykilorðið þitt í stillingum og reyndu aftur."; /* Title of alert when theme activation succeeds */ @@ -2512,9 +2407,6 @@ /* A short error message letting the user know about a problem displaying a post. */ "There was a problem displaying this post." = "Það kom upp vandamál við að sýna þessa færslu."; -/* Error message displayed when the Media Library is unable to load a full sized preview of an item. */ -"There was a problem loading the media item." = "Það kom upp vandamál með að hlaða skrána."; - /* Error message informing the user that there was a problem clearing the block on site preventing its posts from displaying in the reader. */ "There was a problem removing the block for specified site." = "Það kom upp vandamál við að fjarlægja lokun fyrir þennan vef."; @@ -2524,9 +2416,6 @@ /* Explaining to the user there was an error trying to obtain the current location of the user. */ "There was a problem when trying to access your location. Please try again later." = "Það kom upp vandamál við að nálgast upplýsingar um staðsetningu þína. Vinsamlegast reynið aftur síðar."; -/* Explaining to the user there was an generic error accesing media. */ -"There was a problem when trying to access your media. Please try again later." = "Það kom upp vandamál við að nálgast skrárnar þínar. Vinsamlegast reynið aftur síðar."; - /* Text displayed when there is a failure loading the plan list */ "There was an error loading plans" = "Það kom upp villa við að hlaða inn áskriftarleiðum"; @@ -2542,9 +2431,6 @@ /* An error message display if the users device does not have a camera input available */ "This app needs permission to access the Camera to capture new media, please change the privacy settings if you wish to allow this." = "Þetta forrit þarf heimild til þess að komast í myndavélina og taka nýjar myndir\/myndbönd, vinsamlegast breyttu persónuverndarstillingum ef þú vilt leyfa þetta."; -/* Explaining to the user why the app needs access to the device media library. */ -"This app needs permission to access your device media library in order to add photos and\/or video to your posts. Please change the privacy settings if you wish to allow this." = "Þetta forrit þarf heimild til þess að komast í skrárnar þínar og setja inn myndir\/myndbönd í færslurnar þínar. Vinsamlegast breyttu persónuverndarstillingum ef þú vilt leyfa þetta."; - /* Message displayed in Media Library if the user attempts to edit a media asset (image / video) after it has been deleted. */ "This media item has been deleted." = "Þessari skrá hefur verið eytt."; @@ -2607,8 +2493,7 @@ /* Label displaying total number of WordPress.com followers. %@ is the total. */ "Total WordPress.com Followers: %@" = "Heildarfjöldi WordPress.com fylgjenda: %@"; -/* Accessibility label for trash button to delete items from the user's media library - Accessibility label for trash buttons in nav bars +/* Accessibility label for trash buttons in nav bars Trashes a comment Trashes the comment */ "Trash" = "Rusl"; @@ -2645,18 +2530,12 @@ /* Menus label for describing which menu the location uses in the header. */ "USES" = "NOTAR"; -/* An error message. */ -"Unable to Connect" = "Gat ekki tengst"; - /* Title of a prompt saying the app needs an internet connection before it can load posts */ "Unable to Load Posts" = "Gat ekki hlaðið færslur"; /* Title of error prompt shown when a sync the user initiated fails. */ "Unable to Sync" = "Gat ekki samstillt"; -/* Text displayed in HUD if there was an error attempting to delete a group of media items. */ -"Unable to delete all media items." = "Gat ekki eytt öllum skrám."; - /* Text displayed in HUD if there was an error attempting to delete a media item. */ "Unable to delete media item." = "Gat ekki eytt skrá."; @@ -2687,8 +2566,6 @@ Button title. Reverts the previous notification operation Revert an operation Revert enabling notification after successfully subcribing to the comments for the post. - The title of an 'undo' button. Tapping the button moves a trashed page out of the trash folder. - The title of an 'undo' button. Tapping the button moves a trashed post out of the trash folder. Undo action */ "Undo" = "Afturkalla"; @@ -2794,15 +2671,10 @@ /* Push Authentication Alert Title */ "Verify Log In" = "Staðfesta innskráningu"; -/* Accessibility label for video thumbnails in the media collection view. The parameter is the creation date of the video. */ -"Video, %@" = "Myndband, %@"; - /* Period Stats 'Videos' header */ "Videos" = "Myndbönd"; /* Button title. Displays a summary / sharing screen for a specific post. - Label for a button that opens the page when tapped. - Label for the view post button. Tapping displays the post as it appears on the web. Opens the post epilogue screen to allow sharing / viewing of a post. Theme View action title Verb. The screen title shown when viewing a post inside the app. */ diff --git a/WordPress/Resources/it.lproj/Localizable.strings b/WordPress/Resources/it.lproj/Localizable.strings index 39b9025c672f..d20331712d87 100644 --- a/WordPress/Resources/it.lproj/Localizable.strings +++ b/WordPress/Resources/it.lproj/Localizable.strings @@ -1,4 +1,4 @@ -/* Translation-Revision-Date: 2023-10-18 14:54:08+0000 */ +/* Translation-Revision-Date: 2024-01-04 11:54:09+0000 */ /* Plural-Forms: nplurals=2; plural=n != 1; */ /* Generator: GlotPress/4.0.0-alpha.11 */ /* Language: it */ @@ -7,7 +7,7 @@ "\nPlease send us an email to have your content cleared out." = "\nVi preghiamo di inviarci una email per ottenere la cancellazione dei vostri contenuti."; /* Notice shown on the Edit Comment view when the author is a registered user. */ -"\nThis user is registered. Their name, web address, and email address cannot be edited." = "\nQuesto utente è registrato. Il nome, l'indirizzo web e l'indirizzo e-mail non possono essere modificati."; +"\nThis user is registered. Their name, web address, and email address cannot be edited." = "\nQuesto utente è registrato. Il nome, l'indirizzo web e l'indirizzo email non possono essere modificati."; /* Message of Delete Site confirmation alert; substitution is site's host. */ "\nTo confirm, please re-enter your site's address before deleting.\n\n" = "\nPer confermare, si prega di reinserire l'indirizzo del sito da rimuovere.\n"; @@ -152,9 +152,6 @@ /* Difference label for Period Overview stat, indicating change from previous period. Ex: +99.9K (5%) */ "%@%@ (%@%%)" = "%1$@%2$@ (%3$@%%)"; -/* Accessibility label for a post in the post list. The parameters are the title, and date respectively. For example, \"Let it Go, 1 hour ago.\" */ -"%@, %@." = "%1$@, %2$@."; - /* Accessibility value for a Stats' Posting Activity Month if the user selected a day with posts. The first parameter is day (e.g. November 2019). The second parameter is the number of posts. */ "%@. %d posts." = "%1$@. %2$d articoli."; @@ -210,9 +207,6 @@ /* Age between dates over one year. */ "%d years" = "%d anni"; -/* Max image size in pixels (e.g. 300x300px) */ -"%dx%dpx" = "%1$dx%2$dpx"; - /* One menu area available in the theme */ "%i menu area in this theme" = "%i area menu in questo tema"; @@ -271,6 +265,9 @@ translators: Block name. %s: The localized block name */ /* translators: %s: social link name e.g: \"Instagram\". */ "%s social icon" = "Icona social %s"; +/* translators: displayed right after the classic block is converted to blocks. %s: The localized classic block name */ +"'%s' block converted to blocks" = "Blocco \"%s\" convertito in blocchi"; + /* translators: Missing block alert title. %s: The localized block name */ "'%s' is not fully-supported" = "'%s' non è pienamente supportato"; @@ -365,7 +362,7 @@ translators: Block name. %s: The localized block name */ "A \"more\" button contains a dropdown which displays sharing buttons" = "Un pulsante \"Altro\" contiene un menu a tendina che mostra i pulsanti di condivisione"; /* Instruction text to explain magic link login step. */ -"A WordPress.com account is connected to your store credentials. To continue, we will send a verification link to the email address above." = "Un account WordPress.com è collegato alle credenziali del tuo negozio. Per continuare, invieremo un link di verifica all'indirizzo e-mail sopra indicato."; +"A WordPress.com account is connected to your store credentials. To continue, we will send a verification link to the email address above." = "Un account WordPress.com è collegato alle credenziali del tuo negozio. Per continuare, invieremo un link di verifica all'indirizzo email sopra indicato."; /* Label for image that is set as a feature image for post/page */ "A featured image is set. Tap to change it." = "È impostata un'immagine in evidenza. Tocca per modificarla."; @@ -478,13 +475,6 @@ translators: Block name. %s: The localized block name */ /* Label for the Activity Type filter button when there are more than 1 activity type selected */ "Activity Type (%1$d)" = "Tipo di attività (%1$d)"; -/* Accessibility label for add button to add items to the user's media library - Remove asset from media picker list */ -"Add" = "Aggiungi"; - -/* Action for Media Picker to indicate selection of media. The argument in the string represents the number of elements (as numeric digits) selected */ -"Add %@" = "Aggiungi %@"; - /* No comment provided by engineer. */ "Add Block After" = "Aggiungi blocco dopo"; @@ -581,9 +571,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Add menu item to children" = "Aggiungi voce di menu in figli"; -/* Accessibility hint for add button to add items to the user's media library */ -"Add new media" = "Aggiungi nuovo elemento multimediale"; - /* Screen reader text for Menus button that adds a new menu to a site. */ "Add new menu" = "Aggiungi nuovo menu"; @@ -610,7 +597,7 @@ translators: Block name. %s: The localized block name */ "Add tags" = "Aggiungi tag"; /* No comment provided by engineer. */ -"Add this email link" = "Aggiungi questo link dell'e-mail"; +"Add this email link" = "Aggiungi questo link dell'email"; /* No comment provided by engineer. */ "Add this link" = "Aggiungi questo link"; @@ -649,9 +636,6 @@ translators: Block name. %s: The localized block name */ /* Option to select the Airmail app when logging in with magic links */ "Airmail" = "Airmail"; -/* Description of albums in the photo libraries */ -"Albums" = "Album"; - /* Image alignment option title. Title of the screen for choosing an image's alignment. */ "Alignment" = "Allineamento"; @@ -665,6 +649,9 @@ translators: Block name. %s: The localized block name */ Title of the drafts filter. This filter shows a list of draft posts. */ "All" = "Tutti"; +/* Information about redeeming domain credit on site dashboard. */ +"All WordPress.com annual plans include a custom domain name. Register your free domain now." = "Tutti i piani WordPress.com annuali comprendono un nome di dominio personalizzato. Registra subito il tuo dominio gratuito."; + /* Footer of the free domain registration section for a paid plan. Information about redeeming domain credit on site dashboard. */ "All WordPress.com plans include a custom domain name. Register your free premium domain now." = "Tutti i piani WordPress.com comprendono un nome di dominio personalizzato. Registra il tuo dominio premium gratuito ora."; @@ -730,10 +717,13 @@ translators: Block name. %s: The localized block name */ "Alt Text" = "Testo Alt (alternativo)"; /* No comment provided by engineer. */ -"Alternatively, you can detach and edit these blocks separately by tapping “Detach patterns”." = "In alternativa, puoi staccare e modificare questi blocchi separatamente toccando “Scollega pattern”."; +"Alternatively, you can convert the content to blocks." = "In alternativa, puoi convertire i contenuti in blocchi."; /* No comment provided by engineer. */ -"Alternatively, you can detach and edit this block separately by tapping “Detach pattern”." = "In alternativa, puoi staccare e modificare questo blocco separatamente toccando “Scollega pattern”."; +"Alternatively, you can detach and edit this block separately by tapping “Detach”." = "In alternativa, puoi scollegare e modificare questo blocco separatamente toccando “Scollega”."; + +/* translators: Alternative option included in a warning related to having blocks deeply nested. */ +"Alternatively, you can flatten the content by ungrouping the block." = "In alternativa, puoi uniformare i contenuti separando il blocco."; /* Instruction text to explain to help users type their password instead of using magic link login option. */ "Alternatively, you may enter the password for this account." = "In alternativa, puoi inserire la password per questo account."; @@ -865,74 +855,65 @@ translators: Block name. %s: The localized block name */ "Approves the Comment." = "Approva il commento."; /* Menus alert message for alerting the user to unsaved changes while trying back out of Menus. */ -"Are you sure you want to cancel and discard changes?" = "Desideri annullare e scartare le modifiche?"; +"Are you sure you want to cancel and discard changes?" = "Vuoi davvero annullare e scartare le modifiche?"; /* Title for the remove site confirmation alert, first %@ will be replaced with the blog url, second %@ will be replaced with iPhone/iPad/iPod Touch */ -"Are you sure you want to continue?\n All site data for %@ will be removed from your %@." = "Sei sicuro di voler continuare?\n Tutti i dati di %1$@ saranno rimossi da %2$@."; +"Are you sure you want to continue?\n All site data for %@ will be removed from your %@." = "Vuoi davvero continuare?\n Tutti i dati di %1$@ saranno rimossi da %2$@."; /* Title for the remove site confirmation alert, %@ will be replaced with iPhone/iPad/iPod Touch */ -"Are you sure you want to continue?\n All site data will be removed from your %@." = "Sei sicuro di voler continuare? \nTutte le informazioni del sito verranno rimosse dal tuo %@."; +"Are you sure you want to continue?\n All site data will be removed from your %@." = "Vuoi davvero continuare? \nTutte le informazioni del sito verranno rimosse dal tuo %@."; /* Menus confirmation text for confirming if a user wants to delete a menu. */ -"Are you sure you want to delete the menu?" = "Sei sicuro di voler cancellare il menu?"; +"Are you sure you want to delete the menu?" = "Vuoi davvero cancellare il menu?"; /* Message asking for confirmation on tag deletion */ -"Are you sure you want to delete this tag?" = "Sei sicuro di voler cancellare questo tag?"; +"Are you sure you want to delete this tag?" = "Vuoi davvero cancellare questo tag?"; /* Message prompting the user to confirm that they want to disconnect Jetpack from the site. */ -"Are you sure you want to disconnect Jetpack from the site?" = "Sei sicuro di voler disconnettere Jetpack dal sito?"; - -/* Message prompting the user to confirm that they want to permanently delete a group of media items. */ -"Are you sure you want to permanently delete these items?" = "Sei sicuro di volere eliminare per sempre questi elementi?"; +"Are you sure you want to disconnect Jetpack from the site?" = "Vuoi davvero disconnettere Jetpack dal sito?"; /* Message prompting the user to confirm that they want to permanently delete a media item. Should match Calypso. */ -"Are you sure you want to permanently delete this item?" = "Desideri eliminare in modo permanente questo elemento?"; - -/* Message of the confirmation alert when deleting a page from the trash. */ -"Are you sure you want to permanently delete this page?" = "Desideri eliminare in modo permanente questa pagina?"; +"Are you sure you want to permanently delete this item?" = "Vuoi davvero eliminare in modo permanente questo elemento?"; /* Message of the confirmation alert when deleting a post from the trash. */ -"Are you sure you want to permanently delete this post?" = "Sei sicuro di voler eliminare definitivamente questo articolo?"; +"Are you sure you want to permanently delete this post?" = "Vuoi davvero eliminare definitivamente questo articolo?"; /* Title of the message shown when the user taps Publish Now while editing a post. Options will be Publish Now and Keep Editing. */ -"Are you sure you want to publish now?" = "Desideri pubblicare ora?"; +"Are you sure you want to publish now?" = "Vuoi davvero pubblicare ora?"; /* Title of the message shown when the user taps Publish in the post list. Title of the message shown when the user taps Publish while editing a post. Options will be Publish and Keep Editing. */ -"Are you sure you want to publish?" = "Desideri pubblicare?"; +"Are you sure you want to publish?" = "Vuoi davvero pubblicare?"; /* Text for the alert to confirm a plugin removal. %1$@ is the plugin name, %2$@ is the site title. */ -"Are you sure you want to remove %1$@ from %2$@?" = "Desideri rimuovere %1$@ da %2$@?"; +"Are you sure you want to remove %1$@ from %2$@?" = "Vuoi davvero rimuovere %1$@ da %2$@?"; /* Text for the alert to confirm a plugin removal. %1$@ is the plugin name. */ -"Are you sure you want to remove %1$@?" = "Desideri rimuovere %1$@?"; +"Are you sure you want to remove %1$@?" = "Vuoi davvero rimuovere %1$@?"; /* Description for the confirm restore action. %1$@ is a placeholder for the selected date. */ -"Are you sure you want to restore your site back to %1$@? This will remove content and options created or changed since then." = "Desideri ripristinare il sito al giorno %1$@? In questo modo verranno rimossi i contenuti e le opzioni creati o modificati da allora."; +"Are you sure you want to restore your site back to %1$@? This will remove content and options created or changed since then." = "Vuoi davvero ripristinare il sito al giorno %1$@? In questo modo verranno rimossi i contenuti e le opzioni creati o modificati da allora."; /* Title of the message shown when the user taps Save as Draft while editing a post. Options will be Save Now and Keep Editing. */ -"Are you sure you want to save as draft?" = "Desideri salvare come bozza?"; +"Are you sure you want to save as draft?" = "Vuoi davvero salvare come bozza?"; /* Title of the message shown when the user taps Save while editing a post. Options will be Save Now and Keep Editing. */ -"Are you sure you want to save?" = "Desideri salvare?"; +"Are you sure you want to save?" = "Vuoi davvero salvare?"; /* Title of message shown when the user taps Schedule while editing a post. Options will be Schedule and Keep Editing */ -"Are you sure you want to schedule?" = "Desideri pianificare?"; +"Are you sure you want to schedule?" = "Vuoi davvero programmarlo?"; /* Title of message shown when user taps submit for review. */ -"Are you sure you want to submit for review?" = "Desideri inviare per la revisione?"; - -/* Message of the trash page confirmation alert. */ -"Are you sure you want to trash this page?" = "Desideri spostare questa pagina nel cestino?"; +"Are you sure you want to submit for review?" = "Vuoi davvero inviare per la revisione?"; /* Message of the trash confirmation alert. */ -"Are you sure you want to trash this post?" = "Sei sicuro di voler eliminare questo articolo?"; +"Are you sure you want to trash this post?" = "Vuoi davvero eliminare questo articolo?"; /* Title of message shown when user taps continue during homepage editing in site creation. */ -"Are you sure you want to update your homepage?" = "Desideri aggiornare la homepage?"; +"Are you sure you want to update your homepage?" = "Vuoi davvero aggiornare la homepage?"; /* Title of message shown when user taps update. */ -"Are you sure you want to update?" = "Desideri aggiornare?"; +"Are you sure you want to update?" = "Vuoi davvero aggiornare?"; /* Title that asks the user if they are the trying to login. %1$@ is a placeholder for the browser name (Chrome/Firefox), %2$@ is a placeholder for the users location */ "Are you trying to log in to %1$@ near %2$@?" = "Stai tentando di accedere a %1$@ nei pressi di %2$@?"; @@ -965,9 +946,6 @@ translators: Block name. %s: The localized block name */ /* translators: accessibility text. Empty Audio caption. */ "Audio caption. Empty" = "Didascalia audio. Vuota"; -/* Accessibility label for audio items in the media collection view. The parameter is the creation date of the audio. */ -"Audio, %@" = "Audio, %@"; - /* No comment provided by engineer. */ "Authenticating" = "Autenticazione in corso"; @@ -1178,10 +1156,7 @@ translators: Block name. %s: The localized block name */ "Blocks menu" = "Menu Blocchi"; /* translators: Warning related to having blocks deeply nested. %d: The deepest nesting level. */ -"Blocks nested deeper than %d levels may not render properly in the mobile editor. For this reason, we recommend flattening the content by ungrouping the block or editing the block using the web editor." = "I blocchi con più di %d livelli di nidificazione potrebbero non essere visualizzati correttamente nell'editor per dispositivi mobili. Per questo motivo, consigliamo di uniformare i contenuti separando il blocco o modificandolo tramite l'editor web."; - -/* translators: Warning related to having blocks deeply nested. %d: The deepest nesting level. */ -"Blocks nested deeper than %d levels may not render properly in the mobile editor. For this reason, we recommend flattening the content by ungrouping the block or editing the block using your web browser." = "I blocchi con più di %d livelli di nidificazione potrebbero non essere visualizzati correttamente nell'editor per dispositivi mobili. Per questo motivo consigliamo di uniformare i contenuti separando il blocco o modificandolo tramite il browser web."; +"Blocks nested deeper than %d levels may not render properly in the mobile editor." = "I blocchi con più di %d livelli di nidificazione potrebbero non essere visualizzati correttamente nell'editor per dispositivi mobili."; /* Title of a button that displays the WordPress.com blog */ "Blog" = "Blog"; @@ -1190,7 +1165,7 @@ translators: Block name. %s: The localized block name */ "Blog post" = "Articolo del blog"; /* Blog's Email Follower Profile. Displayed when the name is empty! */ -"Blog's Email Follower" = "Follower tramite e-mail del blog"; +"Blog's Email Follower" = "Follower tramite email del blog"; /* Blog's Follower Profile. Displayed when the name is empty! */ "Blog's Follower" = "Follower del blog"; @@ -1257,10 +1232,7 @@ translators: Block name. %s: The localized block name */ "Button position" = "Posizione pulsante"; /* Label for the post author in the post detail. */ -"By " = "Di"; - -/* Accessibility label for the post author in the post list. The parameter is the author name. For example, \"By Elsa.\" */ -"By %@." = "Di %@."; +"By " = "Di "; /* Legal disclaimer for logging in. The underscores _..._ denote underline. */ "By continuing, you agree to our _Terms of Service_." = "Continuando, accetti i nostri _Termini di servizio_."; @@ -1281,8 +1253,7 @@ translators: Block name. %s: The localized block name */ /* Label for size of media while it's being calculated. */ "Calculating..." = "Calcolo in corso..."; -/* Accessibility label for taking an image or video with the camera on formatting toolbar. - Accessibility label for the camera tile in the collection view */ +/* Accessibility label for taking an image or video with the camera on formatting toolbar. */ "Camera" = "Fotocamera"; /* Title of an alert informing the user the camera permission for the app is disabled and its needed to proceed */ @@ -1333,10 +1304,7 @@ translators: Block name. %s: The localized block name */ Cancels an Action Cancels an alert. Cancels the mark all as read action. - Dismiss the post action sheet Editing GIF alert cancel action button. - Label for a cancel button - Label for the auto-upload cancelation button in the post list. Tapping will prevent the app from auto-uploading the post. Menus cancel button for deleting a menu. Menus cancel button within text bar while editing items. Menus: Cancel button title for canceling an edited menu item. @@ -1353,10 +1321,6 @@ translators: Block name. %s: The localized block name */ Verb. Title for Jetpack Restore cancel button. */ "Cancel" = "Annulla"; -/* Label for the Post List option that cancels automatic uploading of a post. - Media Library option to cancel an in-progress or failed upload. */ -"Cancel Upload" = "Annulla caricamento"; - /* Menus alert button title to cancel discarding changes and not createa a new menu. Menus alert button title to cancel discarding changes and not select a new menu Menus alert button title to cancel discarding changes and not select a new menu location */ @@ -1415,9 +1379,6 @@ translators: Block name. %s: The localized block name */ Main title */ "Change Password" = "Cambia la password"; -/* Title of button that takes user to the system Settings section for the app */ -"Change Settings" = "Modifica le impostazioni"; - /* Change Username title. */ "Change Username" = "Modifica nome utente"; @@ -1462,16 +1423,16 @@ translators: Block name. %s: The localized block name */ "Check out our top tips to increase your views and traffic" = "Dai un'occhiata ai nostri migliori suggerimenti per aumentare le visualizzazioni e il traffico"; /* The title text on the magic link requested screen. */ -"Check your email on this device!" = "Controlla la tua e-mail su questo dispositivo!"; +"Check your email on this device!" = "Controlla la tua email su questo dispositivo!"; /* Instruction text after a login Magic Link was requested. */ -"Check your email on this device, and tap the link in the email you receive from WordPress.com." = "Controlla la tua casella di posta su questo dispositivo e segui il link contenuto nell'e-mail ricevuta da WordPress.com."; +"Check your email on this device, and tap the link in the email you receive from WordPress.com." = "Controlla la tua casella di posta su questo dispositivo e segui il link contenuto nell'email ricevuta da WordPress.com."; /* Instructional text on how to open the email containing a magic link. */ -"Check your email on this device, and tap the link in the email you received from WordPress.com.\n\nNot seeing the email? Check your Spam or Junk Mail folder." = "Controlla la tua casella di posta su questo dispositivo e segui il link contenuto nell'e-mail ricevuta da WordPress.com.\n\nNon trovi l'e-mail? Controlla la cartella dello spam o della posta indesiderata."; +"Check your email on this device, and tap the link in the email you received from WordPress.com.\n\nNot seeing the email? Check your Spam or Junk Mail folder." = "Controlla la tua casella di posta su questo dispositivo e segui il link contenuto nell'email ricevuta da WordPress.com.\n\nNon trovi l'email? Controlla la cartella dello spam o della posta indesiderata."; /* Alert title for check your email during logIn/signUp. */ -"Check your email!" = "Controlla l'e-mail."; +"Check your email!" = "Controlla l'email!"; /* Subtitle shown on the dashboard when it fails to load */ "Check your internet connection and pull to refresh." = "Controlla la connessione a Internet e aggiorna."; @@ -1557,9 +1518,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Choose file" = "Scegli il file"; -/* Menu option for selecting media from the device's photo library. */ -"Choose from My Device" = "Scegli da Il mio dispositivo"; - /* Explanatory text for Homepage Settings homepage type selection. */ "Choose from a homepage that displays your latest posts (classic blog) or a fixed \/ static page." = "Scegli da una homepage che mostra i tuoi ultimi articoli (blog classico) o una pagina fissa\/statica."; @@ -1760,9 +1718,6 @@ translators: Block name. %s: The localized block name */ /* Community & Non-Profit site intent topic */ "Community & Non-Profit" = "Community e no-profit"; -/* Accessibility indication that the current Post List style is currently Compact. */ -"Compact" = "Compatto"; - /* The action is completed */ "Completed" = "Operazione completata"; @@ -1948,10 +1903,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Copied block" = "Blocco copiato"; -/* Copy the post url and paste anywhere in phone - Label for page copy link. Tapping copy the url of page */ -"Copy Link" = "Copia Link"; - /* Copy link to oomment button title */ "Copy Link to Comment" = "Copia il link per commentare"; @@ -2060,9 +2011,6 @@ translators: Block name. %s: The localized block name */ /* Error title displayed when unable to close user account. */ "Couldn’t close account automatically" = "Impossibile chiudere l'account automaticamente"; -/* Message to show while media data source is finding the number of items available. */ -"Counting media items..." = "Conteggio degli elementi media"; - /* Period Stats 'Countries' header */ "Countries" = "Paesi"; @@ -2164,7 +2112,7 @@ translators: Block name. %s: The localized block name */ "Creates new post, page, or story" = "Crea un nuovo articolo oppure una nuova pagina o storia"; /* Menus alert message for alerting the user to unsaved changes while trying to create a new menu. */ -"Creating a new menu will discard changes you've made to the current menu. Are you sure you want to continue?" = "Creando un nuovo menu si perderanno tutte le modifiche fatte al menu corrente. Sei sicuro di voler proseguire?"; +"Creating a new menu will discard changes you've made to the current menu. Are you sure you want to continue?" = "Creando un nuovo menu si perderanno tutte le modifiche fatte al menu corrente. Vuoi davvero proseguire?"; /* User-facing string, presented to reflect that site assembly is underway. */ "Creating dashboard" = "Creazione della bacheca"; @@ -2313,7 +2261,6 @@ translators: Block name. %s: The localized block name */ /* Delete Delete button title for the warning shown to the user when he refuses to re-login when the authToken is missing. Title for button that permanently deletes a media item (photo / video) - Title for button that permanently deletes one or more media items (photos / videos) Title of a delete button Title of the trash confirmation alert. */ "Delete" = "Elimina"; @@ -2321,15 +2268,11 @@ translators: Block name. %s: The localized block name */ /* Menus confirmation button for deleting a menu. */ "Delete Menu" = "Elimina menu"; -/* Delete option in the confirmation alert when deleting a page from the trash. - Delete option in the confirmation alert when deleting a post from the trash. - Label for a button permanently deletes a page. - Label for the delete post option. Tapping permanently deletes a post. +/* Delete option in the confirmation alert when deleting a post from the trash. Title for button on the comment details page that deletes the comment when tapped. */ "Delete Permanently" = "Elimina definitivamente"; -/* Title of the confirmation alert when deleting a page from the trash. - Title of the confirmation alert when deleting a post from the trash. */ +/* Title of the confirmation alert when deleting a post from the trash. */ "Delete Permanently?" = "Cancellare definitivamente?"; /* Button label for deleting the current site @@ -2380,7 +2323,7 @@ translators: Block name. %s: The localized block name */ "Details" = "Dettagli"; /* Instructions after a Magic Link was sent, but email is incorrect. */ -"Didn't mean to create a new account? Go back to re-enter your email address." = "Non volevi creare un nuovo account? Torna indietro per inserire nuovamente l'indirizzo e-mail."; +"Didn't mean to create a new account? Go back to re-enter your email address." = "Non volevi creare un nuovo account? Torna indietro per inserire nuovamente l'indirizzo email."; /* Label for the dimensions in pixels for a media asset (image / video) */ "Dimensions" = "Dimensioni"; @@ -2448,7 +2391,6 @@ translators: Block name. %s: The localized block name */ /* Accessibility label for button to dismiss a bottom sheet Accessibility label for the transparent space above the login dialog which acts as a button to dismiss the dialog. Accessibility label for the transparent space above the signup dialog which acts as a button to dismiss the dialog. - Action to show on alert when view asset fails. Dismiss a view. Verb */ "Dismiss" = "Ignora"; @@ -2466,12 +2408,6 @@ translators: Block name. %s: The localized block name */ User's Display Name */ "Display Name" = "Nome da visualizare"; -/* Accessibility label for other media items in the media collection view. The parameter is the creation date of the document. */ -"Document, %@" = "Documento, %@"; - -/* Accessibility label for other media items in the media collection view. The parameter is the filename file. */ -"Document: %@" = "Documento: %@"; - /* Message shown when all tours have been completed */ "Doesn't it feel good to cross things off a list?" = "Non è bello spuntare le cose su una lista?"; @@ -2632,8 +2568,7 @@ translators: Block name. %s: The localized block name */ /* Description of a Quick Start Tour */ "Draft and publish a post." = "Elabora una bozza e pubblica un articolo."; -/* Title of the drafts filter. This filter shows a list of draft posts. - Title of the drafts header in search list. */ +/* Title of the drafts filter. This filter shows a list of draft posts. */ "Drafts" = "Bozze"; /* No comment provided by engineer. */ @@ -2645,10 +2580,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Drag to adjust focal point" = "Trascina per regolare il punto focale"; -/* Label for page duplicate option. Tapping creates a copy of the page. - Label for post duplicate option. Tapping creates a copy of the post. */ -"Duplicate" = "Duplica"; - /* No comment provided by engineer. */ "Duplicate block" = "Duplica il blocco"; @@ -2661,13 +2592,9 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Each block has its own settings. To find them, tap on a block. Its settings will appear on the toolbar at the bottom of the screen." = "Ogni blocco ha le proprie impostazioni. Per trovarle tocca il blocco. Verranno visualizzate le impostazioni nella barra degli strumenti nella parte inferiore dello schermo."; -/* Accessibility label for edit button to enable multi selection mode in the user's media library - Edit the post. - Editing GIF alert default action button. +/* Editing GIF alert default action button. Edits a Comment Edits the comment - Label for a button that opens the Edit Page view controller - Label for the edit post button. Tapping displays the editor. User action to edit media details. Verb, edit a comment */ "Edit" = "Modifica"; @@ -2675,9 +2602,6 @@ translators: Block name. %s: The localized block name */ /* Title for the edit more button section */ "Edit \"More\" button" = "Modifica il pulsante \"Altro\""; -/* Button that displays the media editor to the user */ -"Edit %@" = "Modifica %@"; - /* Blocklist Keyword Edition Title */ "Edit Blocklist Word" = "Modifica elenco delle parole da bloccare"; @@ -2784,7 +2708,7 @@ translators: Block name. %s: The localized block name */ /* A placeholder for the username textfield. Invite Username Placeholder */ -"Email or Username…" = "E-mail o nome utente…"; +"Email or Username…" = "Email o nome utente…"; /* Overlay message displayed when export content started */ "Email sent!" = "Email inviata!"; @@ -2870,9 +2794,6 @@ translators: Block name. %s: The localized block name */ /* Secondary message shown when there are no domains that match the user entered text. */ "Enter different words above and we'll look for an address that matches it." = "Inserire parole diverse nel campo in alto e cercheremo un indirizzo che corrisponda."; -/* Accessibility hint for edit button to enable multi selection mode in the user's media library */ -"Enter edit mode to enable multi select to delete" = "Accedi alla modalità di modifica per attivare la selezione multipla da eliminare"; - /* (placeholder) Help enter WordPress password Placeholder of a field to type a password to protect the post. */ "Enter password" = "Inserisci la password"; @@ -2894,7 +2815,7 @@ translators: Block name. %s: The localized block name */ "Enter your account information for %@." = "Inserisci le informazioni sull’account per %@."; /* Instruction text on the initial email address entry screen. */ -"Enter your email address to log in or create a WordPress.com account." = "Inserisci il tuo indirizzo e-mail per accedere o creare un account WordPress.com."; +"Enter your email address to log in or create a WordPress.com account." = "Inserisci il tuo indirizzo email per accedere o creare un account WordPress.com."; /* Button title. Takes the user to the login by site address flow. */ "Enter your existing site address" = "Inserisci l'indirizzo del sito esistente"; @@ -3028,9 +2949,6 @@ translators: Block name. %s: The localized block name */ /* Short title telling the user they will receive a blogging reminder every day of the week. */ "Every day at %@" = "Ogni giorno alle %@"; -/* Label for the post author filter. This filter shows posts for all users on the blog. */ -"Everyone" = "Tutti"; - /* Example story title description */ "Example story title" = "Esempio titolo storia"; @@ -3040,9 +2958,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Excerpt length (words)" = "Lunghezza estratto (parole)"; -/* Accessibility label for a post's excerpt in the post list. The parameter is the post excerpt. For example, \"Excerpt. This is the first paragraph.\" */ -"Excerpt. %@." = "Estratto. %@."; - /* Should be the same as the text displayed if the user clicks the (i) in Calypso. */ "Excerpts are optional hand-crafted summaries of your content." = "I riassunti sono dei sommari opzionali dei tuoi contenuti, scritti manualmente."; @@ -3052,8 +2967,7 @@ translators: Block name. %s: The localized block name */ /* Accessibility Label for the exit full screen button on the full screen comment reply mode */ "Exit Full Screen" = "Esci da modalità schermo interno"; -/* Accessibility indication that the current Post List style is currently Expanded. - Screen reader text to represent the expanded state of a UI control */ +/* Screen reader text to represent the expanded state of a UI control */ "Expanded" = "Espanso"; /* Accessibility hint */ @@ -3103,9 +3017,6 @@ translators: Block name. %s: The localized block name */ The action failed */ "Failed" = "Fallito"; -/* Error title when picked media cannot be imported into stories. */ -"Failed Media Export" = "Esportazione file multimediali non riuscita"; - /* Message for mark all as read success notice */ "Failed marking Notifications as read" = "Contrassegno delle notifiche come lette non riuscito"; @@ -3307,6 +3218,12 @@ translators: Block name. %s: The localized block name */ /* An example tag used in the login prologue screens. */ "Football" = "Calcio"; +/* translators: Recommendation included in a warning related to having blocks deeply nested. */ +"For this reason, we recommend editing the block using the web editor." = "Per questo motivo consigliamo di modificare il blocco tramite l'editor web."; + +/* translators: Recommendation included in a warning related to having blocks deeply nested. */ +"For this reason, we recommend editing the block using your web browser." = "Per questo motivo consigliamo di modificare il blocco tramite il browser web."; + /* Register Domain - Domain contact information section header description */ "For your convenience, we have pre-filled your WordPress.com contact information. Please review to be sure it’s the correct information you want to use for this domain." = "Per agevolarti, abbiamo precompilato le tue informazioni di contatto di WordPress.com. Rileggi per verificare che le informazioni che desideri utilizzare per questo dominio siano corrette."; @@ -3384,7 +3301,7 @@ translators: Block name. %s: The localized block name */ "Get Started" = "Inizia ora"; /* The button title for a secondary call-to-action button. When the user wants to try sending a magic link instead of entering a password. */ -"Get a login link by email" = "Ottieni un link di accesso tramite e-mail"; +"Get a login link by email" = "Ottieni un link di accesso tramite email"; /* Displayed in the Notifications Tab as a message, when there are no notifications */ "Get active! Comment on posts from blogs you follow." = "Impegnati! Commenta gli articoli dei blog che segui."; @@ -3624,8 +3541,7 @@ translators: Block name. %s: The localized block name */ "Home" = "Homepage"; /* Title for setting which shows the current page assigned as a site's homepage - Title for the homepage section in site settings screen - Title of the Homepage Badge */ + Title for the homepage section in site settings screen */ "Homepage" = "Homepage"; /* Label for Homepage Settings site settings section @@ -3672,7 +3588,7 @@ translators: Block name. %s: The localized block name */ "If you already have a site, you’ll need to install the free Jetpack plugin and connect it to your WordPress.com account." = "Se hai già un sito, dovrai installare il plugin Jetpack gratuito e collegarlo al tuo account WordPress.com."; /* The instructions text about not being able to find the magic link email. */ -"If you can’t find the email, please check your junk or spam email folder" = "Se non riesci a trovare l'e-mail, controlla la cartella della posta indesiderata o dello spam"; +"If you can’t find the email, please check your junk or spam email folder" = "Se non riesci a trovare l'email, controlla la cartella della posta indesiderata o dello spam"; /* Legal disclaimer for signing up. The underscores _..._ denote underline. */ "If you continue with Apple or Google and don't already have a WordPress.com account, you are creating an account and you agree to our _Terms of Service_." = "Se continui con Apple o Google e non hai già un account WordPress.com, crei un account e accetti i nostri _Termini di servizio_."; @@ -3722,9 +3638,6 @@ translators: Block name. %s: The localized block name */ /* Hint for image title on image settings. */ "Image title" = "Titolo immagine"; -/* Accessibility label for image thumbnails in the media collection view. The parameter is the creation date of the image. */ -"Image, %@" = "Immagine, %@"; - /* Undated post time label */ "Immediately" = "Adesso"; @@ -3868,7 +3781,7 @@ translators: Block name. %s: The localized block name */ "Introducing Story Posts" = "Introduzione agli articoli della storia"; /* Title of an alert letting the user know the email address that they've entered isn't valid */ -"Invalid Email Address" = "Indirizzo e-mail non valido"; +"Invalid Email Address" = "Indirizzo email non valido"; /* No comment provided by engineer. */ "Invalid Site Address" = "Indirizzo del sito non valido"; @@ -4210,9 +4123,6 @@ translators: Block name. %s: The localized block name */ Settings: Comments Approval settings */ "Links in comments" = "Link nei commenti"; -/* The accessibility label for the list style button in the Post List. */ -"List style" = "Stile dell'elenco"; - /* Title of the screen that load selected the revisions. */ "Load" = "Carica"; @@ -4228,18 +4138,12 @@ translators: Block name. %s: The localized block name */ /* Text displayed while loading the activity feed for a site */ "Loading Backups..." = "Caricamento dei backup in corso…"; -/* Phrase to show when the user has searched for GIFs and they are being loaded. */ -"Loading GIFs..." = "Caricamento GIF in corso..."; - /* Menus label text displayed while menus are loading */ "Loading Menus..." = "Caricamento dei menu"; /* Text displayed while loading site People. */ "Loading People..." = "Caricamento persone in corso..."; -/* Phrase to show when the user has searched for images and they are being loaded. */ -"Loading Photos..." = "Caricamento foto in corso..."; - /* Text displayed while loading plans details */ "Loading Plan..." = "Caricamento del piano in corso ..."; @@ -4300,8 +4204,7 @@ translators: Block name. %s: The localized block name */ /* Local Services site intent topic */ "Local Services" = "Servizi locali"; -/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. - Title of the Local Changes Badge */ +/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. */ "Local changes" = "Modifiche locali"; /* Title for alert when a generic error happened when trying to find the location of the device */ @@ -4344,7 +4247,7 @@ translators: Block name. %s: The localized block name */ "Log in to the WordPress.com account you used to connect Jetpack." = "Accedi all'account WordPress.com che hai utilizzato per connettere Jetpack."; /* Instruction text on the login's email address screen. */ -"Log in to your WordPress.com account with your email address." = "Accedi al tuo account WordPress.com con il tuo indirizzo e-mail."; +"Log in to your WordPress.com account with your email address." = "Accedi al tuo account WordPress.com con il tuo indirizzo email."; /* Instructions on the WordPress.com username / password log in form. */ "Log in with your WordPress.com username and password." = "Accedi con il nome utente e la password di WordPress.com."; @@ -4465,7 +4368,6 @@ translators: Block name. %s: The localized block name */ "Max Video Upload Size" = "Dimensione massima per il caricamento video"; /* Accessibility label for the Me button in My Site. - Label for the post author filter. This filter shows posts only authored by the current user. Me page title Noun. Title. Links to the Me screen. The accessibility value of the me tab. @@ -4473,9 +4375,7 @@ translators: Block name. %s: The localized block name */ "Me" = "Io"; /* Noun. Title. Links to the blog's Media library. - Tab bar title for the Media tab in Media Picker The menu item to select during a guided tour. - Title for Media Library section of the app. Title for the media section in site settings screen Title label for the media settings section in the app settings */ "Media" = "Media"; @@ -4487,13 +4387,6 @@ translators: Block name. %s: The localized block name */ /* Label for size of media cache in the app. */ "Media Cache Size" = "Dimensioni cache contenuti multimediali"; -/* Title for alert when access to media capture is not granted */ -"Media Capture" = "Acquisizione contenuti multimediali"; - -/* Title for alert when a generic error happened when loading media - Title for alert when access to the media library is not granted by the user */ -"Media Library" = "Libreria media"; - /* Title for action sheet with media options. */ "Media Options" = "Opzioni media"; @@ -4516,9 +4409,6 @@ translators: Block name. %s: The localized block name */ /* translators: %s: block title e.g: \"Paragraph\". */ "Media options" = "Opzioni media"; -/* Alert title when there is issues loading an asset to preview. */ -"Media preview failed." = "Anteprima dei contenuti multimediali non riuscita."; - /* Alert displayed to the user when multiple media items have uploaded successfully. */ "Media uploaded (%ld files)" = "Elemento multimediale caricato (%ld file)"; @@ -4556,9 +4446,6 @@ translators: Block name. %s: The localized block name */ Label for the share message field on the post settings. */ "Message" = "Messaggio"; -/* Title of section containing image / video metadata such as size and file type */ -"Metadata" = "Metadata"; - /* Option to select the Microsft Outlook app when logging in with magic links */ "Microsoft Outlook" = "Microsoft Outlook"; @@ -4578,13 +4465,11 @@ translators: Block name. %s: The localized block name */ "Months and Years" = "Mesi e anni"; /* Accessibility label for more button in dashboard quick start card. - Accessibility label for the More button in Page List. Accessibility label for the More button in Post List (compact view). Accessibility label for the More button on formatting toolbar. Accessibility label for the More button on Reader Cell Accessibility label for the More button on Reader's post details - Action button to display more available options - Label for the more post button. Tapping displays an action sheet with post options. */ + Action button to display more available options */ "More" = "Altro"; /* Action button to display more available options @@ -4642,15 +4527,8 @@ translators: Block name. %s: The localized block name */ /* Screen reader text for button that will move the menu item */ "Move menu item" = "Rimuovi elemento menu"; -/* Label for a button that moves a page to the draft folder - Label for an option that moves a post to the draft folder */ -"Move to Draft" = "Sposta in Bozze"; - -/* Label for a button that moves a page to the trash folder - Label for a option that moves a post to the trash folder - Title for button on the comment details page that moves the comment to trash when tapped. +/* Title for button on the comment details page that moves the comment to trash when tapped. Trash option in the trash confirmation alert. - Trash option in the trash page confirmation alert. Trashes the comment */ "Move to Trash" = "Sposta nel cestino"; @@ -4682,7 +4560,8 @@ translators: Block name. %s: The localized block name */ Title of My Site tab */ "My Site" = "Il mio sito"; -/* Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ +/* Title for site picker screen. + Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ "My Sites" = "I miei siti"; /* Siri Suggestion to open My Sites */ @@ -4932,9 +4811,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title for the view when there aren't any Activities to display in the Activity Log for a given filter. */ "No matching events found." = "Non è stato trovato alcun evento corrispondente alla ricerca."; -/* Message displayed when no results are returned from a media library search. Should match Calypso. - Phrase to show when the user search for images but there are no result to show. - Phrase to show when the user searches for GIFs but there are no result to show. */ +/* Message displayed when no results are returned from a media library search. Should match Calypso. */ "No media matching your search" = "Nessun media corrispondente alla tua ricerca"; /* Menus text shown when no menus were available for loading the Menus editor. */ @@ -4952,8 +4829,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Displayed in the Notifications Tab as a title, when there are no notifications */ "No notifications yet" = "Nessuna notifica"; -/* Displayed when the user is searching the pages list and there are no matching pages - Text displayed when there's no matching with the text search */ +/* Text displayed when there's no matching with the text search */ "No pages matching your search" = "Nessuna pagina corrispondente alla ricerca"; /* Text displayed when search for plugins returns no results */ @@ -4974,9 +4850,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Message shown whent the reader finds no posts for the chosen tag */ "No posts have been made recently with this tag." = "Di recente non sono stati creati articoli con questo tag."; -/* Displayed when the user is searching the posts list and there are no matching posts */ -"No posts matching your search" = "Nessun articolo corrispondente alla ricerca"; - /* Accessibility value for a Stats' Posting Activity Month if there are no posts. */ "No posts." = "Nessun articolo."; @@ -5054,7 +4927,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Not right now" = "Non adesso"; /* Instructions after a Magic Link was sent, but the email can't be found in their inbox. */ -"Not seeing the email? Check your Spam or Junk Mail folder." = "Non trovi l'e-mail? Controlla la cartella dello spam o della posta indesiderata."; +"Not seeing the email? Check your Spam or Junk Mail folder." = "Non trovi l'email? Controlla la cartella dello spam o della posta indesiderata."; /* Button that allows users unsure of what selection they'd like */ "Not sure, show me around" = "Non sono sicuro, fammi dare un'occhiata in giro"; @@ -5077,9 +4950,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* A message title */ "Nothing liked yet" = "Ancora nessun contenuto collegato"; -/* Default message for empty media picker */ -"Nothing to show" = "Niente da mostrare"; - /* Notifications Details Accessibility Identifier */ "Notification Details Table" = "Tabella dettagli notifiche"; @@ -5139,7 +5009,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Alert dismissal title Button title. Acknowledges a prompt. Button title. An acknowledgement of the message displayed in a prompt. - Confirmation of action Default action Dismisses the alert Marks all notifications as read. @@ -5201,9 +5070,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* No comment provided by engineer. */ "Only show excerpt" = "Mostra solo estratto"; -/* Message telling the user that they've only enabled limited photo library permissions for the app. */ -"Only the selected photos you've given access to are available." = "Sono disponibili solo le foto selezionate a cui hai concesso l'accesso. "; - /* An informal exclaimation that means `something went wrong`. Title for the view when there's an error loading a comment. Title for the view when there's an error loading Activity Log @@ -5238,9 +5104,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title of a button that opens the apps settings in the system Settings.app */ "Open Settings" = "Apri impostazioni"; -/* Editor button to swich the media picker from quick mode to full picker */ -"Open full media picker" = "Apri tutti i contenuti multimediali"; - /* No comment provided by engineer. */ "Open in Safari" = "Apri in Safari"; @@ -5280,6 +5143,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Divider on initial auth view separating auth options. */ "Or" = "Or"; +/* Instruction text for other forms of two-factor auth methods. */ +"Or choose another form of authentication." = "Oppure scegli un'altra forma di autenticazione."; + /* Label for button to log in using site address. Underscores _..._ denote underline. */ "Or log in by _entering your site address_." = "Altrimenti, accedi _inserendo l'indirizzo del tuo sito_."; @@ -5338,15 +5204,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title shown when selecting a post type of Page from the Share Extension. */ "Page" = "Pagina"; -/* Prompts the user that a restored page was moved to the drafts list. */ -"Page Restored to Drafts" = "Pagina ripristinata in Bozze"; - -/* Prompts the user that a restored page was moved to the published list. */ -"Page Restored to Published" = "Pagina ripristinata in Pubblicate"; - -/* Prompts the user that a restored page was moved to the scheduled list. */ -"Page Restored to Scheduled" = "Pagina ripristinata in Programmate"; - /* Name of the button to open the page settings The title of the Page Settings screen. */ "Page Settings" = "Impostazioni pagina"; @@ -5363,9 +5220,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of notification displayed when a page has failed to upload. */ "Page failed to upload" = "La pagina non è stato caricata"; -/* A short message explaining that a page was moved to the trash bin. */ -"Page moved to trash." = "Pagina spostata nel cestino."; - /* Title of notification displayed when a page has been successfully saved as a draft. */ "Page pending review" = "Pagina in attesa di revisione"; @@ -5437,8 +5291,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title of pending Comments filter. */ "Pending" = "In sospeso"; -/* Name for the status of a post pending review. - Title of the Pending Review Badge */ +/* Name for the status of a post pending review. */ "Pending review" = "In attesa di revisione"; /* Noun. Title of the people management feature. @@ -5467,12 +5320,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Photography site intent topic */ "Photography" = "Fotografia"; -/* Tab bar title for the Photos tab in Media Picker */ -"Photos" = "Foto"; - -/* Subtitle for placeholder in Free Photos. The company name 'Pexels' should always be written as it is. */ -"Photos provided by Pexels" = "Foto fornite da Pexels"; - /* Title for selecting a new username in the site creation flow. */ "Pick username" = "Scegli il nome utente"; @@ -5550,10 +5397,10 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Please enter a valid URL" = "Inserisci un URL valido"; /* Register Domain - Domain contact information validation error message for an input field */ -"Please enter a valid address" = "Inserisci un indirizzo e-mail valido"; +"Please enter a valid address" = "Inserisci un indirizzo email valido"; /* An error message. */ -"Please enter a valid email address for a WordPress.com account." = "Inserisci un indirizzo e-mail valido per un account WordPress.com."; +"Please enter a valid email address for a WordPress.com account." = "Inserisci un indirizzo email valido per un account WordPress.com."; /* Error message displayed when the user attempts use an invalid email address. */ "Please enter a valid email address." = "Inserisci un indirizzo email valido."; @@ -5565,7 +5412,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Please enter the password for your WordPress.com account to log in with your Apple ID." = "Digita la password del tuo account WordPress.com per accedere con il tuo Apple ID."; /* Instruction text on the two-factor screen. */ -"Please enter the verification code from your authenticator app, or tap the link below to receive a code via SMS." = "Inserisci il codice di verifica dalla tua app Authenticator o tocca il link di seguito per ricevere un codice via SMS."; +"Please enter the verification code from your authenticator app." = "Inserisci il codice di verifica dalla tua app di autenticazione."; /* Popup message to ask for user credentials (fields shown below). */ "Please enter your credentials" = "Si prega di inserire i dati di accesso"; @@ -5660,15 +5507,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ The post formats available for the post. Should be the same as in core WP. */ "Post Format" = "Formato articolo"; -/* Prompts the user that a restored post was moved to the drafts list. */ -"Post Restored to Drafts" = "Articolo ripristinato in Bozze"; - -/* Prompts the user that a restored post was moved to the published list. */ -"Post Restored to Published" = "Articolo ripubblicato"; - -/* Prompts the user that a restored post was moved to the scheduled list. */ -"Post Restored to Scheduled" = "Articolo riprogramamto"; - /* Name of the button to open the post settings The title of the Post Settings screen. */ "Post Settings" = "Impostazioni articolo"; @@ -5688,9 +5526,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of notification displayed when a post has failed to upload. */ "Post failed to upload" = "Il post non è stato caricato"; -/* A short message explaining that a post was moved to the trash bin. */ -"Post moved to trash." = "Articolo spostato nel cestino."; - /* Title of notification displayed when a post has been successfully saved as a draft. */ "Post pending review" = "Post in attesa di revisione"; @@ -5749,9 +5584,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Period Stats 'Posts and Pages' header */ "Posts and Pages" = "Articoli e Pagine"; -/* Title of the Posts Page Badge */ -"Posts page" = "Pagina degli articoli"; - /* Message informing the user that their static homepage for posts was set successfully */ "Posts page successfully updated" = "Pagina degli articoli aggiornata correttamente"; @@ -5764,9 +5596,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* A message explaining the Posts I Like feature in the reader */ "Posts that you like will appear here." = "Gli articoli che ti piacciono vengono visualizzati qui."; -/* Subtitle for placeholder in Tenor picker. `The company name 'Tenor' should always be written as it is. */ -"Powered by Tenor" = "Promosso da Tenor"; - /* Browse premium themes selection title */ "Premium" = "Premium"; @@ -5785,18 +5614,12 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title for screen to preview a static content. */ "Preview" = "Anteprima"; -/* Action for Media Picker to preview the selected media items. The argument in the string represents the number of elements (as numeric digits) selected */ -"Preview %@" = "Anteprima %@"; - /* Title for web preview device switching button */ "Preview Device" = "Anteprima dispositivo"; /* Title on display preview error */ "Preview Unavailable" = "Anteprima non disponibile"; -/* Accessibility label for media item preview for user's viewing an item in their media library */ -"Preview media" = "Visualizza in anteprima l'elemento multimediale"; - /* No comment provided by engineer. */ "Preview page" = "Pagina di anteprima"; @@ -5843,8 +5666,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Privacy notice for California users" = "Nota sulla privacy per utenti in California"; /* Name for the status of a post that is marked private. - Privacy setting for posts set to 'Private'. Should be the same as in core WP. - Title of the Private Badge */ + Privacy setting for posts set to 'Private'. Should be the same as in core WP. */ "Private" = "Privato"; /* No comment provided by engineer. */ @@ -5894,12 +5716,10 @@ translators: %s: Select control button label e.g. \"Button width\" */ Label for the publish date button. */ "Publish Date" = "Data di pubblicazione"; -/* A short phrase indicating a post is due to be immedately published. - Label for a button that moves a page to the published folder, publishing with the current date/time. */ +/* A short phrase indicating a post is due to be immedately published. */ "Publish Immediately" = "In questo momento"; /* Label for a button that publishes the post - Label for an option that moves a publishes a post immediately Title of button allowing the user to immediately publish the post they are editing. */ "Publish Now" = "Pubblica"; @@ -5917,8 +5737,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Name for the status of a published post. Period Stats 'Published' header - Title of the published filter. This filter shows a list of posts that the user has published. - Title of the published header in search list. */ + Title of the published filter. This filter shows a list of posts that the user has published. */ "Published" = "Pubblicato"; /* Precedes the name of the blog just posted on */ @@ -6060,8 +5879,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of the completion screen of the Blogging Reminders Settings screen when the reminders are removed. */ "Reminders removed" = "Promemoria eliminati"; -/* Add asset to media picker list - Alert button to confirm a plugin to be removed +/* Alert button to confirm a plugin to be removed Button label when removing a blog Label for the Remove Feature Image icon. Tapping will show a confirmation screen for removing the feature image from the post. Remove Action @@ -6214,9 +6032,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of secondary button on alert prompting verify their accounts while attempting to publish */ "Resend" = "Invia nuovamente"; -/* Title of the reset button */ -"Reset" = "Azzera"; - /* Accessibility label for the reset activity type button */ "Reset Activity Type filter" = "Ripristina il filtro del tipo di attività"; @@ -6271,12 +6086,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ Button title, displayed when media has failed to upload. Allows the user to try the upload again. Button title. Retries uploading a post. If a user taps the button with this label, the action that evinced this error view will be retried. - Label for a button that attempts to re-upload a page that previously failed to upload. - Label for the retry post upload button. Tapping attempts to upload the post again. Opens the media library . Retries the upload of a user's post. Retry updating User's Role - Retry uploading the post. Retry. Action Retry. Verb – retry a failed media upload. The Jetpack view button title used when an error occurred @@ -6288,9 +6100,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Button title that triggers a scan */ "Retry Scan" = "Riprova la scansione"; -/* User action to retry media upload. */ -"Retry Upload" = "Riprova il caricamento"; - /* User action to retry all failed media uploads. */ "Retry all" = "Recupera tutto"; @@ -6388,9 +6197,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Accessibility label for the 'Save Post' button when a post has been saved. */ "Saved Post" = "Articolo salvato"; -/* Text displayed in HUD when a media item's metadata (title, etc) is saved successfully. */ -"Saved!" = "Salvato"; - /* Accessibility hint for the 'Save Post' button. */ "Saves this post for later." = "Salva questo articolo per dopo"; @@ -6401,7 +6207,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Saving post…" = "Salvataggio dell'articolo…"; /* Menus save button title while it is saving a Menu. - Text displayed in HUD while a media item's metadata (title, etc) is being saved. Text displayed in HUD while a post is being saved as a draft. */ "Saving..." = "Salvataggio in corso..."; @@ -6492,21 +6297,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* No comment provided by engineer. */ "Search or type URL" = "Cerca o digita URL"; -/* Text displayed when the search controller will be presented */ -"Search pages" = "Cerca tra le pagine"; - -/* Text displayed when the search controller will be presented */ -"Search posts" = "Cerca articoli"; - /* No comment provided by engineer. */ "Search settings" = "Impostazioni di ricerca"; -/* Title for placeholder in Tenor picker */ -"Search to find GIFs to add to your Media Library!" = "Cerca per trovare GIF da aggiungere alla tua Libreria multimediale!"; - -/* Title for placeholder in Free Photos */ -"Search to find free photos to add to your Media Library!" = "Cerca per trovare foto gratuite da aggiungere alla tua Libreria multimediale!"; - /* Menus search bar placeholder text. */ "Search..." = "Cerca..."; @@ -6577,9 +6370,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Register Domain - Domain contact information field placeholder for Country */ "Select Country" = "Seleziona il paese"; -/* Title of button that allows the user to select more photos to access within the app */ -"Select More" = "Seleziona più elementi"; - /* Blog Picker's Title */ "Select Site" = "Seleziona sito"; @@ -6601,9 +6391,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Register domain - Title for the Choose domain button of Suggested domains screen */ "Select domain" = "Seleziona un dominio"; -/* Accessibility hint for actions when displaying media items. */ -"Select media." = "Seleziona contenuto multimediale."; - /* Accessibility label for selecting paragraph style button on formatting toolbar. */ "Select paragraph style" = "Seleziona lo stile del paragrafo"; @@ -6660,10 +6447,10 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "Selected: Default" = "Selezionato: impostazione predefinita"; /* Menus alert message for alerting the user to unsaved changes while trying to select a different menu location. */ -"Selecting a different menu location will discard changes you've made to the current menu. Are you sure you want to continue?" = "Selezionando una differente posizione di menu si perderanno le modifiche fatta al menu corrente. Desideri proseguire??"; +"Selecting a different menu location will discard changes you've made to the current menu. Are you sure you want to continue?" = "Selezionando una differente posizione di menu si perderanno le modifiche fatta al menu corrente. Vuoi davvero proseguire?"; /* Menus alert message for alerting the user to unsaved changes while trying to select a different menu. */ -"Selecting a different menu will discard changes you've made to the current menu. Are you sure you want to continue?" = "Se selezioni un altro menù le modifiche effettuate all'interno del menù corrente verranno perse. Sei sicuro di voler continuare?"; +"Selecting a different menu will discard changes you've made to the current menu. Are you sure you want to continue?" = "Se selezioni un altro menù le modifiche effettuate all'interno del menù corrente verranno perse. Vuoi davvero continuare?"; /* Title of an alert informing users that the video they are trying to select is not allowed. */ "Selection not allowed" = "Selezione non consentita"; @@ -6681,7 +6468,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "Send Link" = "Invia link"; /* The button title text for sending a magic link. */ -"Send Link by Email" = "Invia link tramite e-mail"; +"Send Link by Email" = "Invia link tramite email"; /* Title of a row displayed on the debug screen used to send a pretend error message to the crash logging provider to ensure everything is working correctly */ "Send Log Message" = "Invia messaggio del registro"; @@ -6693,7 +6480,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "Send Test Crash" = "Invia crash test"; /* Button title. Sends a email verification link (Magin link) for signing in. */ -"Send email verification link" = "Invia link di verifica dell'e-mail"; +"Send email verification link" = "Invia link di verifica dell'email"; /* Jetpack Monitor Settings: Send notifications by email */ "Send notifications by email" = "Invia notifiche via email"; @@ -6707,19 +6494,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label for connected service in Publicize stat. */ "Service" = "Servizio"; -/* Label for a button that opens the Set Parent options view controller - Navigation title displayed on the navigation bar */ +/* Navigation title displayed on the navigation bar */ "Set Parent" = "Imposta genitore"; /* No comment provided by engineer. */ "Set as Featured Image" = "Imposta come immagine in evidenza "; -/* Label for a button that sets the selected page as the site's Homepage */ -"Set as Homepage" = "Imposta come homepage"; - -/* Label for a button that sets the selected page as the site's Posts page */ -"Set as Posts Page" = "Imposta come pagina degli articoli"; - /* Notice confirming that an image has been set as the post's featured image. */ "Set as featured image" = "Imposta come immagine in evidenza"; @@ -6763,7 +6543,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Accessibility label for share buttons in nav bars Button label to share a post Button label to share a web page - Share the post. Shares the comment URL Spoken accessibility label Title for a button that allows the user to share their answer to the prompt. @@ -7149,8 +6928,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Name of setting configured when a site uses a static page as its homepage */ "Static Homepage" = "Homepage statica"; -/* Label for post stats option. Tapping displays statistics for a post. - Noun. Abbreviation of Statistics. Name of the Stats feature +/* Noun. Abbreviation of Statistics. Name of the Stats feature Noun. Abbv. of Statistics. Links to a blog's Stats screen. Stats 3D Touch Shortcut Stats window title @@ -7181,9 +6959,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label text that defines a post marked as sticky */ "Sticky" = "In evidenza"; -/* Accessibility label for a sticky post in the post list. */ -"Sticky." = "In evidenza."; - /* User action to stop upload. */ "Stop upload" = "Ferma il caricamento"; @@ -7240,7 +7015,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ Theme Support action title */ "Support" = "Supporto"; -/* Button used to switch site */ +/* Title for back button that leads to the site picker screen. */ "Switch Site" = "Cambia sito"; /* Switches the Editor to HTML Mode */ @@ -7328,9 +7103,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label explaining why users might want to add tags. */ "Tags help tell readers what a post is about. Separate different tags with commas." = "I tag aiutano a dire al lettore di cosa parla un articolo. Separa i differenti tag con delle virgole."; -/* Menu option for taking an image or video with the device's camera. */ -"Take Photo or Video" = "Scatta foto o fai un video."; - /* No comment provided by engineer. */ "Take a Photo" = "Scatta una foto"; @@ -7401,12 +7173,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Accessibility hint */ "Tap to select the previous period" = "Tocca per selezionare il periodo precedente"; -/* Accessibility hint for button used to switch site */ -"Tap to switch to another site, or add a new site" = "Tocca per passare a un altro sito o aggiungerne uno nuovo"; - -/* Accessibility hint for media item preview for user's viewing an item in their media library */ -"Tap to view media in full screen" = "Tocca per visualizzare l'elemento multimediale a schermo intero"; - /* Accessibility hint for a button that opens a new view with more details. */ "Tap to view more details." = "Tocca per mostrare più dettagli."; @@ -7452,10 +7218,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* No comment provided by engineer. */ "Text formatting controls are located within the toolbar positioned above the keyboard while editing a text block" = "I controlli relativi alla formattazione del testo sono localizzati all'interno della barra degli strumenti posizionata sulla tastiera mentre modifichi un blocco di testo"; -/* Button title - The button's title text to send a 2FA code via SMS text message. */ +/* Button title */ "Text me a code instead" = "Inviami un codice invece"; +/* The button's title text to send a 2FA code via SMS text message. */ +"Text me a code via SMS" = "Inviami un codice tramite SMS"; + /* Message of alert when theme activation succeeds */ "Thanks for choosing %@ by %@" = "Grazie per aver scelto %1$@ di %2$@"; @@ -7483,9 +7251,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Error message shown to a user who is trying to share to Facebook but does not have any available Facebook Pages. */ "The Facebook connection cannot find any Pages. Publicize cannot connect to Facebook Profiles, only published Pages." = "La connessione a Facebook non riesce a trovare pagine. Pubblicizza non può connettersi ai profili Facebook, bensì solo alle pagine pubblicate."; -/* Message shown when a GIF failed to load while trying to add it to the Media library. */ -"The GIF could not be added to the Media Library." = "La GIF non può essere aggiunta alla Libreria media."; - /* Description shown when a user logs in with Google but no matching WordPress.com account is found */ "The Google account \"%@\" doesn't match any account on WordPress.com" = "L'account Google \"%@\" non corrisponde a nessun account su WordPress.com"; @@ -7613,7 +7378,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Error message shown when user attempts to remove the site owner. */ "The user you are trying to remove is the owner of this site. Please contact support for assistance." = "L'utente che stai provando a rimuovere è il proprietario di questo sito. Per favore contatta il supporto per assistenza."; -/* Error message informing a user about an invalid password. */ +/* No comment provided by engineer. */ "The username or password stored in the app may be out of date. Please re-enter your password in the settings and try again." = "Il nome utente o la password memorizzatI nell'app potrebbero non essere aggiornati. Inserisci nuovamente la password nelle impostazioni e riprova."; /* Message shown when a video failed to load while trying to add it to the Media library. */ @@ -7681,9 +7446,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* A short error message letting the user know about a problem displaying a post. */ "There was a problem displaying this post." = "C'è stato un problema nel mostrare questo articolo."; -/* Error message displayed when the Media Library is unable to load a full sized preview of an item. */ -"There was a problem loading the media item." = "Si è verificato un problema durante il caricamento dell'elemento multimediale."; - /* The loading view subtitle displayed when an error occurred */ "There was a problem loading your data, refresh your page to try again." = "C’è stato un problema durante il caricamento dei tuoi dati, ricarica la pagina per riprovare."; @@ -7696,9 +7458,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Explaining to the user there was an error trying to obtain the current location of the user. */ "There was a problem when trying to access your location. Please try again later." = "Si è verificato un problema cercando di accedere alla tua posizione. Prova più tardi."; -/* Explaining to the user there was an generic error accesing media. */ -"There was a problem when trying to access your media. Please try again later." = "Si è verificato un problema nel tentativo di accedere ai tuoi elementi media. Riprova più tardi."; - /* Message for stories unknown error. */ "There was a problem with the Stories editor. If the problem persists you can contact us via the Me > Help & Support screen." = "Si è verificato un problema con l'Editor delle Storie. Se il problema persiste, puoi contattarci tramite la schermata Io > Schermata di Aiuto e supporto."; @@ -7769,9 +7528,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* A description informing the user in order to proceed with this feature we will need camera permissions, and how to enable it. */ "This app needs permission to access the Camera to scan login codes, tap on the Open Settings button to enable it." = "Questa app ha bisogno dell'autorizzazione di accesso alla fotocamera per scansionare i codici di accesso, tocca il pulsante Apri impostazioni per abilitarla."; -/* Explaining to the user why the app needs access to the device media library. */ -"This app needs permission to access your device media library in order to add photos and\/or video to your posts. Please change the privacy settings if you wish to allow this." = "Questa app richiede l'autorizzazione a poter accedere alla libreria multimediale del dispositivo per aggiungere le foto e\/o i video ai tuoi articoli. Se desideri che ciò avvenga, modifica le impostazioni della privacy."; - /* No comment provided by engineer. */ "This color combination may be hard for people to read. Try using a brighter background color and\/or a darker text color." = "Questa combinazione di colori può essere difficile da leggere per le persone. Prova a usare un colore di sfondo più brillante e\/o un colore di testo più scuro."; @@ -7881,6 +7637,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Description of a Quick Start Tour */ "Time to finish setting up your site! Our checklist walks you through the next steps." = "È il momento di finire l'impostazione del tuo sito! La nostra checklist ti guida attraverso i prossimi passaggi."; +/* Error when the uses takes more than 1 minute to submit a security key. */ +"Time's up, but don't worry, your security is our priority. Please try again!" = "Il tempo è scaduto, ma non preoccuparti: la tua sicurezza è la nostra priorità. Riprova."; + /* WordPress.com Marketing Footer Text */ "Tips for getting the most out of WordPress.com." = "Suggerimenti per utilizzare al meglio WordPress.com."; @@ -8004,24 +7763,20 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title for the traffic section in site settings screen */ "Traffic" = "Traffico"; +/* Describes a domain that was transferred from elsewhere to wordpress.com */ +"Transferred Domain" = "Dominio trasferito"; + /* translators: %s: block title e.g: \"Paragraph\". */ "Transform %s to" = "Trasforma %s in"; /* No comment provided by engineer. */ "Transform block…" = "Trasforma il blocco..."; -/* Accessibility label for trash button to delete items from the user's media library - Accessibility label for trash buttons in nav bars +/* Accessibility label for trash buttons in nav bars Trashes a comment Trashes the comment */ "Trash" = "Elimina"; -/* Accessibility hint for trash button to delete items from the user's media library */ -"Trash selected media" = "Sposta nel cestino l'elemento multimediale"; - -/* Title of the trash page confirmation alert. */ -"Trash this page?" = "Spostare la pagina nel cestino?"; - /* Title of the trash confirmation alert. */ "Trash this post?" = "Spostare il post nel cestino?"; @@ -8139,9 +7894,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Shown when a user logs in with Google but it subsequently fails to work as login to WordPress.com */ "Unable To Connect" = "Impossibile connettersi"; -/* An error message. */ -"Unable to Connect" = "Impossibile connettersi"; - /* Title for stories unknown error. */ "Unable to Create Stories Editor" = "Impossibile creare l'Editor delle Storie"; @@ -8157,9 +7909,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* An error message shown when there is an issue creating new invite links. */ "Unable to create new invite links." = "Impossibile creare nuovi link di invito."; -/* Text displayed in HUD if there was an error attempting to delete a group of media items. */ -"Unable to delete all media items." = "Non è possibile eliminare tutti gli elementi multimediali."; - /* Text displayed in HUD if there was an error attempting to delete a media item. */ "Unable to delete media item." = "Non è possibile eliminare l'elemento multimediale. "; @@ -8223,12 +7972,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Message displayed when sharing a link to the downloadable backup fails. */ "Unable to share link" = "Impossibile condividere il link"; -/* Message that appears when a user tries to trash a page while their device is offline. */ -"Unable to trash pages while offline. Please try again later." = "Impossibile spostare le pagine nel cestino mentre sei offline. Riprova più tardi."; - -/* Message that appears when a user tries to trash a post while their device is offline. */ -"Unable to trash posts while offline. Please try again later." = "Impossibile spostare gli articoli nel cestino mentre sei offline. Riprova più tardi."; - /* Notice title when turning site notifications off fails. */ "Unable to turn off site notifications" = "Impossibile disattivare le notifiche del sito"; @@ -8301,8 +8044,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ Button title. Reverts the previous notification operation Revert an operation Revert enabling notification after successfully subcribing to the comments for the post. - The title of an 'undo' button. Tapping the button moves a trashed page out of the trash folder. - The title of an 'undo' button. Tapping the button moves a trashed post out of the trash folder. Undo action */ "Undo" = "Annulla"; @@ -8345,9 +8086,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title for Unknown HTML Editor */ "Unknown HTML" = "HTML sconosciuto"; -/* Label to use when creation date from media asset is not know. */ -"Unknown creation date" = "Data di creazione sconosciuta"; - /* No comment provided by engineer. */ "Unknown error" = "Errore sconosciuto"; @@ -8513,6 +8251,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of a row displayed on the debug screen used to configure the sandbox store use in the App. */ "Use Sandbox Store" = "Usa il negozio sandbox"; +/* The button's title text to use a security key. */ +"Use a security key" = "Usa una chiave di sicurezza"; + /* Option to enable the block editor for new posts */ "Use block editor" = "Usa editor a blocchi"; @@ -8588,15 +8329,10 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of an alert informing users that the video they are trying to select is not allowed. */ "Video not uploaded" = "Video non caricato"; -/* Accessibility label for video thumbnails in the media collection view. The parameter is the creation date of the video. */ -"Video, %@" = "Video, %@"; - /* Period Stats 'Videos' header */ "Videos" = "Video"; /* Button title. Displays a summary / sharing screen for a specific post. - Label for a button that opens the page when tapped. - Label for the view post button. Tapping displays the post as it appears on the web. Opens the post epilogue screen to allow sharing / viewing of a post. Theme View action title Verb. The screen title shown when viewing a post inside the app. */ @@ -8711,6 +8447,10 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Message shown on screen while waiting for Google to finish its signup process. */ "Waiting for Google to complete…" = "In attesa di Google per il completamento…"; +/* Text while the webauthn signature is being verified + Text while waiting for a security key challenge */ +"Waiting for security key" = "In attesa della chiave di sicurezza"; + /* View title during the Google auth process. */ "Waiting..." = "In attesa..."; @@ -8839,7 +8579,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "We had trouble loading data" = "Abbiamo riscontrato difficoltà con il caricamento dei dati"; /* message to ask a user to check their email for a WordPress.com email */ -"We just emailed a link to %@. Please check your mail app and tap the link to log in." = "Abbiamo appena inviato un link tramite e-mail a %@. Controlla la tua app di posta e tocca il link per accedere."; +"We just emailed a link to %@. Please check your mail app and tap the link to log in." = "Abbiamo appena inviato un link tramite email a %@. Controlla la tua app di posta e tocca il link per accedere."; /* The subtitle text on the magic link requested screen followed by the email address. */ "We just sent a magic link to" = "Abbiamo appena inviato un link magico a"; @@ -8863,7 +8603,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "We were unable to send you an email at this time. Please try again later." = "Al momento non siamo stati in grado di inviarti un'email. Riprova più tardi."; /* Description for label when the actively scanning the users site */ -"We will send you an email if security threats are found. In the meantime feel free to continue to use your site as normal, you can check back on progress at any time." = "Ti invieremo un'e-mail se vengono trovate minacce alla sicurezza. Nel frattempo continua pure a utilizzare tranquillamente il sito come al solito; potrai controllare l'andamento in qualsiasi momento."; +"We will send you an email if security threats are found. In the meantime feel free to continue to use your site as normal, you can check back on progress at any time." = "Ti invieremo un'email se vengono trovate minacce alla sicurezza. Nel frattempo continua pure a utilizzare tranquillamente il sito come al solito; potrai controllare l'andamento in qualsiasi momento."; /* Title for notice displayed on canceling auto-upload published page Title for notice displayed on canceling auto-upload published post */ @@ -8885,7 +8625,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "We'll email you a magic link that'll log you in instantly, no password needed. Hunt and peck no more!" = "Ti invieremo per email un link per accedere subito, senza password. Non ci sarà più bisogno di digitare la password!"; /* Instruction text on the Sign Up screen. */ -"We'll email you a signup link to create your new WordPress.com account." = "Ti invieremo un link di iscrizione via e-mail per creare il tuo nuovo account WordPress.com."; +"We'll email you a signup link to create your new WordPress.com account." = "Ti invieremo un link di iscrizione via email per creare il tuo nuovo account WordPress.com."; /* This is the string we display when asking the user to approve push notifications */ "We'll notify you when you get followers, comments, and likes." = "Ti invieremo delle notifiche quando riceverai follower, commenti e like."; @@ -8921,7 +8661,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "We'll submit your post for review when your device is back online." = "Invieremo il tuo articolo per la revisione quando il dispositivo sarà di nuovo online."; /* Text confirming email address to be used for new account. */ -"We'll use this email address to create your new WordPress.com account." = "Useremo questo indirizzo e-mail per creare un nuovo account WordPress.com."; +"We'll use this email address to create your new WordPress.com account." = "Useremo questo indirizzo email per creare un nuovo account WordPress.com."; /* Description for the Jetpack Backup Status message. %1$@ is a placeholder for the selected date. */ "We're creating a downloadable backup of your site from %1$@." = "Stiamo creando un backup scaricabile del sito dal giorno %1$@."; @@ -8942,10 +8682,10 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "We're sorry, Jetpack Scan is not compatible with multisite WordPress installations at this time." = "Al momento, Jetpack Scan non è compatibile con installazioni WordPress multisito."; /* A hint to users indicating a link to the downloadable backup file has also been sent to their email. */ -"We've also emailed you a link to your file." = "Ti abbiamo anche inviato un'e-mail con un link al file."; +"We've also emailed you a link to your file." = "Ti abbiamo anche inviato un'email con un link al file."; /* Instruction text after a signup Magic Link was requested. */ -"We've emailed you a signup link to create your new WordPress.com account. Check your email on this device, and tap the link in the email you receive from WordPress.com." = "Ti abbiamo inviato via e-mail un link di iscrizione per creare il tuo nuovo account WordPress.com. Controlla la tua e-mail su questo dispositivo e tocca il link nell'e-mail ricevuta da WordPress.com."; +"We've emailed you a signup link to create your new WordPress.com account. Check your email on this device, and tap the link in the email you receive from WordPress.com." = "Ti abbiamo inviato via email un link di iscrizione per creare il tuo nuovo account WordPress.com. Controlla la tua email su questo dispositivo e tocca il link nell'email ricevuta da WordPress.com."; /* Register Domain - error displayed when a domain was purchased succesfully, but there was a problem setting it to a primary domain for the site */ "We've had problems changing the primary domain on your site — but don't worry, your domain was successfully purchased." = "Abbiamo riscontrato problemi nella modifica del dominio principale sul tuo sito, ma non preoccuparti, l'acquisto del tuo dominio è andato a buon fine."; @@ -9082,6 +8822,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* An error message shown when a wpcom user provides the wrong password. */ "Whoops, something went wrong and we couldn't log you in. Please try again!" = "Qualche cosa è andato storto e non siamo riusciti ad autenticarti. Riprova!"; +/* Generic error on the 2FA screen */ +"Whoops, something went wrong. Please try again!" = "Siamo spiacenti, si è verificato un problema. Riprova."; + +/* Error when the uses chooses an invalid security key on the 2FA screen. */ +"Whoops, that security key does not seem valid. Please try again with another one" = "Siamo spiacenti, la chiave di sicurezza sembra non essere valida. Riprova con un'altra chiave"; + /* Error message shown when an incorrect two factor code is provided. */ "Whoops, that's not a valid two-factor verification code. Double-check your code and try again!" = "Questo non è un codice di autenticazione a due fattori valido. Controlla il tuo codice e riprova!"; @@ -9109,9 +8855,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Siri Suggestion to open Support */ "WordPress Help" = "Assistenza WordPress"; -/* Name for the WordPress Media Library */ -"WordPress Media" = "Media WordPress"; - /* Accessibility label for selecting an image or video from the user's WordPress media library on formatting toolbar. */ "WordPress Media Library" = "WordPress Libreria Media"; @@ -9426,9 +9169,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Share extension error dialog text. */ "Your account does not have permission to upload media to this site. The Site Administrator can change these permissions." = "L’account non ha l’autorizzazione per il caricamento di file multimediali su questo sito. L’amministratore del sito può modificare queste autorizzazioni."; -/* Explaining to the user why the app needs access to the device media library. */ -"Your app is not authorized to access media library due to active restrictions such as parental controls. Please check your parental control settings in this device." = "La tua app non è autorizzata ad accedere alla libreria multimediale per restrizioni attive come il parental control. Controlla le impostazioni del parental control su questo dispositivo."; - /* Title for the Jetpack Backup Complete message. */ "Your backup is now available for download" = "Il tuo backup ora è pronto per essere scaricato"; @@ -9447,9 +9187,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of the site address section in the Domains Dashboard. */ "Your free WordPress.com address is" = "Il tuo indirizzo WordPress.com gratuito è"; -/* Error message when picked media cannot be imported into stories. */ -"Your media could not be exported. If the problem persists you can contact us via the Me > Help & Support screen." = "Impossibile esportare i file multimediali. If the problem persists you can contact us via the Me > Schermata di Aiuto e supporto."; - /* Details about recently acquired domain on domain credit redemption success screen */ "Your new domain %@ is being set up. It may take up to 30 minutes for your domain to start working." = "Il tuo nuovo dominio %@ è in fase di configurazione. Potrebbero essere necessari fino a 30 minuti prima che il tuo dominio inizi a funzionare."; @@ -9520,10 +9257,10 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "Your weekly roundup is ready, tap here to see the details!" = "Il tuo resoconto settimanale è pronto, tocca qui per visualizzare i dettagli."; /* Describes the expected behavior when the user disables in-app notifications in Reader Comments. */ -"You’re following this conversation. You will receive an email and a notification whenever a new comment is made." = "Stai seguendo questa conversazione. Riceverai una e-mail e una notifica da qualsiasi luogo venga scritto un nuovo commento."; +"You’re following this conversation. You will receive an email and a notification whenever a new comment is made." = "Stai seguendo questa conversazione. Riceverai una email e una notifica da qualsiasi luogo venga scritto un nuovo commento."; /* Describes the expected behavior when the user enables in-app notifications in Reader Comments. */ -"You’re following this conversation. You will receive an email whenever a new comment is made." = "Stai seguendo questa conversazione. Riceverai una e-mail da qualsiasi luogo venga scritto un nuovo commento."; +"You’re following this conversation. You will receive an email whenever a new comment is made." = "Stai seguendo questa conversazione. Riceverai una email da qualsiasi luogo venga scritto un nuovo commento."; /* Popup content about why this post is being opened in block editor */ "You’re now using the block editor for new pages — great! If you’d like to change to the classic editor, go to ‘My Site’ > ‘Site Settings’." = "Ora stai usando l'editor dei blocchi per le nuove pagine, fantastico. Se desideri passare all'editor classico, vai a \"Il mio sito\" > \"Impostazioni del sito\"."; @@ -9573,8 +9310,38 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* This is the string we display when prompting the user to review the WordPress app */ "appRatings.wordpress.prompt" = "Che cosa pensi di WordPress?"; -/* Label displayed on audio media items. */ -"audio" = "audio"; +/* Option to enable the optimization of images when uploading. */ +"appSettings.media.imageOptimizationRow" = "Ottimizza le immagini"; + +/* Indicates an image will use high quality when uploaded. */ +"appSettings.media.imageQuality.high" = "Alta"; + +/* Indicates an image will use low quality when uploaded. */ +"appSettings.media.imageQuality.low" = "Bassa"; + +/* Indicates an image will use maximum quality when uploaded. */ +"appSettings.media.imageQuality.maximum" = "Massima"; + +/* Indicates an image will use medium quality when uploaded. */ +"appSettings.media.imageQuality.medium" = "Media"; + +/* The quality of image used when uploading */ +"appSettings.media.imageQuality.title" = "Qualità"; + +/* Title for the image quality settings option. */ +"appSettings.media.imageQualityRow" = "Qualità delle immagini"; + +/* Message of an alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.message" = "L'ottimizzazione delle immagini restringe l'immagine per un caricamento più veloce.\n\nQuesta opzione viene attivata per impostazione predefinita, ma puoi modificarla nelle impostazioni dell'app in qualsiasi momento."; + +/* Title of an alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.title" = "Vuoi continuare a ottimizzare le immagini?"; + +/* Title of button for turning off image optimization, displayed in the alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.turnOff" = "No, disattiva"; + +/* Title of button for leaving on image optimization, displayed in the alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.turnOn" = "Si, continua"; /* translators: displays audio file extension. e.g. MP3 audio file */ "audio file" = "file audio"; @@ -9688,7 +9455,41 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "blogHeader.actionCopyURL" = "Copia URL"; /* Context menu button title */ -"blogHeader.actionOpenInBrowser" = "Apri nel browser"; +"blogHeader.actionVisitSite" = "Visualizza il sito"; + +/* Title for a button that, when tapped, shows more info about participating in Bloganuary. */ +"bloganuary.dashboard.card.button.learnMore" = "Scopri di più"; + +/* Title for the Bloganuary dashboard card while Bloganuary is running. */ +"bloganuary.dashboard.card.runningTitle" = "È arrivato Bloganuary!"; + +/* Title for the Bloganuary dashboard card. */ +"bloganuary.dashboard.card.title" = "Bloganuary è in arrivo."; + +/* Title of a button that calls the user to enable the Blogging Prompts feature. */ +"bloganuary.learnMore.modal.button.promptsDisabled" = "Attiva le richieste di blogging"; + +/* Title of a button that will dismiss the Bloganuary modal when tapped. +Note that the word 'go' here should have a closer meaning to 'start' rather than 'move forward'. */ +"bloganuary.learnMore.modal.button.promptsEnabled" = "Iniziamo!"; + +/* The second line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.description.second" = "Pubblica la tua risposta."; + +/* The third line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.description.third" = "Leggi le risposte degli altri blogger per trovare l'ispirazione e creare nuove connessioni."; + +/* The first line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.descriptions.first" = "Ricevi un nuovo suggerimento per ottenere ispirazione ogni giorno."; + +/* An additional piece of information shown in case the user has the Blogging Prompts feature disabled. */ +"bloganuary.learnMore.modal.footer.addition" = "Per unirti a Bloganuary devi attivare le richieste di blogging."; + +/* An informative excerpt shown in a subtler tone. */ +"bloganuary.learnMore.modal.footer.text" = "Bloganuary utilizzerà le richieste di blogging giornaliere per inviarti argomenti per il mese di gennaio."; + +/* The headline text of the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.headline" = "Prendi parte alla nostra sfida di scrittura: dura un mese."; /* Verb. Dismisses the blogging prompt notification. */ "bloggingPrompt.pushNotification.customActionDescription.dismiss" = "Ignora"; @@ -9717,6 +9518,12 @@ Example: Comment on Example: Reply to Pamela Nguyen */ "comment.header.subText.reply" = "Rispondi a %1$@"; +/* Error message informing a user about an invalid password. */ +"common.reEnterPasswordMessage" = "L'aggiornamento della password memorizzata o del nome utente memorizzato nell'app potrebbe non essere stato eseguito. Inserisci nuovamente la password nelle impostazioni e riprova."; + +/* An error message. */ +"common.unableToConnect" = "Impossibile connettersi"; + /* Footnote for the privacy compliance popover. */ "compliance.analytics.popover.footnote" = "Questi cookie ci consentono di ottimizzare le prestazioni raccogliendo informazioni su come gli utenti interagiscono con i nostri siti web."; @@ -9867,50 +9674,92 @@ Example: Reply to Pamela Nguyen */ /* Title for a menu action in the context menu on the Jetpack install card. */ "domain.dashboard.card.menu.hide" = "Nascondi"; -/* Domain Purchase Completion footer */ -"domain.purchase.preview.footer" = "Potrebbero essere necessari fino a 30 minuti prima che il tuo dominio personalizzato inizi a funzionare."; +/* Search domain - Title for the Suggested domains screen */ +"domain.management.addDomain.search.title" = "Cerca un dominio"; -/* Domain Purchase Completion description (only for FREE domains). */ -"domain.purchase.preview.free.description" = "In seguito, ti aiuteremo a prepararlo per essere sfogliato."; +/* Domain management buy domain card button title */ +"domain.management.buy.card.button.title" = "Ottieni il dominio"; -/* Domain Purchase Completion description (only for PAID domains). */ -"domain.purchase.preview.paid.description" = "Ti abbiamo inviato la ricevuta tramite e-mail. In seguito, ti aiuteremo a prepararlo per tutti."; +/* Domain management buy domain card subtitle */ +"domain.management.buy.card.subtitle" = "Aggiungi un sito in seguito."; -/* Reflects that site is live when domain purchase feature flag is ON. */ -"domain.purchase.preview.title" = "Congratulazioni, il tuo sito è attivo."; +/* Domain management buy domain card title */ +"domain.management.buy.card.title" = "Basta acquistare un dominio"; + +/* The expired label of the domain card in All Domains screen. */ +"domain.management.card.expired.label" = "Scaduto"; + +/* The renews label of the domain card in All Domains screen. */ +"domain.management.card.renews.label" = "Rinnovi"; + +/* The empty state button title in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.button.title" = "Trova un dominio"; + +/* The empty state description in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.description" = "Tocca qui sotto per trovare il dominio perfetto."; + +/* The empty state title in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.title" = "Non disponi di alcun dominio"; + +/* The empty state description in All Domains screen when an error occurs */ +"domain.management.error.empty.state.description" = "Abbiamo riscontrato un errore durante il caricamento dei tuoi domini. Se il problema persiste, contatta il supporto."; + +/* The empty state title in All Domains screen when an error occurs */ +"domain.management.error.empty.state.title" = "Si è verificato un problema"; + +/* The empty state button title in All Domains screen when an error occurs */ +"domain.management.error.state.button.title" = "Riprova"; + +/* The empty state description in All Domains screen when the user is offline */ +"domain.management.offline.empty.state.description" = "Controlla la connessione di rete e riprova."; -/* Status of a domain in `Action Required` state */ -"domain.status.action.required" = "Azione necessaria"; +/* The empty state title in All Domains screen when the user is offline */ +"domain.management.offline.empty.state.title" = "Nessuna connessione a Internet"; -/* Status of a domain in `Active` state */ -"domain.status.active" = "Attivo"; +/* Domain management choose site card button title */ +"domain.management.purchase.footer" = "*Dominio gratuito per un anno incluso in tutti i piani annuali a pagamento"; -/* Status of a domain in `Complete Setup` state */ -"domain.status.complete.setup" = "Configurazione completa"; +/* Domain management purchase domain screen title */ +"domain.management.purchase.subtitle" = "Non preoccuparti, puoi facilmente aggiungere un sito più tardi."; -/* Status of a domain in `Error` state */ -"domain.status.error" = "Errore"; +/* Domain management purchase domain screen title. */ +"domain.management.purchase.title" = "Scegli come usare il tuo dominio"; -/* Status of a domain in `Expired` state */ -"domain.status.expired" = "Scaduto"; +/* The search bar title in All Domains screen. */ +"domain.management.search-bar.title" = "Cerca domini"; -/* Status of a domain in `Expiring Soon` state */ -"domain.status.expiring.soon" = "In scadenza a breve"; +/* The empty state description in All Domains screen when the are no domains matching the search criteria */ +"domain.management.search.empty.state.description" = "Non siamo riusciti a trovare alcun dominio che corrisponda alla tua ricerca di \"%@\""; -/* Status of a domain in `Failed` state */ -"domain.status.failed" = "Non riuscito"; +/* The empty state title in All Domains screen when the are no domains matching the search criteria */ +"domain.management.search.empty.state.title" = "Nessun dominio corrispondente trovato"; -/* Status of a domain in `In Progress` state */ -"domain.status.in.progress" = "In corso"; +/* Domain management choose site card button title */ +"domain.management.site.card.button.title" = "Scegli il sito"; -/* Status of a domain in `Renew` state */ -"domain.status.renew" = "Rinnova"; +/* Domain management choose site card subtitle */ +"domain.management.site.card.footer" = "Dominio gratuito per il primo anno*"; -/* Status of a domain in `Verify Email` state */ -"domain.status.verify.email" = "Verifica e-mail"; +/* Domain management choose site card subtitle */ +"domain.management.site.card.subtitle" = "Usa con un sito già esistente."; -/* Status of a domain in `Verifying` state */ -"domain.status.verifying" = "Verifica in corso"; +/* Domain management choose site card title */ +"domain.management.site.card.title" = "Sito WordPress.com esistente"; + +/* Domain Management Screen Title */ +"domain.management.title" = "Tutti i domini"; + +/* Domain Purchase Completion footer */ +"domain.purchase.preview.footer" = "Potrebbero essere necessari fino a 30 minuti prima che il tuo dominio personalizzato inizi a funzionare."; + +/* Domain Purchase Completion description (only for FREE domains). */ +"domain.purchase.preview.free.description" = "In seguito, ti aiuteremo a prepararlo per essere sfogliato."; + +/* Domain Purchase Completion description (only for PAID domains). */ +"domain.purchase.preview.paid.description" = "Ti abbiamo inviato la ricevuta tramite email. In seguito, ti aiuteremo a prepararlo per tutti."; + +/* Reflects that site is live when domain purchase feature flag is ON. */ +"domain.purchase.preview.title" = "Congratulazioni, il tuo sito è attivo."; /* The 'Best Alternative' label under the domain name in 'Choose a domain' screen */ "domain.suggestions.row.best-alternative" = "Alternativa migliore"; @@ -9933,12 +9782,24 @@ Example: Reply to Pamela Nguyen */ /* The text to display for paid domains in 'Site Creation > Choose a domain' screen */ "domain.suggestions.row.yearly" = "all'anno"; +/* Title for the checkout screen. */ +"domains.checkout.title" = "Pagamento"; + /* Action shown in a bottom notice to dismiss it. */ "domains.failure.dismiss" = "Ignora"; /* Content show when the domain selection action fails. */ "domains.failure.title" = "Siamo spiacenti, il dominio che stai cercando di aggiungere non può essere acquistato sull'app di Jetpack in questo momento."; +/* Title for the screen where the user can choose how to use the domain they're end up purchasing. */ +"domains.purchase.choice.title" = "Acquista il dominio"; + +/* Back button title that navigates back to the search domains screen. */ +"domains.search.backButton.title" = "Cerca"; + +/* Title of screen where user chooses a site to connect to their selected domain */ +"domains.sitePicker.title" = "Scegli il sito"; + /* No comment provided by engineer. */ "double-tap to change unit" = "tocca due volte per modificare l'unità"; @@ -9956,6 +9817,15 @@ Example: Reply to Pamela Nguyen */ Site Address placeholder */ "example.com" = "esempio.com"; +/* Title for confirmation navigation bar button item */ +"externalMediaPicker.add" = "Aggiungi"; + +/* Bottom toolbar title in the selection mode */ +"externalMediaPicker.toolbarSelectItemsPrompt" = "Seleziona le immagini"; + +/* Bottom toolbar title in the selection mode */ +"externalMediaPicker.toolbarViewSelected" = "Vista selezionata (%@)"; + /* Title of screen the displays the details of an advertisement campaign. */ "feature.blaze.campaignDetails.title" = "Dettagli della campagna"; @@ -10055,9 +9925,6 @@ Example: Reply to Pamela Nguyen */ /* (placeholder) Help the user enter a URL into the field */ "http:\/\/my-site-address (URL)" = "http:\/\/indirizzo-del-sito (URL)"; -/* Label displayed on image media items. */ -"image" = "Immagine"; - /* Sentence to justify why the app is asking permission from the user to use their camera. */ "infoplist.NSCameraUsageDescription" = "Per acquisire foto o video da usare nei tuoi articoli."; @@ -10358,6 +10225,9 @@ Example: Reply to Pamela Nguyen */ /* Indicating that referrer was marked as spam */ "marked as spam" = "contrassegnato come spam"; +/* Products header text in Me Screen. */ +"me.products.header" = "Prodotti"; + /* Title of error prompt shown when a sync fails. */ "media.syncFailed" = "Impossibile sincronizzare gli elementi multimediali"; @@ -10370,18 +10240,24 @@ Example: Reply to Pamela Nguyen */ /* Message of an alert informing users that the video they are trying to select is not allowed. */ "mediaExporter.videoLimitExceededError" = "Per caricare i video più lunghi di 5 minuti è richiesto un piano a pagamento."; -/* Verb. User action to dismiss error alert when failing to load media item. */ -"mediaItemTable.errorAlert.dismissButton" = "Ignora"; - /* Accessibility hint for add button to add items to the user's media library */ "mediaLibrary.addButtonAccessibilityHint" = "Aggiungi nuovo elemento multimediale"; /* Accessibility label for add button to add items to the user's media library */ "mediaLibrary.addButtonAccessibilityLabel" = "Aggiungi"; +/* Button name in the more menu */ +"mediaLibrary.aspectRatioGrid" = "Griglia proporzioni"; + +/* Context menu button */ +"mediaLibrary.buttonDelete" = "Elimina"; + /* Media screen navigation bar button Select title */ "mediaLibrary.buttonSelect" = "Seleziona"; +/* Context menu button */ +"mediaLibrary.buttonShare" = "Condividi"; + /* Verb. Button title. Tapping cancels an action. */ "mediaLibrary.deleteConfirmationCancel" = "Annulla"; @@ -10389,10 +10265,10 @@ Example: Reply to Pamela Nguyen */ "mediaLibrary.deleteConfirmationConfirm" = "Elimina"; /* Message prompting the user to confirm that they want to permanently delete a group of media items. */ -"mediaLibrary.deleteConfirmationMessageMany" = "Desideri eliminare in modo permanente questi elementi?"; +"mediaLibrary.deleteConfirmationMessageMany" = "Vuoi davvero eliminare in modo permanente questi elementi?"; /* Message prompting the user to confirm that they want to permanently delete a media item. Should match Calypso. */ -"mediaLibrary.deleteConfirmationMessageOne" = "Desideri eliminare in modo permanente questo elemento?"; +"mediaLibrary.deleteConfirmationMessageOne" = "Vuoi davvero eliminare in modo permanente questo elemento?"; /* Text displayed in HUD if there was an error attempting to delete a group of media items. */ "mediaLibrary.deletionFailureMessage" = "Non è possibile eliminare tutti gli elementi multimediali."; @@ -10403,6 +10279,21 @@ Example: Reply to Pamela Nguyen */ /* Text displayed in HUD after successfully deleting a media item */ "mediaLibrary.deletionSuccessMessage" = "Elemento eliminato."; +/* The name of the media filter */ +"mediaLibrary.filterAll" = "Tutti"; + +/* The name of the media filter */ +"mediaLibrary.filterAudio" = "Audio"; + +/* The name of the media filter */ +"mediaLibrary.filterDocuments" = "Documenti"; + +/* The name of the media filter */ +"mediaLibrary.filterImages" = "Immagini"; + +/* The name of the media filter */ +"mediaLibrary.filterVideos" = "Video"; + /* User action to delete un-uploaded media. */ "mediaLibrary.retryOptionsAlert.delete" = "Elimina"; @@ -10415,6 +10306,12 @@ Example: Reply to Pamela Nguyen */ /* Message displayed when no results are returned from a media library search. Should match Calypso. */ "mediaLibrary.searchResultsEmptyTitle" = "Nessun elemento multimediale corrispondente alla ricerca"; +/* Text displayed in HUD if there was an error attempting to share a group of media items. */ +"mediaLibrary.sharingFailureMessage" = "Impossibile condividere gli elementi selezionati."; + +/* Button name in the more menu */ +"mediaLibrary.squareGrid" = "Griglia quadrata"; + /* Bottom toolbar title in the selection mode */ "mediaLibrary.toolbarSelectImagesMany" = "%d immagini selezionate"; @@ -10433,6 +10330,12 @@ Example: Reply to Pamela Nguyen */ /* The title of the button to dismiss the alert shown when the picked media cannot be imported into stories. */ "mediaPicker.failedMediaExportAlert.dismissButton" = "Ignora"; +/* Error message when picked media cannot be imported into stories. */ +"mediaPicker.failedMediaExportAlert.message" = "Impossibile esportare gli elementi multimediali. Se il problema persiste, puoi contattarci tramite la schermata Io > Schermata di Aiuto e supporto."; + +/* Error title when picked media cannot be imported into stories. */ +"mediaPicker.failedMediaExportAlert.title" = "Esportazione degli elementi multimediali non riuscita"; + /* Message for alert when access to camera is not granted */ "mediaPicker.noCameraAccessMessage" = "Questa app richiede l'autorizzazione a poter accedere alla fotocamera per acquisire nuovi elementi multimediali. Se desideri che ciò avvenga, modifica le impostazioni della privacy."; @@ -10466,6 +10369,12 @@ Example: Reply to Pamela Nguyen */ /* The name of the action in the context menu */ "mediaPicker.takeVideo" = "Fai un video"; +/* Navigation title for media preview. Example: 1 of 3 */ +"mediaPreview.NofM" = "%1$@ di %2$@"; + +/* Max image size in pixels (e.g. 300x300px) */ +"mediaSizeSlider.valueFormat" = "%1$dx%2$d px"; + /* The description in the Delete WordPress screen */ "migration.deleteWordpress.description" = "Sembra che tu abbia ancora l'app WordPress installata."; @@ -10478,9 +10387,6 @@ Example: Reply to Pamela Nguyen */ /* The title in the Delete WordPress screen */ "migration.deleteWordpress.title" = "Non hai più bisogno dell'app WordPress sul dispositivo"; -/* Primary button title in the migration done screen. */ -"migration.done.actions.primary.title" = "Finito"; - /* Footer for the migration done screen. */ "migration.done.footer" = "Ti consigliamo di disinstallare l'app WordPress sul dispositivo per evitare conflitti di dati."; @@ -10490,6 +10396,9 @@ Example: Reply to Pamela Nguyen */ /* Primary description in the migration done screen. */ "migration.done.primaryDescription" = "Abbiamo trasferito tutti i tuoi dati e le impostazioni. Tutto è dove lo hai lasciato."; +/* Secondary description (second paragraph) in the migration done screen. */ +"migration.done.secondaryDescription" = "È ora di continuare il tuo viaggio con WordPress sull'app Jetpack."; + /* Title of the migration done screen. */ "migration.done.title" = "Grazie per aver scelto di passare a Jetpack!"; @@ -10538,6 +10447,9 @@ Example: Reply to Pamela Nguyen */ /* The title in the migration welcome screen */ "migration.welcome.title" = "Jetpack ti dà il benvenuto!"; +/* Primary button title in the migration done screen. */ +"migrationDone.actions.primaryTitle" = "Iniziamo"; + /* Description for the static screen displayed prompting users to switch the Jetpack app. */ "movedToJetpack.description" = "L'app Jetpack offre tutte le funzionalità dell'app WordPress e ora anche l'accesso esclusivo a Statistiche, Reader, Notifiche e altro."; @@ -10613,6 +10525,30 @@ Example: Reply to Pamela Nguyen */ /* Message title for when a user has no sites. */ "mySite.noSites.title" = "Non sono presenti siti"; +/* Menu title for the add site option */ +"mySite.siteActions.addSite" = "Aggiungi sito"; + +/* Button that reveals more site actions */ +"mySite.siteActions.button" = "Azioni del sito"; + +/* Accessibility hint for button used to show more site actions */ +"mySite.siteActions.hint" = "Clicca per mostrare più azioni del sito"; + +/* Menu title for the personalize home option */ +"mySite.siteActions.personalizeHome" = "Personalizza la home"; + +/* Menu title for the change site icon option */ +"mySite.siteActions.siteIcon" = "Cambia l'icona del sito"; + +/* Menu title for the change site title option */ +"mySite.siteActions.siteTitle" = "Modifica il titolo del sito"; + +/* Menu title for the switch site option */ +"mySite.siteActions.switchSite" = "Cambia sito"; + +/* Menu title for the visit site option */ +"mySite.siteActions.visitSite" = "Visualizza il sito"; + /* Dismiss button title. */ "noResultsViewController.dismissButton" = "Ignora"; @@ -10628,14 +10564,17 @@ Example: Reply to Pamela Nguyen */ /* This is one of the buttons we display when prompting the user for a review */ "notifications.appRatings.sendFeedback.yes.buttonTitle" = "Invia feedback"; -/* Word separating the current index from the total amount. I.e.: 7 of 9 */ -"of" = "di"; +/* Badge for page cells */ +"pageList.badgeLocalChanges" = "Modifiche locali"; -/* Label displayed on media items that are not video, image, or audio. */ -"other" = "altro"; +/* Badge for page cells */ +"pageList.badgePendingReview" = "In attesa di revisione"; -/* Promote the page with Blaze. */ -"pages.blaze.actionTitle" = "Promuovi con la Blaze"; +/* Badge for page cells */ +"pageList.badgePosts" = "Pagina degli articoli"; + +/* Badge for page cells */ +"pageList.badgePrivate" = "Privato"; /* Subtitle of the theme template homepage cell */ "pages.template.subtitle" = "La home page utilizza un modello di tema e si aprirà nell'editor web."; @@ -10643,6 +10582,36 @@ Example: Reply to Pamela Nguyen */ /* Title of the theme template homepage cell */ "pages.template.title" = "Home page"; +/* Message informing the user that their static homepage page was set successfully */ +"pages.updatePage.successTitle" = "Pagina aggiornata correttamente"; + +/* Delete option in the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.actionTitle" = "Elimina in modo permanente"; + +/* Message of the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.alertMessage" = "Desideri eliminare in modo permanente questa pagina?"; + +/* Title of the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.alertTitle" = "Eliminare in modo permanente?"; + +/* Menu option for filtering posts by everyone */ +"pagesList.pagesByEveryone" = "Pagine di tutti"; + +/* Menu option for filtering posts by me */ +"pagesList.pagesByMe" = "Le mie pagine"; + +/* Trash option in the trash page confirmation alert. */ +"pagesList.trash.actionTitle" = "Sposta nel cestino"; + +/* Message of the trash page confirmation alert. */ +"pagesList.trash.alertMessage" = "Desideri spostare questa pagina nel cestino?"; + +/* Title of the trash page confirmation alert. */ +"pagesList.trash.alertTitle" = "Spostare la pagina nel cestino?"; + +/* Cancels an Action */ +"pagesList.trash.cancel" = "Annulla"; + /* No comment provided by engineer. */ "password" = "password"; @@ -10682,6 +10651,48 @@ Example: Reply to Pamela Nguyen */ /* Register Domain - Domain contact information field Phone */ "phone number" = "numero di telefono"; +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.createdTimeAgo" = "Creazione: %@"; + +/* Status mesasge for post cells */ +"post.deletingPostPermanentlyStatusMessage" = "Eliminazione dell'articolo in corso..."; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.editedTimeAgo" = "Modifica: %@"; + +/* Status mesasge for post cells */ +"post.movingToTrashStatusMessage" = "Spostamento dell'articolo nel cestino in corso..."; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.publishedTimeAgo" = "Pubblicazione: %@"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.scheduledForDate" = "Pianificazione: %@"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.trashedTimeAgo" = "Spostamento nel cestino: %@"; + +/* Accessibility label for the post author in the post list. The parameter is the author name. For example, \"By Elsa.\" */ +"postList.a11y.authorChunkFormat" = "Di %@."; + +/* Accessibility label for a post's excerpt in the post list. The parameter is the post excerpt. For example, \"Excerpt. This is the first paragraph.\" */ +"postList.a11y.exerptChunkFormat" = "Estratto. %@."; + +/* Accessibility label for a sticky post in the post list. */ +"postList.a11y.sticky" = "In evidenza."; + +/* Title for the 'Trash' post list row swipe action */ +"postList.swipeActionDelete" = "Cestino"; + +/* Title for the 'Delete' post list row swipe action */ +"postList.swipeActionDeletePermanently" = "Elimina"; + +/* Title for the 'Share' post list row swipe action */ +"postList.swipeActionShare" = "Condividi"; + +/* Title for the 'View' post list row swipe action */ +"postList.swipeActionView" = "Visualizza"; + /* User action to dismiss featured media options. */ "postSettings.featuredImageUploadActionSheet.dismiss" = "Ignora"; @@ -10700,9 +10711,84 @@ Example: Reply to Pamela Nguyen */ /* Button in Post Settings */ "postSettings.setFeaturedImageButton" = "Imposta l'immagine in evidenza"; +/* Error message on post/page settings screen */ +"postSettings.updateFailedMessage" = "Impossibile aggiornare le impostazioni dell'articolo"; + /* Promote the post with Blaze. */ "posts.blaze.actionTitle" = "Promuovi con la Blaze"; +/* Label for the Post List option that cancels automatic uploading of a post. */ +"posts.cancelUpload.actionTitle" = "Annulla caricamento"; + +/* Label for post comments option. Tapping displays comments for a post. */ +"posts.comments.actionTitle" = "Commenti"; + +/* Label for the delete post option. Tapping permanently deletes a post. */ +"posts.delete.actionTitle" = "Elimina in modo permanente"; + +/* Label for an option that moves a post to the draft folder */ +"posts.draft.actionTitle" = "Sposta in bozze"; + +/* Label for post duplicate option. Tapping creates a copy of the post. */ +"posts.duplicate.actionTitle" = "Duplica"; + +/* Opens a submenu for page attributes. */ +"posts.pageAttributes.actionTitle" = "Attributi della pagina"; + +/* Label for the preview post button. Tapping displays the post as it appears on the web. */ +"posts.preview.actionTitle" = "Anteprima"; + +/* Label for an option that moves a publishes a post immediately */ +"posts.publish.actionTitle" = "Pubblica ora"; + +/* Retry uploading the post. */ +"posts.retry.actionTitle" = "Riprova"; + +/* Set the selected page as the homepage. */ +"posts.setHomepage.actionTitle" = "Imposta come homepage"; + +/* Set the parent page for the selected page. */ +"posts.setParent.actionTitle" = "Imposta principale"; + +/* Set the selected page as a posts page. */ +"posts.setPostsPage.actionTitle" = "Imposta come pagina degli articoli"; + +/* Set the selected page as a regular page. */ +"posts.setRegularPage.actionTitle" = "Imposta come pagina regolare"; + +/* Label for post settings option. Tapping displays settings for a post. */ +"posts.settings.actionTitle" = "Impostazioni"; + +/* Share the post. */ +"posts.share.actionTitle" = "Condividi"; + +/* Label for post stats option. Tapping displays statistics for a post. */ +"posts.stats.actionTitle" = "Statistiche"; + +/* Label for a option that moves a post to the trash folder */ +"posts.trash.actionTitle" = "Sposta nel cestino"; + +/* Label for the view post button. Tapping displays the post as it appears on the web. */ +"posts.view.actionTitle" = "Visualizza"; + +/* A short message explaining that a page was deleted permanently. */ +"postsList.deletePage.message" = "Pagina eliminata in modo permanente"; + +/* A short message explaining that a post was deleted permanently. */ +"postsList.deletePost.message" = "Articolo eliminato in modo permanente"; + +/* A short message explaining that a page was moved to the trash bin. */ +"postsList.movePageToTrash.message" = "Pagina spostata nel cestino"; + +/* A short message explaining that a post was moved to the trash bin. */ +"postsList.movePostToTrash.message" = "Articolo spostato nel cestino"; + +/* Menu option for filtering posts by everyone */ +"postsList.postsByEveryone" = "Articoli di tutti"; + +/* Menu option for filtering posts by me */ +"postsList.postsByMe" = "I miei articoli"; + /* Title for the button to subscribe to Jetpack Social on the remaining shares view */ "postsettings.social.remainingshares.subscribe" = "Iscriviti ora per avere più condivisioni"; @@ -10847,13 +10933,15 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Text for the 'Like' button on the reader post card cell. */ "reader.post.button.like" = "Mi piace"; -/* Accessibility hint for the like button on the reader post card cell - Accessibility hint for the liked button on the reader post card cell */ +/* Accessibility hint for the like button on the reader post card cell */ "reader.post.button.like.accessibility.hint" = "Mette Mi piace all'articolo."; /* Text for the 'Liked' button on the reader post card cell. */ "reader.post.button.liked" = "Con Mi piace"; +/* Accessibility hint for the liked button on the reader post card cell */ +"reader.post.button.liked.accessibility.hint" = "Non mi piace più."; + /* Accessibility hint for the site header on the reader post card cell */ "reader.post.button.menu.accessibility.hint" = "Apre un menu con altre azioni."; @@ -10917,6 +11005,12 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Title for the tooltip anchor. */ "readerDetail.tooltipAnchorTitle.accessibilityLabel" = "Nuovo"; +/* The button title for the transfer footer view in Register Domain screen */ +"register.domain.transfer.button.title" = "Trasferisci il dominio"; + +/* The title for the transfer footer view in Register Domain screen */ +"register.domain.transfer.title" = "Vuoi trasferire un dominio che già possiedi?"; + /* Information of what related post are and how they are presented */ "relatedPostsSettings.optionsFooter" = "Articoli correlati visualizza contenuti pertinenti del tuo sito sotto i tuoi articoli."; @@ -11016,11 +11110,26 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Accessibility hint for actions when displaying media items. */ "siteMedia.cellAccessibilityHint" = "Seleziona elemento multimediale."; +/* Accessibility hint for media item preview for user's viewing an item in their media library */ +"siteMediaItem.contentViewAccessibilityHint" = "Tocca per visualizzare l'elemento multimediale a schermo intero"; + +/* Accessibility label for media item preview for user's viewing an item in their media library */ +"siteMediaItem.contentViewAccessibilityLabel" = "Visualizza in anteprima elemento multimediale"; + +/* Title for confirmation navigation bar button item */ +"siteMediaPicker.add" = "Aggiungi"; + +/* Button selection media in media picker */ +"siteMediaPicker.deselect" = "Deseleziona"; + +/* Button selection media in media picker */ +"siteMediaPicker.select" = "Seleziona"; + /* Title for screen to select the privacy options for a blog */ "siteSettings.privacy.title" = "Privacy"; /* Hint for users when hidden privacy setting is set */ -"siteVisibility.hidden.hint" = "Il tuo sito è visibile a chiunque, ma chiede ai motori di ricerca di non indicizzarlo."; +"siteVisibility.hidden.hint" = "Il tuo sito resterà invisibile ai visitatori dietro un avviso \"Presto disponibile\" finché non sarà pronto per essere mostrato."; /* Text for privacy settings: Hidden */ "siteVisibility.hidden.title" = "Nascosto"; @@ -11181,6 +11290,12 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Dismiss the AlertView */ "stockPhotos.strings.dismiss" = "Ignora"; +/* Subtitle for placeholder in Free Photos. The company name 'Pexels' should always be written as it is. */ +"stockPhotos.subtitle" = "Foto fornite da Pexels"; + +/* Title for placeholder in Free Photos */ +"stockPhotos.title" = "Cerca per trovare foto gratuite da aggiungere alla tua Libreria multimediale!"; + /* Section title for prominent suggestions */ "suggestions.section.prominent" = "In questa conversazione"; @@ -11272,13 +11387,13 @@ Example: given a notice format "Following %@" and empty site name, this will be "support.row.communityForum.title" = "Fai una domanda nel forum della community e lasciati aiutare dal nostro gruppo di volontari."; /* Accessibility hint describing what happens if the Contact Email button is tapped. */ -"support.row.contactEmail.accessibilityHint" = "Visualizza una casella di dialogo per modificare l'e-mail di contatto."; +"support.row.contactEmail.accessibilityHint" = "Visualizza una casella di dialogo per modificare l'email di contatto."; /* Display value for Support email field if there is no user email address. */ "support.row.contactEmail.emailNoteSet.detail" = "Non impostato"; /* Support email label. */ -"support.row.contactEmail.title" = "E-mail"; +"support.row.contactEmail.title" = "Email"; /* Option in Support view to contact the support team. */ "support.row.contactUs.title" = "Contatta il supporto"; @@ -11287,7 +11402,7 @@ Example: given a notice format "Following %@" and empty site name, this will be "support.row.debug.title" = "Debug"; /* Support email label. */ -"support.row.email.title" = "E-mail"; +"support.row.email.title" = "Email"; /* Option in Support view to view the Forums. */ "support.row.forums.title" = "Forum di WordPress"; @@ -11328,6 +11443,9 @@ Example: given a notice format "Following %@" and empty site name, this will be /* View title for Help & Support page. */ "support.title" = "Aiuto"; +/* Title for placeholder in Tenor picker */ +"tenor.welcomeMessage" = "Cerca per trovare GIF da aggiungere alla tua Libreria multimediale!"; + /* Header of delete screen section listing things that will be deleted. */ "these items will be deleted:" = "questi elementi verranno eliminati:"; @@ -11343,9 +11461,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Displayed in the confirmation alert when marking unread notifications as read. */ "unread" = "non lette"; -/* Label displayed on video media items. */ -"video" = "video"; - /* Portion of a message for Jetpack users that have multisite WP installation, thus Restore is not available. This part is a link, colored with a different color. */ "visit our documentation page" = "visita la pagina della documentazione"; diff --git a/WordPress/Resources/ja.lproj/Localizable.strings b/WordPress/Resources/ja.lproj/Localizable.strings index bcadda75f5a7..f60bcce90293 100644 --- a/WordPress/Resources/ja.lproj/Localizable.strings +++ b/WordPress/Resources/ja.lproj/Localizable.strings @@ -1,4 +1,4 @@ -/* Translation-Revision-Date: 2023-10-19 11:54:08+0000 */ +/* Translation-Revision-Date: 2024-01-04 11:54:09+0000 */ /* Plural-Forms: nplurals=1; plural=0; */ /* Generator: GlotPress/4.0.0-alpha.11 */ /* Language: ja_JP */ @@ -152,9 +152,6 @@ /* Difference label for Period Overview stat, indicating change from previous period. Ex: +99.9K (5%) */ "%@%@ (%@%%)" = "%1$@%2$@ (%3$@%%)"; -/* Accessibility label for a post in the post list. The parameters are the title, and date respectively. For example, \"Let it Go, 1 hour ago.\" */ -"%@, %@." = "%1$@、%2$@。"; - /* Accessibility value for a Stats' Posting Activity Month if the user selected a day with posts. The first parameter is day (e.g. November 2019). The second parameter is the number of posts. */ "%@. %d posts." = "%1$@。%2$d件の投稿。"; @@ -210,9 +207,6 @@ /* Age between dates over one year. */ "%d years" = "%d年"; -/* Max image size in pixels (e.g. 300x300px) */ -"%dx%dpx" = "%1$dx%2$dピクセル"; - /* One menu area available in the theme */ "%i menu area in this theme" = "このテーマにはメニューエリアが%i個あります"; @@ -271,6 +265,9 @@ translators: Block name. %s: The localized block name */ /* translators: %s: social link name e.g: \"Instagram\". */ "%s social icon" = "%s ソーシャルアイコン"; +/* translators: displayed right after the classic block is converted to blocks. %s: The localized classic block name */ +"'%s' block converted to blocks" = "「%s」ブロックがブロックに変換されました"; + /* translators: Missing block alert title. %s: The localized block name */ "'%s' is not fully-supported" = "「%s」は完全にはサポートされていません"; @@ -478,13 +475,6 @@ translators: Block name. %s: The localized block name */ /* Label for the Activity Type filter button when there are more than 1 activity type selected */ "Activity Type (%1$d)" = "アクティビティタイプ (%1$d)"; -/* Accessibility label for add button to add items to the user's media library - Remove asset from media picker list */ -"Add" = "追加"; - -/* Action for Media Picker to indicate selection of media. The argument in the string represents the number of elements (as numeric digits) selected */ -"Add %@" = "%@ を追加"; - /* No comment provided by engineer. */ "Add Block After" = "後にブロックを追加"; @@ -581,9 +571,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Add menu item to children" = "子項目としてメニュー項目を追加"; -/* Accessibility hint for add button to add items to the user's media library */ -"Add new media" = "新しいメディアを追加"; - /* Screen reader text for Menus button that adds a new menu to a site. */ "Add new menu" = "新規メニューを追加"; @@ -649,9 +636,6 @@ translators: Block name. %s: The localized block name */ /* Option to select the Airmail app when logging in with magic links */ "Airmail" = "Airmail"; -/* Description of albums in the photo libraries */ -"Albums" = "アルバム"; - /* Image alignment option title. Title of the screen for choosing an image's alignment. */ "Alignment" = "配置"; @@ -665,6 +649,9 @@ translators: Block name. %s: The localized block name */ Title of the drafts filter. This filter shows a list of draft posts. */ "All" = "すべて"; +/* Information about redeeming domain credit on site dashboard. */ +"All WordPress.com annual plans include a custom domain name. Register your free domain now." = "WordPress.com のすべての年間プランにはカスタムドメイン名が含まれています。 無料ドメインを登録してください。"; + /* Footer of the free domain registration section for a paid plan. Information about redeeming domain credit on site dashboard. */ "All WordPress.com plans include a custom domain name. Register your free premium domain now." = "WordPress.com のすべてのプランにはカスタムドメイン名があります。無料プレミアムドメインを登録してください。"; @@ -730,10 +717,13 @@ translators: Block name. %s: The localized block name */ "Alt Text" = "代替テキスト"; /* No comment provided by engineer. */ -"Alternatively, you can detach and edit these blocks separately by tapping “Detach patterns”." = "または、「パターンを切り離す」をタップして、これらのブロックを個別に切り離して編集できます。"; +"Alternatively, you can convert the content to blocks." = "またはコンテンツをブロックに変換することもできます。"; /* No comment provided by engineer. */ -"Alternatively, you can detach and edit this block separately by tapping “Detach pattern”." = "または、「パターンを切り離す」をタップして、このブロックを個別に切り離して編集できます。"; +"Alternatively, you can detach and edit this block separately by tapping “Detach”." = "または、「切り離す」をタップして、このブロックを個別に切り離して編集できます。"; + +/* translators: Alternative option included in a warning related to having blocks deeply nested. */ +"Alternatively, you can flatten the content by ungrouping the block." = "あるいは、ブロックのグループ化を解除してコンテンツをフラットにすることもできます。"; /* Instruction text to explain to help users type their password instead of using magic link login option. */ "Alternatively, you may enter the password for this account." = "または、このアカウントのパスワードを入力することもできます。"; @@ -882,15 +872,9 @@ translators: Block name. %s: The localized block name */ /* Message prompting the user to confirm that they want to disconnect Jetpack from the site. */ "Are you sure you want to disconnect Jetpack from the site?" = "このサイトから Jetpack の連携を解除してもよいですか ?"; -/* Message prompting the user to confirm that they want to permanently delete a group of media items. */ -"Are you sure you want to permanently delete these items?" = "本当にこれらの項目を永久に削除しますか ?"; - /* Message prompting the user to confirm that they want to permanently delete a media item. Should match Calypso. */ "Are you sure you want to permanently delete this item?" = "この項目を完全に削除してもよいですか ?"; -/* Message of the confirmation alert when deleting a page from the trash. */ -"Are you sure you want to permanently delete this page?" = "このページを完全に削除しますか ?"; - /* Message of the confirmation alert when deleting a post from the trash. */ "Are you sure you want to permanently delete this post?" = "この投稿を完全に削除してもよいですか ?"; @@ -922,9 +906,6 @@ translators: Block name. %s: The localized block name */ /* Title of message shown when user taps submit for review. */ "Are you sure you want to submit for review?" = "レビュー待ちとして送信してもよいですか ?"; -/* Message of the trash page confirmation alert. */ -"Are you sure you want to trash this page?" = "本当にこのページをゴミ箱に移動しますか ?"; - /* Message of the trash confirmation alert. */ "Are you sure you want to trash this post?" = "この投稿を本当にゴミ箱へ移動してよいですか ?"; @@ -965,9 +946,6 @@ translators: Block name. %s: The localized block name */ /* translators: accessibility text. Empty Audio caption. */ "Audio caption. Empty" = "音声キャプション。 空"; -/* Accessibility label for audio items in the media collection view. The parameter is the creation date of the audio. */ -"Audio, %@" = "音声ファイル、%@"; - /* No comment provided by engineer. */ "Authenticating" = "認証中"; @@ -1178,10 +1156,7 @@ translators: Block name. %s: The localized block name */ "Blocks menu" = "ブロックのメニュー"; /* translators: Warning related to having blocks deeply nested. %d: The deepest nesting level. */ -"Blocks nested deeper than %d levels may not render properly in the mobile editor. For this reason, we recommend flattening the content by ungrouping the block or editing the block using the web editor." = "%dレベルよりも深くネストされたブロックは、モバイルエディターで適切にレンダリングされない場合があります。 このため、ブロックのグループを解除するか Web エディターでブロックを編集して、コンテンツをフラット化することをお勧めします。"; - -/* translators: Warning related to having blocks deeply nested. %d: The deepest nesting level. */ -"Blocks nested deeper than %d levels may not render properly in the mobile editor. For this reason, we recommend flattening the content by ungrouping the block or editing the block using your web browser." = "%dレベルよりも深くネストされたブロックは、モバイルエディターで適切にレンダリングされない場合があります。 このため、ブロックのグループを解除するか Web ブラウザーでブロックを編集して、コンテンツをフラット化することをお勧めします。"; +"Blocks nested deeper than %d levels may not render properly in the mobile editor." = "%dレベルよりも深くネストされたブロックは、モバイルエディターで適切にレンダリングされない場合があります。"; /* Title of a button that displays the WordPress.com blog */ "Blog" = "ブログ"; @@ -1259,9 +1234,6 @@ translators: Block name. %s: The localized block name */ /* Label for the post author in the post detail. */ "By " = "By "; -/* Accessibility label for the post author in the post list. The parameter is the author name. For example, \"By Elsa.\" */ -"By %@." = "作成者 : %@。"; - /* Legal disclaimer for logging in. The underscores _..._ denote underline. */ "By continuing, you agree to our _Terms of Service_." = "継続すると、利用規約に合意したことになります。"; @@ -1281,8 +1253,7 @@ translators: Block name. %s: The localized block name */ /* Label for size of media while it's being calculated. */ "Calculating..." = "計算中..."; -/* Accessibility label for taking an image or video with the camera on formatting toolbar. - Accessibility label for the camera tile in the collection view */ +/* Accessibility label for taking an image or video with the camera on formatting toolbar. */ "Camera" = "カメラ"; /* Title of an alert informing the user the camera permission for the app is disabled and its needed to proceed */ @@ -1333,10 +1304,7 @@ translators: Block name. %s: The localized block name */ Cancels an Action Cancels an alert. Cancels the mark all as read action. - Dismiss the post action sheet Editing GIF alert cancel action button. - Label for a cancel button - Label for the auto-upload cancelation button in the post list. Tapping will prevent the app from auto-uploading the post. Menus cancel button for deleting a menu. Menus cancel button within text bar while editing items. Menus: Cancel button title for canceling an edited menu item. @@ -1353,10 +1321,6 @@ translators: Block name. %s: The localized block name */ Verb. Title for Jetpack Restore cancel button. */ "Cancel" = "キャンセル"; -/* Label for the Post List option that cancels automatic uploading of a post. - Media Library option to cancel an in-progress or failed upload. */ -"Cancel Upload" = "アップロードのキャンセル"; - /* Menus alert button title to cancel discarding changes and not createa a new menu. Menus alert button title to cancel discarding changes and not select a new menu Menus alert button title to cancel discarding changes and not select a new menu location */ @@ -1415,9 +1379,6 @@ translators: Block name. %s: The localized block name */ Main title */ "Change Password" = "パスワードの変更"; -/* Title of button that takes user to the system Settings section for the app */ -"Change Settings" = "設定を変更"; - /* Change Username title. */ "Change Username" = "ユーザー名の変更"; @@ -1557,9 +1518,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Choose file" = "ファイルを選択"; -/* Menu option for selecting media from the device's photo library. */ -"Choose from My Device" = "自分の端末から選択"; - /* Explanatory text for Homepage Settings homepage type selection. */ "Choose from a homepage that displays your latest posts (classic blog) or a fixed \/ static page." = "最新の投稿を表示するホームページ (クラシックブログ) か固定ページから選択してください。"; @@ -1760,9 +1718,6 @@ translators: Block name. %s: The localized block name */ /* Community & Non-Profit site intent topic */ "Community & Non-Profit" = "コミュニティ & 非営利"; -/* Accessibility indication that the current Post List style is currently Compact. */ -"Compact" = "コンパクト"; - /* The action is completed */ "Completed" = "完了"; @@ -1948,10 +1903,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Copied block" = "コピーしたブロック"; -/* Copy the post url and paste anywhere in phone - Label for page copy link. Tapping copy the url of page */ -"Copy Link" = "リンクをコピー"; - /* Copy link to oomment button title */ "Copy Link to Comment" = "コメントへのリンクをコピー"; @@ -2060,9 +2011,6 @@ translators: Block name. %s: The localized block name */ /* Error title displayed when unable to close user account. */ "Couldn’t close account automatically" = "アカウントを自動的にクローズできませんでした"; -/* Message to show while media data source is finding the number of items available. */ -"Counting media items..." = "メディア項目を数えています…"; - /* Period Stats 'Countries' header */ "Countries" = "国"; @@ -2313,7 +2261,6 @@ translators: Block name. %s: The localized block name */ /* Delete Delete button title for the warning shown to the user when he refuses to re-login when the authToken is missing. Title for button that permanently deletes a media item (photo / video) - Title for button that permanently deletes one or more media items (photos / videos) Title of a delete button Title of the trash confirmation alert. */ "Delete" = "削除"; @@ -2321,15 +2268,11 @@ translators: Block name. %s: The localized block name */ /* Menus confirmation button for deleting a menu. */ "Delete Menu" = "メニューを削除"; -/* Delete option in the confirmation alert when deleting a page from the trash. - Delete option in the confirmation alert when deleting a post from the trash. - Label for a button permanently deletes a page. - Label for the delete post option. Tapping permanently deletes a post. +/* Delete option in the confirmation alert when deleting a post from the trash. Title for button on the comment details page that deletes the comment when tapped. */ "Delete Permanently" = "完全に削除する"; -/* Title of the confirmation alert when deleting a page from the trash. - Title of the confirmation alert when deleting a post from the trash. */ +/* Title of the confirmation alert when deleting a post from the trash. */ "Delete Permanently?" = "完全に削除しますか ?"; /* Button label for deleting the current site @@ -2448,7 +2391,6 @@ translators: Block name. %s: The localized block name */ /* Accessibility label for button to dismiss a bottom sheet Accessibility label for the transparent space above the login dialog which acts as a button to dismiss the dialog. Accessibility label for the transparent space above the signup dialog which acts as a button to dismiss the dialog. - Action to show on alert when view asset fails. Dismiss a view. Verb */ "Dismiss" = "非表示にする"; @@ -2466,12 +2408,6 @@ translators: Block name. %s: The localized block name */ User's Display Name */ "Display Name" = "表示名"; -/* Accessibility label for other media items in the media collection view. The parameter is the creation date of the document. */ -"Document, %@" = "文書、%@"; - -/* Accessibility label for other media items in the media collection view. The parameter is the filename file. */ -"Document: %@" = "文書: %@"; - /* Message shown when all tours have been completed */ "Doesn't it feel good to cross things off a list?" = "リストから消したいものがありますか ?"; @@ -2632,8 +2568,7 @@ translators: Block name. %s: The localized block name */ /* Description of a Quick Start Tour */ "Draft and publish a post." = "投稿の下書きを作成して公開してください。"; -/* Title of the drafts filter. This filter shows a list of draft posts. - Title of the drafts header in search list. */ +/* Title of the drafts filter. This filter shows a list of draft posts. */ "Drafts" = "下書き"; /* No comment provided by engineer. */ @@ -2645,10 +2580,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Drag to adjust focal point" = "ドラッグしてフォーカルポイントを調整"; -/* Label for page duplicate option. Tapping creates a copy of the page. - Label for post duplicate option. Tapping creates a copy of the post. */ -"Duplicate" = "複製"; - /* No comment provided by engineer. */ "Duplicate block" = "ブロックを複製"; @@ -2661,13 +2592,9 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Each block has its own settings. To find them, tap on a block. Its settings will appear on the toolbar at the bottom of the screen." = "各ブロックには独自の設定があります。 確認するには、ブロックをタップします。 画面下部にあるツールバーに設定が表示されます。"; -/* Accessibility label for edit button to enable multi selection mode in the user's media library - Edit the post. - Editing GIF alert default action button. +/* Editing GIF alert default action button. Edits a Comment Edits the comment - Label for a button that opens the Edit Page view controller - Label for the edit post button. Tapping displays the editor. User action to edit media details. Verb, edit a comment */ "Edit" = "編集"; @@ -2675,9 +2602,6 @@ translators: Block name. %s: The localized block name */ /* Title for the edit more button section */ "Edit \"More\" button" = "「その他」ボタンを編集"; -/* Button that displays the media editor to the user */ -"Edit %@" = "%@ を編集"; - /* Blocklist Keyword Edition Title */ "Edit Blocklist Word" = "ブロックリストの単語を編集"; @@ -2870,9 +2794,6 @@ translators: Block name. %s: The localized block name */ /* Secondary message shown when there are no domains that match the user entered text. */ "Enter different words above and we'll look for an address that matches it." = "上に別の語句を入力してください。一致するアドレスを検索します。"; -/* Accessibility hint for edit button to enable multi selection mode in the user's media library */ -"Enter edit mode to enable multi select to delete" = "複数選択および削除を有効にするには、編集モードに移動してください"; - /* (placeholder) Help enter WordPress password Placeholder of a field to type a password to protect the post. */ "Enter password" = "パスワードを入力"; @@ -3028,9 +2949,6 @@ translators: Block name. %s: The localized block name */ /* Short title telling the user they will receive a blogging reminder every day of the week. */ "Every day at %@" = "毎日%@時"; -/* Label for the post author filter. This filter shows posts for all users on the blog. */ -"Everyone" = "誰でも"; - /* Example story title description */ "Example story title" = "ストーリーのタイトルの例"; @@ -3040,9 +2958,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Excerpt length (words)" = "抜粋の長さ (単語数)"; -/* Accessibility label for a post's excerpt in the post list. The parameter is the post excerpt. For example, \"Excerpt. This is the first paragraph.\" */ -"Excerpt. %@." = "抜粋 :%@。"; - /* Should be the same as the text displayed if the user clicks the (i) in Calypso. */ "Excerpts are optional hand-crafted summaries of your content." = "抜粋は、手動で書かれたコンテンツの要約です (オプション)。"; @@ -3052,8 +2967,7 @@ translators: Block name. %s: The localized block name */ /* Accessibility Label for the exit full screen button on the full screen comment reply mode */ "Exit Full Screen" = "フルスクリーン表示を終了"; -/* Accessibility indication that the current Post List style is currently Expanded. - Screen reader text to represent the expanded state of a UI control */ +/* Screen reader text to represent the expanded state of a UI control */ "Expanded" = "展開表示"; /* Accessibility hint */ @@ -3103,9 +3017,6 @@ translators: Block name. %s: The localized block name */ The action failed */ "Failed" = "失敗しました"; -/* Error title when picked media cannot be imported into stories. */ -"Failed Media Export" = "メディアのエクスポートに失敗しました"; - /* Message for mark all as read success notice */ "Failed marking Notifications as read" = "「通知」を既読としてマークできませんでした"; @@ -3307,6 +3218,12 @@ translators: Block name. %s: The localized block name */ /* An example tag used in the login prologue screens. */ "Football" = "フットボール"; +/* translators: Recommendation included in a warning related to having blocks deeply nested. */ +"For this reason, we recommend editing the block using the web editor." = "このため、Web エディターでブロックを編集することをお勧めします。"; + +/* translators: Recommendation included in a warning related to having blocks deeply nested. */ +"For this reason, we recommend editing the block using your web browser." = "このため、Web ブラウザーでブロックを編集することをお勧めします。"; + /* Register Domain - Domain contact information section header description */ "For your convenience, we have pre-filled your WordPress.com contact information. Please review to be sure it’s the correct information you want to use for this domain." = "お手間を省くため、WordPress.com 連絡先情報にはデータが事前に入力されています。このドメインに使用する正しい情報かどうか、ご確認ください。"; @@ -3624,8 +3541,7 @@ translators: Block name. %s: The localized block name */ "Home" = "ホーム"; /* Title for setting which shows the current page assigned as a site's homepage - Title for the homepage section in site settings screen - Title of the Homepage Badge */ + Title for the homepage section in site settings screen */ "Homepage" = "ホームページ"; /* Label for Homepage Settings site settings section @@ -3722,9 +3638,6 @@ translators: Block name. %s: The localized block name */ /* Hint for image title on image settings. */ "Image title" = "画像タイトル"; -/* Accessibility label for image thumbnails in the media collection view. The parameter is the creation date of the image. */ -"Image, %@" = "画像 (%@)"; - /* Undated post time label */ "Immediately" = "すぐに"; @@ -4210,9 +4123,6 @@ translators: Block name. %s: The localized block name */ Settings: Comments Approval settings */ "Links in comments" = "コメント内のリンク"; -/* The accessibility label for the list style button in the Post List. */ -"List style" = "リストスタイル"; - /* Title of the screen that load selected the revisions. */ "Load" = "読み込む"; @@ -4228,18 +4138,12 @@ translators: Block name. %s: The localized block name */ /* Text displayed while loading the activity feed for a site */ "Loading Backups..." = "バックアップを読み込み中…"; -/* Phrase to show when the user has searched for GIFs and they are being loaded. */ -"Loading GIFs..." = "GIF を読み込み中…"; - /* Menus label text displayed while menus are loading */ "Loading Menus..." = "メニューの読み込み中..."; /* Text displayed while loading site People. */ "Loading People..." = "人物を読み込んでいます..."; -/* Phrase to show when the user has searched for images and they are being loaded. */ -"Loading Photos..." = "写真を読み込み中…"; - /* Text displayed while loading plans details */ "Loading Plan..." = "プランを読み込み中…"; @@ -4300,8 +4204,7 @@ translators: Block name. %s: The localized block name */ /* Local Services site intent topic */ "Local Services" = "地域サービス"; -/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. - Title of the Local Changes Badge */ +/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. */ "Local changes" = "ローカルでの変更"; /* Title for alert when a generic error happened when trying to find the location of the device */ @@ -4465,7 +4368,6 @@ translators: Block name. %s: The localized block name */ "Max Video Upload Size" = "アップロードできる最大の動画サイズ"; /* Accessibility label for the Me button in My Site. - Label for the post author filter. This filter shows posts only authored by the current user. Me page title Noun. Title. Links to the Me screen. The accessibility value of the me tab. @@ -4473,9 +4375,7 @@ translators: Block name. %s: The localized block name */ "Me" = "自分"; /* Noun. Title. Links to the blog's Media library. - Tab bar title for the Media tab in Media Picker The menu item to select during a guided tour. - Title for Media Library section of the app. Title for the media section in site settings screen Title label for the media settings section in the app settings */ "Media" = "メディア"; @@ -4487,13 +4387,6 @@ translators: Block name. %s: The localized block name */ /* Label for size of media cache in the app. */ "Media Cache Size" = "メディア キャッシュサイズ"; -/* Title for alert when access to media capture is not granted */ -"Media Capture" = "メディアをキャプチャ"; - -/* Title for alert when a generic error happened when loading media - Title for alert when access to the media library is not granted by the user */ -"Media Library" = "メディアライブラリ"; - /* Title for action sheet with media options. */ "Media Options" = "メディア設定"; @@ -4516,9 +4409,6 @@ translators: Block name. %s: The localized block name */ /* translators: %s: block title e.g: \"Paragraph\". */ "Media options" = "メディア設定"; -/* Alert title when there is issues loading an asset to preview. */ -"Media preview failed." = "メディアのプレビューに失敗しました。"; - /* Alert displayed to the user when multiple media items have uploaded successfully. */ "Media uploaded (%ld files)" = "メディアをアップロードしました (%ldファイル)"; @@ -4556,9 +4446,6 @@ translators: Block name. %s: The localized block name */ Label for the share message field on the post settings. */ "Message" = "メッセージ"; -/* Title of section containing image / video metadata such as size and file type */ -"Metadata" = "メタデータ"; - /* Option to select the Microsft Outlook app when logging in with magic links */ "Microsoft Outlook" = "Microsoft Outlook"; @@ -4578,13 +4465,11 @@ translators: Block name. %s: The localized block name */ "Months and Years" = "月・年ごと"; /* Accessibility label for more button in dashboard quick start card. - Accessibility label for the More button in Page List. Accessibility label for the More button in Post List (compact view). Accessibility label for the More button on formatting toolbar. Accessibility label for the More button on Reader Cell Accessibility label for the More button on Reader's post details - Action button to display more available options - Label for the more post button. Tapping displays an action sheet with post options. */ + Action button to display more available options */ "More" = "続き"; /* Action button to display more available options @@ -4642,15 +4527,8 @@ translators: Block name. %s: The localized block name */ /* Screen reader text for button that will move the menu item */ "Move menu item" = "メニュー項目を移動"; -/* Label for a button that moves a page to the draft folder - Label for an option that moves a post to the draft folder */ -"Move to Draft" = "ドラフトに移動"; - -/* Label for a button that moves a page to the trash folder - Label for a option that moves a post to the trash folder - Title for button on the comment details page that moves the comment to trash when tapped. +/* Title for button on the comment details page that moves the comment to trash when tapped. Trash option in the trash confirmation alert. - Trash option in the trash page confirmation alert. Trashes the comment */ "Move to Trash" = "ゴミ箱へ移動"; @@ -4682,7 +4560,8 @@ translators: Block name. %s: The localized block name */ Title of My Site tab */ "My Site" = "マイサイト"; -/* Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ +/* Title for site picker screen. + Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ "My Sites" = "自分のサイト"; /* Siri Suggestion to open My Sites */ @@ -4932,9 +4811,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title for the view when there aren't any Activities to display in the Activity Log for a given filter. */ "No matching events found." = "一致するイベントは見つかりませんでした。"; -/* Message displayed when no results are returned from a media library search. Should match Calypso. - Phrase to show when the user search for images but there are no result to show. - Phrase to show when the user searches for GIFs but there are no result to show. */ +/* Message displayed when no results are returned from a media library search. Should match Calypso. */ "No media matching your search" = "検索と一致するメディアがありません"; /* Menus text shown when no menus were available for loading the Menus editor. */ @@ -4952,8 +4829,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Displayed in the Notifications Tab as a title, when there are no notifications */ "No notifications yet" = "まだ通知はありません"; -/* Displayed when the user is searching the pages list and there are no matching pages - Text displayed when there's no matching with the text search */ +/* Text displayed when there's no matching with the text search */ "No pages matching your search" = "検索に一致するページはありません"; /* Text displayed when search for plugins returns no results */ @@ -4974,9 +4850,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Message shown whent the reader finds no posts for the chosen tag */ "No posts have been made recently with this tag." = "最近このタグで公開された投稿はありません。"; -/* Displayed when the user is searching the posts list and there are no matching posts */ -"No posts matching your search" = "検索に一致する投稿はありません"; - /* Accessibility value for a Stats' Posting Activity Month if there are no posts. */ "No posts." = "投稿がありません。"; @@ -5077,9 +4950,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* A message title */ "Nothing liked yet" = "「いいね」がつけられているものはまだありません"; -/* Default message for empty media picker */ -"Nothing to show" = "表示するものはありません"; - /* Notifications Details Accessibility Identifier */ "Notification Details Table" = "通知詳細テーブル"; @@ -5139,7 +5009,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Alert dismissal title Button title. Acknowledges a prompt. Button title. An acknowledgement of the message displayed in a prompt. - Confirmation of action Default action Dismisses the alert Marks all notifications as read. @@ -5201,9 +5070,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* No comment provided by engineer. */ "Only show excerpt" = "抜粋のみを表示"; -/* Message telling the user that they've only enabled limited photo library permissions for the app. */ -"Only the selected photos you've given access to are available." = "アクセスを付与された選択済みの写真のみ使用できます。"; - /* An informal exclaimation that means `something went wrong`. Title for the view when there's an error loading a comment. Title for the view when there's an error loading Activity Log @@ -5238,9 +5104,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title of a button that opens the apps settings in the system Settings.app */ "Open Settings" = "設定を開く"; -/* Editor button to swich the media picker from quick mode to full picker */ -"Open full media picker" = "メディアピッカーを通常モードで開く"; - /* No comment provided by engineer. */ "Open in Safari" = "Safari で開く"; @@ -5280,6 +5143,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Divider on initial auth view separating auth options. */ "Or" = "OR"; +/* Instruction text for other forms of two-factor auth methods. */ +"Or choose another form of authentication." = "または別の認証形式を選択してください。"; + /* Label for button to log in using site address. Underscores _..._ denote underline. */ "Or log in by _entering your site address_." = "または、サイトアドレスを入力してログインしてください。"; @@ -5338,15 +5204,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title shown when selecting a post type of Page from the Share Extension. */ "Page" = "ページ"; -/* Prompts the user that a restored page was moved to the drafts list. */ -"Page Restored to Drafts" = "ページが下書きに復元されました"; - -/* Prompts the user that a restored page was moved to the published list. */ -"Page Restored to Published" = "ページが公開済みに復元されました"; - -/* Prompts the user that a restored page was moved to the scheduled list. */ -"Page Restored to Scheduled" = "ページが予約済みに復元されました"; - /* Name of the button to open the page settings The title of the Page Settings screen. */ "Page Settings" = "ページ設定"; @@ -5363,9 +5220,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of notification displayed when a page has failed to upload. */ "Page failed to upload" = "ページはアップロードに失敗しました"; -/* A short message explaining that a page was moved to the trash bin. */ -"Page moved to trash." = "ページをゴミ箱へ移動しました。"; - /* Title of notification displayed when a page has been successfully saved as a draft. */ "Page pending review" = "ページはレビュー待ちです"; @@ -5437,8 +5291,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title of pending Comments filter. */ "Pending" = "承認待ち"; -/* Name for the status of a post pending review. - Title of the Pending Review Badge */ +/* Name for the status of a post pending review. */ "Pending review" = "レビュー待ち"; /* Noun. Title of the people management feature. @@ -5467,12 +5320,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Photography site intent topic */ "Photography" = "写真"; -/* Tab bar title for the Photos tab in Media Picker */ -"Photos" = "写真"; - -/* Subtitle for placeholder in Free Photos. The company name 'Pexels' should always be written as it is. */ -"Photos provided by Pexels" = "写真提供元: Pexels"; - /* Title for selecting a new username in the site creation flow. */ "Pick username" = "ユーザー名を選択"; @@ -5565,7 +5412,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Please enter the password for your WordPress.com account to log in with your Apple ID." = "Apple ID でログインするには、WordPress.com のアカウントのパスワードを入力してください。"; /* Instruction text on the two-factor screen. */ -"Please enter the verification code from your authenticator app, or tap the link below to receive a code via SMS." = "認証アプリから確認コードを入力するか、下のリンクをタップして SMS でコードを受信して​​ください。"; +"Please enter the verification code from your authenticator app." = "認証アプリから認証コードを入力してください。"; /* Popup message to ask for user credentials (fields shown below). */ "Please enter your credentials" = "ログイン情報を入力してください"; @@ -5660,15 +5507,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ The post formats available for the post. Should be the same as in core WP. */ "Post Format" = "投稿フォーマット"; -/* Prompts the user that a restored post was moved to the drafts list. */ -"Post Restored to Drafts" = "投稿を下書きとして復元しました"; - -/* Prompts the user that a restored post was moved to the published list. */ -"Post Restored to Published" = "公開した投稿を復元しました"; - -/* Prompts the user that a restored post was moved to the scheduled list. */ -"Post Restored to Scheduled" = "投稿を予約済みとして復元しました"; - /* Name of the button to open the post settings The title of the Post Settings screen. */ "Post Settings" = "投稿設定"; @@ -5688,9 +5526,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of notification displayed when a post has failed to upload. */ "Post failed to upload" = "投稿はアップロードに失敗しました"; -/* A short message explaining that a post was moved to the trash bin. */ -"Post moved to trash." = "投稿をゴミ箱へ移動しました。"; - /* Title of notification displayed when a post has been successfully saved as a draft. */ "Post pending review" = "投稿はレビュー待ちです"; @@ -5749,9 +5584,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Period Stats 'Posts and Pages' header */ "Posts and Pages" = "投稿とページ"; -/* Title of the Posts Page Badge */ -"Posts page" = "投稿ページ"; - /* Message informing the user that their static homepage for posts was set successfully */ "Posts page successfully updated" = "投稿ページが正常に更新されました"; @@ -5764,9 +5596,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* A message explaining the Posts I Like feature in the reader */ "Posts that you like will appear here." = "ここに表示したい投稿。"; -/* Subtitle for placeholder in Tenor picker. `The company name 'Tenor' should always be written as it is. */ -"Powered by Tenor" = "Powered by Tenor"; - /* Browse premium themes selection title */ "Premium" = "プレミアム"; @@ -5785,18 +5614,12 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title for screen to preview a static content. */ "Preview" = "プレビュー"; -/* Action for Media Picker to preview the selected media items. The argument in the string represents the number of elements (as numeric digits) selected */ -"Preview %@" = "%@ をプレビュー:"; - /* Title for web preview device switching button */ "Preview Device" = "デバイスをプレビュー"; /* Title on display preview error */ "Preview Unavailable" = "プレビューは利用できません"; -/* Accessibility label for media item preview for user's viewing an item in their media library */ -"Preview media" = "メディアをプレビュー"; - /* No comment provided by engineer. */ "Preview page" = "固定ページをプレビュー"; @@ -5843,8 +5666,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Privacy notice for California users" = "カリフォルニア州のユーザーへのプライバシー通知"; /* Name for the status of a post that is marked private. - Privacy setting for posts set to 'Private'. Should be the same as in core WP. - Title of the Private Badge */ + Privacy setting for posts set to 'Private'. Should be the same as in core WP. */ "Private" = "プライベート"; /* No comment provided by engineer. */ @@ -5894,12 +5716,10 @@ translators: %s: Select control button label e.g. \"Button width\" */ Label for the publish date button. */ "Publish Date" = "公開日"; -/* A short phrase indicating a post is due to be immedately published. - Label for a button that moves a page to the published folder, publishing with the current date/time. */ +/* A short phrase indicating a post is due to be immedately published. */ "Publish Immediately" = "すぐに公開"; /* Label for a button that publishes the post - Label for an option that moves a publishes a post immediately Title of button allowing the user to immediately publish the post they are editing. */ "Publish Now" = "今すぐ公開"; @@ -5917,8 +5737,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Name for the status of a published post. Period Stats 'Published' header - Title of the published filter. This filter shows a list of posts that the user has published. - Title of the published header in search list. */ + Title of the published filter. This filter shows a list of posts that the user has published. */ "Published" = "公開済み"; /* Precedes the name of the blog just posted on */ @@ -6060,8 +5879,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of the completion screen of the Blogging Reminders Settings screen when the reminders are removed. */ "Reminders removed" = "リマインダーを削除しました"; -/* Add asset to media picker list - Alert button to confirm a plugin to be removed +/* Alert button to confirm a plugin to be removed Button label when removing a blog Label for the Remove Feature Image icon. Tapping will show a confirmation screen for removing the feature image from the post. Remove Action @@ -6214,9 +6032,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of secondary button on alert prompting verify their accounts while attempting to publish */ "Resend" = "再送信"; -/* Title of the reset button */ -"Reset" = "リセット"; - /* Accessibility label for the reset activity type button */ "Reset Activity Type filter" = "アクティビティタイプフィルターを再設定"; @@ -6271,12 +6086,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ Button title, displayed when media has failed to upload. Allows the user to try the upload again. Button title. Retries uploading a post. If a user taps the button with this label, the action that evinced this error view will be retried. - Label for a button that attempts to re-upload a page that previously failed to upload. - Label for the retry post upload button. Tapping attempts to upload the post again. Opens the media library . Retries the upload of a user's post. Retry updating User's Role - Retry uploading the post. Retry. Action Retry. Verb – retry a failed media upload. The Jetpack view button title used when an error occurred @@ -6288,9 +6100,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Button title that triggers a scan */ "Retry Scan" = "スキャンを再試行"; -/* User action to retry media upload. */ -"Retry Upload" = "再アップロード"; - /* User action to retry all failed media uploads. */ "Retry all" = "すべて再試行"; @@ -6388,9 +6197,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Accessibility label for the 'Save Post' button when a post has been saved. */ "Saved Post" = "保存済みの投稿"; -/* Text displayed in HUD when a media item's metadata (title, etc) is saved successfully. */ -"Saved!" = "保存しました。"; - /* Accessibility hint for the 'Save Post' button. */ "Saves this post for later." = "この投稿をブックマークする。"; @@ -6401,7 +6207,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Saving post…" = "投稿を保存中…"; /* Menus save button title while it is saving a Menu. - Text displayed in HUD while a media item's metadata (title, etc) is being saved. Text displayed in HUD while a post is being saved as a draft. */ "Saving..." = "保存中..."; @@ -6492,21 +6297,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* No comment provided by engineer. */ "Search or type URL" = "検索または URL を入力"; -/* Text displayed when the search controller will be presented */ -"Search pages" = "ページを検索"; - -/* Text displayed when the search controller will be presented */ -"Search posts" = "投稿を検索"; - /* No comment provided by engineer. */ "Search settings" = "検索設定"; -/* Title for placeholder in Tenor picker */ -"Search to find GIFs to add to your Media Library!" = "GIF を検索して、メディアライブラリに追加してください。"; - -/* Title for placeholder in Free Photos */ -"Search to find free photos to add to your Media Library!" = "無料の写真を検索して、メディアライブラリに追加してください。"; - /* Menus search bar placeholder text. */ "Search..." = "検索..."; @@ -6577,9 +6370,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Register Domain - Domain contact information field placeholder for Country */ "Select Country" = "国名を選択"; -/* Title of button that allows the user to select more photos to access within the app */ -"Select More" = "さらに選択"; - /* Blog Picker's Title */ "Select Site" = "サイトを選択"; @@ -6601,9 +6391,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Register domain - Title for the Choose domain button of Suggested domains screen */ "Select domain" = "ドメインを選択"; -/* Accessibility hint for actions when displaying media items. */ -"Select media." = "メディアを選択。"; - /* Accessibility label for selecting paragraph style button on formatting toolbar. */ "Select paragraph style" = "段落スタイルを選択"; @@ -6707,19 +6494,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label for connected service in Publicize stat. */ "Service" = "サービス"; -/* Label for a button that opens the Set Parent options view controller - Navigation title displayed on the navigation bar */ +/* Navigation title displayed on the navigation bar */ "Set Parent" = "親の設定"; /* No comment provided by engineer. */ "Set as Featured Image" = "アイキャッチ画像として設定"; -/* Label for a button that sets the selected page as the site's Homepage */ -"Set as Homepage" = "ホームページに設定する"; - -/* Label for a button that sets the selected page as the site's Posts page */ -"Set as Posts Page" = "投稿ページとして設定"; - /* Notice confirming that an image has been set as the post's featured image. */ "Set as featured image" = "アイキャッチ画像として設定"; @@ -6763,7 +6543,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Accessibility label for share buttons in nav bars Button label to share a post Button label to share a web page - Share the post. Shares the comment URL Spoken accessibility label Title for a button that allows the user to share their answer to the prompt. @@ -7149,8 +6928,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Name of setting configured when a site uses a static page as its homepage */ "Static Homepage" = "固定ホームページ"; -/* Label for post stats option. Tapping displays statistics for a post. - Noun. Abbreviation of Statistics. Name of the Stats feature +/* Noun. Abbreviation of Statistics. Name of the Stats feature Noun. Abbv. of Statistics. Links to a blog's Stats screen. Stats 3D Touch Shortcut Stats window title @@ -7181,9 +6959,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label text that defines a post marked as sticky */ "Sticky" = "先頭固定表示"; -/* Accessibility label for a sticky post in the post list. */ -"Sticky." = "先頭固定表示。"; - /* User action to stop upload. */ "Stop upload" = "アップロードを停止"; @@ -7240,7 +7015,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ Theme Support action title */ "Support" = "サポート"; -/* Button used to switch site */ +/* Title for back button that leads to the site picker screen. */ "Switch Site" = "サイト切り替え"; /* Switches the Editor to HTML Mode */ @@ -7328,9 +7103,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label explaining why users might want to add tags. */ "Tags help tell readers what a post is about. Separate different tags with commas." = "タグは、投稿の内容を読者に伝えるのに役立ちます。異なるタグはコンマで区切ってください。"; -/* Menu option for taking an image or video with the device's camera. */ -"Take Photo or Video" = "写真または動画を撮影する"; - /* No comment provided by engineer. */ "Take a Photo" = "写真を撮影する"; @@ -7401,12 +7173,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Accessibility hint */ "Tap to select the previous period" = "タップして前の期間を選択"; -/* Accessibility hint for button used to switch site */ -"Tap to switch to another site, or add a new site" = "タップして別のサイトに切り替え、または新しいサイトを追加"; - -/* Accessibility hint for media item preview for user's viewing an item in their media library */ -"Tap to view media in full screen" = "タップしてメディアをフルスクリーンで表示"; - /* Accessibility hint for a button that opens a new view with more details. */ "Tap to view more details." = "タップして詳細を表示します。"; @@ -7452,10 +7218,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* No comment provided by engineer. */ "Text formatting controls are located within the toolbar positioned above the keyboard while editing a text block" = "書式設定コントロールは、テキストブロックの編集中に表示されるキーボードの上に位置するツールバー内にあります"; -/* Button title - The button's title text to send a 2FA code via SMS text message. */ +/* Button title */ "Text me a code instead" = "代わりに SMS でコードを送信する"; +/* The button's title text to send a 2FA code via SMS text message. */ +"Text me a code via SMS" = "SMS 経由でコードを送信"; + /* Message of alert when theme activation succeeds */ "Thanks for choosing %@ by %@" = "%1$@ (%2$@) のご利用ありがとうございます"; @@ -7483,9 +7251,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Error message shown to a user who is trying to share to Facebook but does not have any available Facebook Pages. */ "The Facebook connection cannot find any Pages. Publicize cannot connect to Facebook Profiles, only published Pages." = "Facebook との連携でページが見つかりません。パブリサイズで Facebook のプロフィールに連携できません。連携できるのは公開されているページのみです。"; -/* Message shown when a GIF failed to load while trying to add it to the Media library. */ -"The GIF could not be added to the Media Library." = "GIF をメディアライブラリに追加できませんでした。"; - /* Description shown when a user logs in with Google but no matching WordPress.com account is found */ "The Google account \"%@\" doesn't match any account on WordPress.com" = "Google アカウント「%@」が WordPress.com のアカウントと一致しません"; @@ -7613,7 +7378,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Error message shown when user attempts to remove the site owner. */ "The user you are trying to remove is the owner of this site. Please contact support for assistance." = "削除しようとしているユーザーはこのサイトの所有者です。ヘルプが必要な場合はサポートにご連絡ください。"; -/* Error message informing a user about an invalid password. */ +/* No comment provided by engineer. */ "The username or password stored in the app may be out of date. Please re-enter your password in the settings and try again." = "アプリに保存されたユーザー名またはパスワードが古くなっているのかもしれません。設定画面でパスワードを再入力し、もう一度お試しください。"; /* Message shown when a video failed to load while trying to add it to the Media library. */ @@ -7681,9 +7446,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* A short error message letting the user know about a problem displaying a post. */ "There was a problem displaying this post." = "この投稿を表示する際に問題が発生しました。"; -/* Error message displayed when the Media Library is unable to load a full sized preview of an item. */ -"There was a problem loading the media item." = "メディア項目を読み込む際に問題が発生しました。"; - /* The loading view subtitle displayed when an error occurred */ "There was a problem loading your data, refresh your page to try again." = "データの読み込み中に問題が発生しました。ページを更新してもう一度お試しください。"; @@ -7696,9 +7458,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Explaining to the user there was an error trying to obtain the current location of the user. */ "There was a problem when trying to access your location. Please try again later." = "位置情報にアクセスしようとした際に問題が発生しました。後ほどもう一度お試しください。"; -/* Explaining to the user there was an generic error accesing media. */ -"There was a problem when trying to access your media. Please try again later." = "メディアにアクセスしようとした際に問題が発生しました。後ほどもう一度お試しください。"; - /* Message for stories unknown error. */ "There was a problem with the Stories editor. If the problem persists you can contact us via the Me > Help & Support screen." = "ストーリーエディターに問題が生じました。 問題が解決しない場合は、「ME」>「ヘルプとサポート」画面からお問い合わせください。"; @@ -7769,9 +7528,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* A description informing the user in order to proceed with this feature we will need camera permissions, and how to enable it. */ "This app needs permission to access the Camera to scan login codes, tap on the Open Settings button to enable it." = "このアプリにはログインコードをスキャンするカメラにアクセスするための権限が必要です。有効化するには「設定を開く」をタップします。"; -/* Explaining to the user why the app needs access to the device media library. */ -"This app needs permission to access your device media library in order to add photos and\/or video to your posts. Please change the privacy settings if you wish to allow this." = "このアプリが投稿に写真・動画を追加するため端末のメディアライブラリにアクセスするには、許可が必要です。プライバシー設定を変更してください。"; - /* No comment provided by engineer. */ "This color combination may be hard for people to read. Try using a brighter background color and\/or a darker text color." = "この色の組み合わせは読みにくい可能性があります。 背景色を明るくするか、文字の色を暗くしてみましょう。"; @@ -7881,6 +7637,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Description of a Quick Start Tour */ "Time to finish setting up your site! Our checklist walks you through the next steps." = "サイトの設定の最終段階です。チェックリストで次のステップの詳細をご覧ください。"; +/* Error when the uses takes more than 1 minute to submit a security key. */ +"Time's up, but don't worry, your security is our priority. Please try again!" = "時間切れですがご心配は要りません。皆さまの安全が当社の最優先事項です。 もう一度お試しください。"; + /* WordPress.com Marketing Footer Text */ "Tips for getting the most out of WordPress.com." = "WordPress.com を最大限に活用するためのヒント。"; @@ -8004,24 +7763,20 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title for the traffic section in site settings screen */ "Traffic" = "トラフィック"; +/* Describes a domain that was transferred from elsewhere to wordpress.com */ +"Transferred Domain" = "移管されたドメイン"; + /* translators: %s: block title e.g: \"Paragraph\". */ "Transform %s to" = "%sを変換:"; /* No comment provided by engineer. */ "Transform block…" = "ブロックを変換…"; -/* Accessibility label for trash button to delete items from the user's media library - Accessibility label for trash buttons in nav bars +/* Accessibility label for trash buttons in nav bars Trashes a comment Trashes the comment */ "Trash" = "ゴミ箱"; -/* Accessibility hint for trash button to delete items from the user's media library */ -"Trash selected media" = "選択したメディアをゴミ箱に移動"; - -/* Title of the trash page confirmation alert. */ -"Trash this page?" = "このページをゴミ箱に移動しますか ?"; - /* Title of the trash confirmation alert. */ "Trash this post?" = "ゴミ箱に移動しますか ?"; @@ -8139,9 +7894,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Shown when a user logs in with Google but it subsequently fails to work as login to WordPress.com */ "Unable To Connect" = "接続できない"; -/* An error message. */ -"Unable to Connect" = "連携できません"; - /* Title for stories unknown error. */ "Unable to Create Stories Editor" = "ストーリーエディターを作成できません"; @@ -8157,9 +7909,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* An error message shown when there is an issue creating new invite links. */ "Unable to create new invite links." = "新規招待リストを作成できません。"; -/* Text displayed in HUD if there was an error attempting to delete a group of media items. */ -"Unable to delete all media items." = "すべてのメディアファイルを削除できません。"; - /* Text displayed in HUD if there was an error attempting to delete a media item. */ "Unable to delete media item." = "メディアファイルを削除できません。"; @@ -8223,12 +7972,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Message displayed when sharing a link to the downloadable backup fails. */ "Unable to share link" = "リンクを共有できません"; -/* Message that appears when a user tries to trash a page while their device is offline. */ -"Unable to trash pages while offline. Please try again later." = "オフライン中は、ページをゴミ箱に移動できません。 後ほど、もう一度お試しください。"; - -/* Message that appears when a user tries to trash a post while their device is offline. */ -"Unable to trash posts while offline. Please try again later." = "オフライン中は、投稿をゴミ箱に移動できません。後ほど、もう一度お試しください。"; - /* Notice title when turning site notifications off fails. */ "Unable to turn off site notifications" = "サイト通知を無効にできません"; @@ -8301,8 +8044,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ Button title. Reverts the previous notification operation Revert an operation Revert enabling notification after successfully subcribing to the comments for the post. - The title of an 'undo' button. Tapping the button moves a trashed page out of the trash folder. - The title of an 'undo' button. Tapping the button moves a trashed post out of the trash folder. Undo action */ "Undo" = "元に戻す"; @@ -8345,9 +8086,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title for Unknown HTML Editor */ "Unknown HTML" = "不明な HTML"; -/* Label to use when creation date from media asset is not know. */ -"Unknown creation date" = "作成日不明"; - /* No comment provided by engineer. */ "Unknown error" = "不明なエラー"; @@ -8513,6 +8251,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of a row displayed on the debug screen used to configure the sandbox store use in the App. */ "Use Sandbox Store" = "サンドボックスストアを使用"; +/* The button's title text to use a security key. */ +"Use a security key" = "セキュリティキーを使用"; + /* Option to enable the block editor for new posts */ "Use block editor" = "ブロックエディターを使用"; @@ -8588,15 +8329,10 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of an alert informing users that the video they are trying to select is not allowed. */ "Video not uploaded" = "動画がアップロードされていません"; -/* Accessibility label for video thumbnails in the media collection view. The parameter is the creation date of the video. */ -"Video, %@" = "動画 (%@)"; - /* Period Stats 'Videos' header */ "Videos" = "動画"; /* Button title. Displays a summary / sharing screen for a specific post. - Label for a button that opens the page when tapped. - Label for the view post button. Tapping displays the post as it appears on the web. Opens the post epilogue screen to allow sharing / viewing of a post. Theme View action title Verb. The screen title shown when viewing a post inside the app. */ @@ -8711,6 +8447,10 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Message shown on screen while waiting for Google to finish its signup process. */ "Waiting for Google to complete…" = "Google が完了するのを待機しています…"; +/* Text while the webauthn signature is being verified + Text while waiting for a security key challenge */ +"Waiting for security key" = "セキュリティキーの待機中"; + /* View title during the Google auth process. */ "Waiting..." = "待機中…"; @@ -9082,6 +8822,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* An error message shown when a wpcom user provides the wrong password. */ "Whoops, something went wrong and we couldn't log you in. Please try again!" = "エラーが発生し、ログインできませんでした。もう一度お試しください。"; +/* Generic error on the 2FA screen */ +"Whoops, something went wrong. Please try again!" = "エラーが発生しました。 もう一度お試しください。"; + +/* Error when the uses chooses an invalid security key on the 2FA screen. */ +"Whoops, that security key does not seem valid. Please try again with another one" = "そのセキュリティキーは有効ではないようです。 今度は別のキーでお試しください。"; + /* Error message shown when an incorrect two factor code is provided. */ "Whoops, that's not a valid two-factor verification code. Double-check your code and try again!" = "正しい二段階認証コードではありません。コードを再確認してもう一度お試しください。"; @@ -9109,9 +8855,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Siri Suggestion to open Support */ "WordPress Help" = "WordPress ヘルプ"; -/* Name for the WordPress Media Library */ -"WordPress Media" = "WordPress メディア"; - /* Accessibility label for selecting an image or video from the user's WordPress media library on formatting toolbar. */ "WordPress Media Library" = "WordPress メディアライブラリ"; @@ -9426,9 +9169,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Share extension error dialog text. */ "Your account does not have permission to upload media to this site. The Site Administrator can change these permissions." = "ご利用のアカウントにはこのサイトにメディアをアップロードする権限がありません。サイト管理者がこの権限を変更できます。"; -/* Explaining to the user why the app needs access to the device media library. */ -"Your app is not authorized to access media library due to active restrictions such as parental controls. Please check your parental control settings in this device." = "ペアレンタルコントロールなどの制限が有効になっているため、アプリからメディアライブラリにアクセスできません。このデバイスのペアレンタルコントロール設定を確認してください。"; - /* Title for the Jetpack Backup Complete message. */ "Your backup is now available for download" = "バックアップをダウンロードする準備が整いました"; @@ -9447,9 +9187,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of the site address section in the Domains Dashboard. */ "Your free WordPress.com address is" = "あなたの WordPress.com の無料アドレスは"; -/* Error message when picked media cannot be imported into stories. */ -"Your media could not be exported. If the problem persists you can contact us via the Me > Help & Support screen." = "メディアをエクスポートできません。問題が解決しない場合、「自分 > ヘルプ & サポート」画面からお問い合わせください。"; - /* Details about recently acquired domain on domain credit redemption success screen */ "Your new domain %@ is being set up. It may take up to 30 minutes for your domain to start working." = "新しいドメイン %@ の設定中です。 ドメインが使用可能になるまでに最大で30分かかる場合があります。"; @@ -9573,8 +9310,38 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* This is the string we display when prompting the user to review the WordPress app */ "appRatings.wordpress.prompt" = "WordPress アプリについてどう思いますか ?"; -/* Label displayed on audio media items. */ -"audio" = "音声ファイル"; +/* Option to enable the optimization of images when uploading. */ +"appSettings.media.imageOptimizationRow" = "画像を最適化"; + +/* Indicates an image will use high quality when uploaded. */ +"appSettings.media.imageQuality.high" = "高い"; + +/* Indicates an image will use low quality when uploaded. */ +"appSettings.media.imageQuality.low" = "低い"; + +/* Indicates an image will use maximum quality when uploaded. */ +"appSettings.media.imageQuality.maximum" = "最高"; + +/* Indicates an image will use medium quality when uploaded. */ +"appSettings.media.imageQuality.medium" = "普通"; + +/* The quality of image used when uploading */ +"appSettings.media.imageQuality.title" = "品質"; + +/* Title for the image quality settings option. */ +"appSettings.media.imageQualityRow" = "画質"; + +/* Message of an alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.message" = "画像の最適化により、画像のサイズを縮小して迅速に読み込めます。\n\nこのオプションはデフォルトで有効になっていますが、アプリの設定でいつでも変更できます。"; + +/* Title of an alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.title" = "画像の最適化を続けますか ?"; + +/* Title of button for turning off image optimization, displayed in the alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.turnOff" = "オフにする"; + +/* Title of button for leaving on image optimization, displayed in the alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.turnOn" = "そのままにする"; /* translators: displays audio file extension. e.g. MP3 audio file */ "audio file" = "音声ファイル"; @@ -9685,7 +9452,41 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "blogHeader.actionCopyURL" = "URL をコピー"; /* Context menu button title */ -"blogHeader.actionOpenInBrowser" = "ブラウザーで開く"; +"blogHeader.actionVisitSite" = "サイトを表示"; + +/* Title for a button that, when tapped, shows more info about participating in Bloganuary. */ +"bloganuary.dashboard.card.button.learnMore" = "さらに詳しく"; + +/* Title for the Bloganuary dashboard card while Bloganuary is running. */ +"bloganuary.dashboard.card.runningTitle" = "Bloganuary が登場 !"; + +/* Title for the Bloganuary dashboard card. */ +"bloganuary.dashboard.card.title" = "まもなく Bloganuary です !"; + +/* Title of a button that calls the user to enable the Blogging Prompts feature. */ +"bloganuary.learnMore.modal.button.promptsDisabled" = "ブログ作成のプロンプトをオンにする"; + +/* Title of a button that will dismiss the Bloganuary modal when tapped. +Note that the word 'go' here should have a closer meaning to 'start' rather than 'move forward'. */ +"bloganuary.learnMore.modal.button.promptsEnabled" = "スタート"; + +/* The second line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.description.second" = "回答を公開します。"; + +/* The third line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.description.third" = "他のブロガーの回答を読んでインスピレーションを得て、新しいつながりを作りましょう。"; + +/* The first line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.descriptions.first" = "毎日インスピレーションを与える新しいプロンプトを受け取ることができます。"; + +/* An additional piece of information shown in case the user has the Blogging Prompts feature disabled. */ +"bloganuary.learnMore.modal.footer.addition" = "Bloganuary に参加するには「ブログ作成のプロンプト」を有効にする必要があります。"; + +/* An informative excerpt shown in a subtler tone. */ +"bloganuary.learnMore.modal.footer.text" = "Bloganuary は毎日のブログ作成のプロンプトから1月のトピックを送信します。"; + +/* The headline text of the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.headline" = "1か月間のライティングチャレンジにご参加ください"; /* Verb. Dismisses the blogging prompt notification. */ "bloggingPrompt.pushNotification.customActionDescription.dismiss" = "削除"; @@ -9714,6 +9515,12 @@ Example: Comment on Example: Reply to Pamela Nguyen */ "comment.header.subText.reply" = "%1$@ への返信"; +/* Error message informing a user about an invalid password. */ +"common.reEnterPasswordMessage" = "アプリに格納されているユーザー名またはパスワードが古い可能性があります。 設定にパスワードを再度入力して、もう一度お試しください。"; + +/* An error message. */ +"common.unableToConnect" = "連携できません"; + /* Footnote for the privacy compliance popover. */ "compliance.analytics.popover.footnote" = "これらの Cookie を使用することにより、ユーザーによるサイトの利用状況に関する情報が収集され、パフォーマンスが最適化されます。"; @@ -9864,50 +9671,92 @@ Example: Reply to Pamela Nguyen */ /* Title for a menu action in the context menu on the Jetpack install card. */ "domain.dashboard.card.menu.hide" = "非表示"; -/* Domain Purchase Completion footer */ -"domain.purchase.preview.footer" = "カスタムドメインが使用可能になるまでに最大で30分かかる場合があります。"; +/* Search domain - Title for the Suggested domains screen */ +"domain.management.addDomain.search.title" = "ドメインを検索"; -/* Domain Purchase Completion description (only for FREE domains). */ -"domain.purchase.preview.free.description" = "次に、閲覧できるように準備を整えます。"; +/* Domain management buy domain card button title */ +"domain.management.buy.card.button.title" = "ドメインを取得"; -/* Domain Purchase Completion description (only for PAID domains). */ -"domain.purchase.preview.paid.description" = "領収書をメールで送信しました。 次に、すべての人に向け準備を整えます。"; +/* Domain management buy domain card subtitle */ +"domain.management.buy.card.subtitle" = "サイトは後で追加します。"; -/* Reflects that site is live when domain purchase feature flag is ON. */ -"domain.purchase.preview.title" = "おめでとうございます ! サイトが公開されました。"; +/* Domain management buy domain card title */ +"domain.management.buy.card.title" = "ドメインだけを購入"; + +/* The expired label of the domain card in All Domains screen. */ +"domain.management.card.expired.label" = "期限切れ"; + +/* The renews label of the domain card in All Domains screen. */ +"domain.management.card.renews.label" = "更新"; + +/* The empty state button title in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.button.title" = "ドメインを探す"; + +/* The empty state description in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.description" = "以下をタップして最適なドメインを見つけてください。"; + +/* The empty state title in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.title" = "ドメインがありません"; + +/* The empty state description in All Domains screen when an error occurs */ +"domain.management.error.empty.state.description" = "ドメインの読み込み中にエラーが発生しました。 問題が解決しない場合は、サポートにご連絡ください。"; + +/* The empty state title in All Domains screen when an error occurs */ +"domain.management.error.empty.state.title" = "エラーが発生しました"; + +/* The empty state button title in All Domains screen when an error occurs */ +"domain.management.error.state.button.title" = "再試行"; + +/* The empty state description in All Domains screen when the user is offline */ +"domain.management.offline.empty.state.description" = "ネットワーク接続を確認して、もう一度お試しください。"; + +/* The empty state title in All Domains screen when the user is offline */ +"domain.management.offline.empty.state.title" = "インターネットに接続していません"; -/* Status of a domain in `Action Required` state */ -"domain.status.action.required" = "アクションが必要"; +/* Domain management choose site card button title */ +"domain.management.purchase.footer" = "* すべての有料年間プランに1年間の無料ドメインが含まれています"; -/* Status of a domain in `Active` state */ -"domain.status.active" = "有効"; +/* Domain management purchase domain screen title */ +"domain.management.purchase.subtitle" = "サイトは後から簡単に追加できます。"; -/* Status of a domain in `Complete Setup` state */ -"domain.status.complete.setup" = "設定を完了"; +/* Domain management purchase domain screen title. */ +"domain.management.purchase.title" = "ドメインの使用方法を選択してください"; -/* Status of a domain in `Error` state */ -"domain.status.error" = "エラー"; +/* The search bar title in All Domains screen. */ +"domain.management.search-bar.title" = "ドメインを検索"; -/* Status of a domain in `Expired` state */ -"domain.status.expired" = "期限切れ"; +/* The empty state description in All Domains screen when the are no domains matching the search criteria */ +"domain.management.search.empty.state.description" = "「%@」の検索に一致するドメインが見つかりませんでした"; -/* Status of a domain in `Expiring Soon` state */ -"domain.status.expiring.soon" = "まもなく期限切れ"; +/* The empty state title in All Domains screen when the are no domains matching the search criteria */ +"domain.management.search.empty.state.title" = "一致するドメインは見つかりませんでした"; -/* Status of a domain in `Failed` state */ -"domain.status.failed" = "失敗"; +/* Domain management choose site card button title */ +"domain.management.site.card.button.title" = "サイトを選択"; -/* Status of a domain in `In Progress` state */ -"domain.status.in.progress" = "進行中"; +/* Domain management choose site card subtitle */ +"domain.management.site.card.footer" = "ドメインは最初の1年間無料 *"; -/* Status of a domain in `Renew` state */ -"domain.status.renew" = "更新"; +/* Domain management choose site card subtitle */ +"domain.management.site.card.subtitle" = "お持ちのサイトと一緒に使用しましょう。"; -/* Status of a domain in `Verify Email` state */ -"domain.status.verify.email" = "メールを確認"; +/* Domain management choose site card title */ +"domain.management.site.card.title" = "既存の WordPress.com サイト"; -/* Status of a domain in `Verifying` state */ -"domain.status.verifying" = "認証中"; +/* Domain Management Screen Title */ +"domain.management.title" = "すべてのドメイン"; + +/* Domain Purchase Completion footer */ +"domain.purchase.preview.footer" = "カスタムドメインが使用可能になるまでに最大で30分かかる場合があります。"; + +/* Domain Purchase Completion description (only for FREE domains). */ +"domain.purchase.preview.free.description" = "次に、閲覧できるように準備を整えます。"; + +/* Domain Purchase Completion description (only for PAID domains). */ +"domain.purchase.preview.paid.description" = "領収書をメールで送信しました。 次に、すべての人に向け準備を整えます。"; + +/* Reflects that site is live when domain purchase feature flag is ON. */ +"domain.purchase.preview.title" = "おめでとうございます ! サイトが公開されました。"; /* The 'Best Alternative' label under the domain name in 'Choose a domain' screen */ "domain.suggestions.row.best-alternative" = "こちらもおすすめ"; @@ -9930,12 +9779,24 @@ Example: Reply to Pamela Nguyen */ /* The text to display for paid domains in 'Site Creation > Choose a domain' screen */ "domain.suggestions.row.yearly" = "\/ 年"; +/* Title for the checkout screen. */ +"domains.checkout.title" = "購入手続き"; + /* Action shown in a bottom notice to dismiss it. */ "domains.failure.dismiss" = "閉じる"; /* Content show when the domain selection action fails. */ "domains.failure.title" = "追加しようとしているドメインは現在 Jetpack アプリでは購入できません。"; +/* Title for the screen where the user can choose how to use the domain they're end up purchasing. */ +"domains.purchase.choice.title" = "ドメインを購入する"; + +/* Back button title that navigates back to the search domains screen. */ +"domains.search.backButton.title" = "検索"; + +/* Title of screen where user chooses a site to connect to their selected domain */ +"domains.sitePicker.title" = "サイトを選択"; + /* No comment provided by engineer. */ "double-tap to change unit" = "ダブルタップして単位を変更"; @@ -9953,6 +9814,15 @@ Example: Reply to Pamela Nguyen */ Site Address placeholder */ "example.com" = "example.com"; +/* Title for confirmation navigation bar button item */ +"externalMediaPicker.add" = "追加"; + +/* Bottom toolbar title in the selection mode */ +"externalMediaPicker.toolbarSelectItemsPrompt" = "画像を選択"; + +/* Bottom toolbar title in the selection mode */ +"externalMediaPicker.toolbarViewSelected" = "選択した項目を表示 (%@)"; + /* Title of screen the displays the details of an advertisement campaign. */ "feature.blaze.campaignDetails.title" = "キャンペーン詳細"; @@ -10052,9 +9922,6 @@ Example: Reply to Pamela Nguyen */ /* (placeholder) Help the user enter a URL into the field */ "http:\/\/my-site-address (URL)" = "http:\/\/my-site-address (URL)"; -/* Label displayed on image media items. */ -"image" = "画像"; - /* Sentence to justify why the app is asking permission from the user to use their camera. */ "infoplist.NSCameraUsageDescription" = "投稿に使用する写真または動画を撮る。"; @@ -10355,6 +10222,9 @@ Example: Reply to Pamela Nguyen */ /* Indicating that referrer was marked as spam */ "marked as spam" = "スパムとしてマーク"; +/* Products header text in Me Screen. */ +"me.products.header" = "商品"; + /* Title of error prompt shown when a sync fails. */ "media.syncFailed" = "メディアを同期できません"; @@ -10367,18 +10237,24 @@ Example: Reply to Pamela Nguyen */ /* Message of an alert informing users that the video they are trying to select is not allowed. */ "mediaExporter.videoLimitExceededError" = "5分以上の動画ファイルのアップロードには有料プランのご利用が必要です。"; -/* Verb. User action to dismiss error alert when failing to load media item. */ -"mediaItemTable.errorAlert.dismissButton" = "削除"; - /* Accessibility hint for add button to add items to the user's media library */ "mediaLibrary.addButtonAccessibilityHint" = "新しいメディアを追加"; /* Accessibility label for add button to add items to the user's media library */ "mediaLibrary.addButtonAccessibilityLabel" = "追加"; +/* Button name in the more menu */ +"mediaLibrary.aspectRatioGrid" = "縦横比グリッド"; + +/* Context menu button */ +"mediaLibrary.buttonDelete" = "削除"; + /* Media screen navigation bar button Select title */ "mediaLibrary.buttonSelect" = "選択"; +/* Context menu button */ +"mediaLibrary.buttonShare" = "共有"; + /* Verb. Button title. Tapping cancels an action. */ "mediaLibrary.deleteConfirmationCancel" = "キャンセル"; @@ -10400,6 +10276,21 @@ Example: Reply to Pamela Nguyen */ /* Text displayed in HUD after successfully deleting a media item */ "mediaLibrary.deletionSuccessMessage" = "削除しました。"; +/* The name of the media filter */ +"mediaLibrary.filterAll" = "すべて"; + +/* The name of the media filter */ +"mediaLibrary.filterAudio" = "音声ファイル"; + +/* The name of the media filter */ +"mediaLibrary.filterDocuments" = "文書"; + +/* The name of the media filter */ +"mediaLibrary.filterImages" = "画像"; + +/* The name of the media filter */ +"mediaLibrary.filterVideos" = "動画"; + /* User action to delete un-uploaded media. */ "mediaLibrary.retryOptionsAlert.delete" = "削除"; @@ -10412,6 +10303,12 @@ Example: Reply to Pamela Nguyen */ /* Message displayed when no results are returned from a media library search. Should match Calypso. */ "mediaLibrary.searchResultsEmptyTitle" = "検索と一致するメディアがありません"; +/* Text displayed in HUD if there was an error attempting to share a group of media items. */ +"mediaLibrary.sharingFailureMessage" = "選択したアイテムを共有できません。"; + +/* Button name in the more menu */ +"mediaLibrary.squareGrid" = "正方形グリッド"; + /* Media screen navigation title */ "mediaLibrary.title" = "メディア"; @@ -10433,6 +10330,12 @@ Example: Reply to Pamela Nguyen */ /* The title of the button to dismiss the alert shown when the picked media cannot be imported into stories. */ "mediaPicker.failedMediaExportAlert.dismissButton" = "削除"; +/* Error message when picked media cannot be imported into stories. */ +"mediaPicker.failedMediaExportAlert.message" = "メディアをエクスポートできませんでした。 問題が解決しない場合は、「ME」>「ヘルプとサポート」画面からお問い合わせください。"; + +/* Error title when picked media cannot be imported into stories. */ +"mediaPicker.failedMediaExportAlert.title" = "メディアのエクスポートに失敗しました"; + /* Message for alert when access to camera is not granted */ "mediaPicker.noCameraAccessMessage" = "このアプリが新しいメディアをキャプチャするためカメラにアクセスするには許可が必要です。許可するには、プライバシー設定を変更してください。"; @@ -10466,6 +10369,12 @@ Example: Reply to Pamela Nguyen */ /* The name of the action in the context menu */ "mediaPicker.takeVideo" = "動画を撮影"; +/* Navigation title for media preview. Example: 1 of 3 */ +"mediaPreview.NofM" = "%1$@ \/ %2$@"; + +/* Max image size in pixels (e.g. 300x300px) */ +"mediaSizeSlider.valueFormat" = "%1$d × %2$d px"; + /* The description in the Delete WordPress screen */ "migration.deleteWordpress.description" = "WordPress アプリがまだインストールされているようです。"; @@ -10478,9 +10387,6 @@ Example: Reply to Pamela Nguyen */ /* The title in the Delete WordPress screen */ "migration.deleteWordpress.title" = "お使いの端末に WordPress アプリは不要になりました"; -/* Primary button title in the migration done screen. */ -"migration.done.actions.primary.title" = "完了"; - /* Footer for the migration done screen. */ "migration.done.footer" = "データの競合を避けるため、お使いの端末から WordPress アプリをアンインストールすることをおすすめします。"; @@ -10490,6 +10396,9 @@ Example: Reply to Pamela Nguyen */ /* Primary description in the migration done screen. */ "migration.done.primaryDescription" = "すべてのデータと設定を転送しました。 すべて移行先にあります。"; +/* Secondary description (second paragraph) in the migration done screen. */ +"migration.done.secondaryDescription" = "Jetpack アプリで WordPress のジャーニーを続ける時が来ました。"; + /* Title of the migration done screen. */ "migration.done.title" = "Jetpack にお切り替えいただきありがとうございます。"; @@ -10538,6 +10447,9 @@ Example: Reply to Pamela Nguyen */ /* The title in the migration welcome screen */ "migration.welcome.title" = "Jetpack へようこそ !"; +/* Primary button title in the migration done screen. */ +"migrationDone.actions.primaryTitle" = "スタート"; + /* Description for the static screen displayed prompting users to switch the Jetpack app. */ "movedToJetpack.description" = "Jetpack アプリで WordPress アプリの全機能に加えて統計、Reader、通知などを特別に利用できるようになりました。"; @@ -10613,6 +10525,30 @@ Example: Reply to Pamela Nguyen */ /* Message title for when a user has no sites. */ "mySite.noSites.title" = "サイトがありません"; +/* Menu title for the add site option */ +"mySite.siteActions.addSite" = "サイトを追加"; + +/* Button that reveals more site actions */ +"mySite.siteActions.button" = "サイトのアクション"; + +/* Accessibility hint for button used to show more site actions */ +"mySite.siteActions.hint" = "タップするとその他のサイトのアクションが表示されます"; + +/* Menu title for the personalize home option */ +"mySite.siteActions.personalizeHome" = "ホームをパーソナライズ"; + +/* Menu title for the change site icon option */ +"mySite.siteActions.siteIcon" = "サイトアイコンを変更"; + +/* Menu title for the change site title option */ +"mySite.siteActions.siteTitle" = "サイトタイトルを変更"; + +/* Menu title for the switch site option */ +"mySite.siteActions.switchSite" = "サイトを切り替え"; + +/* Menu title for the visit site option */ +"mySite.siteActions.visitSite" = "サイトを表示"; + /* Dismiss button title. */ "noResultsViewController.dismissButton" = "閉じる"; @@ -10628,14 +10564,20 @@ Example: Reply to Pamela Nguyen */ /* This is one of the buttons we display when prompting the user for a review */ "notifications.appRatings.sendFeedback.yes.buttonTitle" = "フィードバックを送る"; -/* Word separating the current index from the total amount. I.e.: 7 of 9 */ -"of" = "\/"; +/* Badge for page cells */ +"pageList.badgeHomepage" = "ホームページ"; + +/* Badge for page cells */ +"pageList.badgeLocalChanges" = "ローカルの変更"; -/* Label displayed on media items that are not video, image, or audio. */ -"other" = "その他"; +/* Badge for page cells */ +"pageList.badgePendingReview" = "レビュー待ち"; -/* Promote the page with Blaze. */ -"pages.blaze.actionTitle" = "Blaze を使って宣伝"; +/* Badge for page cells */ +"pageList.badgePosts" = "投稿ページ"; + +/* Badge for page cells */ +"pageList.badgePrivate" = "非公開"; /* Subtitle of the theme template homepage cell */ "pages.template.subtitle" = "ホームページではテーマテンプレートが使用されており、Web エディターで開きます。"; @@ -10643,6 +10585,36 @@ Example: Reply to Pamela Nguyen */ /* Title of the theme template homepage cell */ "pages.template.title" = "ホームページ"; +/* Message informing the user that their static homepage page was set successfully */ +"pages.updatePage.successTitle" = "ページの更新に成功しました"; + +/* Delete option in the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.actionTitle" = "完全に削除"; + +/* Message of the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.alertMessage" = "このページを完全に削除しますか ?"; + +/* Title of the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.alertTitle" = "完全に削除しますか ?"; + +/* Menu option for filtering posts by everyone */ +"pagesList.pagesByEveryone" = "全員のページ"; + +/* Menu option for filtering posts by me */ +"pagesList.pagesByMe" = "自分のページ"; + +/* Trash option in the trash page confirmation alert. */ +"pagesList.trash.actionTitle" = "ゴミ箱に移動"; + +/* Message of the trash page confirmation alert. */ +"pagesList.trash.alertMessage" = "本当にこのページをゴミ箱に移動しますか ?"; + +/* Title of the trash page confirmation alert. */ +"pagesList.trash.alertTitle" = "このページをゴミ箱に移動しますか ?"; + +/* Cancels an Action */ +"pagesList.trash.cancel" = "キャンセル"; + /* No comment provided by engineer. */ "password" = "パスワード"; @@ -10682,6 +10654,51 @@ Example: Reply to Pamela Nguyen */ /* Register Domain - Domain contact information field Phone */ "phone number" = "電話番号"; +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.createdTimeAgo" = "%@に作成済み"; + +/* Status mesasge for post cells */ +"post.deletingPostPermanentlyStatusMessage" = "投稿を削除中..."; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.editedTimeAgo" = "%@に編集済み"; + +/* Status mesasge for post cells */ +"post.movingToTrashStatusMessage" = "投稿をゴミ箱に移動中..."; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.publishedTimeAgo" = "%@に公開"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.scheduledForDate" = "%@に予約"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.trashedTimeAgo" = "%@にゴミ箱に移動"; + +/* Accessibility label for the post author in the post list. The parameter is the author name. For example, \"By Elsa.\" */ +"postList.a11y.authorChunkFormat" = "作成者: %@。"; + +/* Accessibility label for a post's excerpt in the post list. The parameter is the post excerpt. For example, \"Excerpt. This is the first paragraph.\" */ +"postList.a11y.exerptChunkFormat" = "抜粋: %@。"; + +/* Accessibility label for a sticky post in the post list. */ +"postList.a11y.sticky" = "先頭固定表示。"; + +/* Accessibility label for a post in the post list. The first placeholder is the post title. The second placeholder is the date. */ +"postList.a11y.titleAndDateChunkFormat" = "%1$@、%2$@。"; + +/* Title for the 'Trash' post list row swipe action */ +"postList.swipeActionDelete" = "ゴミ箱"; + +/* Title for the 'Delete' post list row swipe action */ +"postList.swipeActionDeletePermanently" = "削除"; + +/* Title for the 'Share' post list row swipe action */ +"postList.swipeActionShare" = "共有"; + +/* Title for the 'View' post list row swipe action */ +"postList.swipeActionView" = "表示"; + /* User action to dismiss featured media options. */ "postSettings.featuredImageUploadActionSheet.dismiss" = "閉じる"; @@ -10700,9 +10717,84 @@ Example: Reply to Pamela Nguyen */ /* Button in Post Settings */ "postSettings.setFeaturedImageButton" = "アイキャッチ画像を設定"; +/* Error message on post/page settings screen */ +"postSettings.updateFailedMessage" = "投稿設定を更新できませんでした"; + /* Promote the post with Blaze. */ "posts.blaze.actionTitle" = "Blaze を使って宣伝"; +/* Label for the Post List option that cancels automatic uploading of a post. */ +"posts.cancelUpload.actionTitle" = "アップロードのキャンセル"; + +/* Label for post comments option. Tapping displays comments for a post. */ +"posts.comments.actionTitle" = "コメント"; + +/* Label for the delete post option. Tapping permanently deletes a post. */ +"posts.delete.actionTitle" = "完全に削除"; + +/* Label for an option that moves a post to the draft folder */ +"posts.draft.actionTitle" = "下書きに移動"; + +/* Label for post duplicate option. Tapping creates a copy of the post. */ +"posts.duplicate.actionTitle" = "複製"; + +/* Opens a submenu for page attributes. */ +"posts.pageAttributes.actionTitle" = "ページ属性"; + +/* Label for the preview post button. Tapping displays the post as it appears on the web. */ +"posts.preview.actionTitle" = "プレビュー"; + +/* Label for an option that moves a publishes a post immediately */ +"posts.publish.actionTitle" = "今すぐ公開"; + +/* Retry uploading the post. */ +"posts.retry.actionTitle" = "再試行"; + +/* Set the selected page as the homepage. */ +"posts.setHomepage.actionTitle" = "ホームページに設定する"; + +/* Set the parent page for the selected page. */ +"posts.setParent.actionTitle" = "親の設定"; + +/* Set the selected page as a posts page. */ +"posts.setPostsPage.actionTitle" = "投稿ページとして設定"; + +/* Set the selected page as a regular page. */ +"posts.setRegularPage.actionTitle" = "標準ページとして設定"; + +/* Label for post settings option. Tapping displays settings for a post. */ +"posts.settings.actionTitle" = "設定"; + +/* Share the post. */ +"posts.share.actionTitle" = "共有"; + +/* Label for post stats option. Tapping displays statistics for a post. */ +"posts.stats.actionTitle" = "統計情報"; + +/* Label for a option that moves a post to the trash folder */ +"posts.trash.actionTitle" = "ゴミ箱に移動"; + +/* Label for the view post button. Tapping displays the post as it appears on the web. */ +"posts.view.actionTitle" = "表示"; + +/* A short message explaining that a page was deleted permanently. */ +"postsList.deletePage.message" = "ページが完全に削除されました"; + +/* A short message explaining that a post was deleted permanently. */ +"postsList.deletePost.message" = "投稿が完全に削除されました"; + +/* A short message explaining that a page was moved to the trash bin. */ +"postsList.movePageToTrash.message" = "ページをゴミ箱へ移動しました"; + +/* A short message explaining that a post was moved to the trash bin. */ +"postsList.movePostToTrash.message" = "投稿をゴミ箱へ移動しました"; + +/* Menu option for filtering posts by everyone */ +"postsList.postsByEveryone" = "全員の投稿"; + +/* Menu option for filtering posts by me */ +"postsList.postsByMe" = "自分の投稿"; + /* Title for the button to subscribe to Jetpack Social on the remaining shares view */ "postsettings.social.remainingshares.subscribe" = "今すぐ購読してさらにシェア"; @@ -10847,13 +10939,15 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Text for the 'Like' button on the reader post card cell. */ "reader.post.button.like" = "いいね"; -/* Accessibility hint for the like button on the reader post card cell - Accessibility hint for the liked button on the reader post card cell */ +/* Accessibility hint for the like button on the reader post card cell */ "reader.post.button.like.accessibility.hint" = "投稿に「いいね」を付けます。"; /* Text for the 'Liked' button on the reader post card cell. */ "reader.post.button.liked" = "いいね済み"; +/* Accessibility hint for the liked button on the reader post card cell */ +"reader.post.button.liked.accessibility.hint" = "投稿の「いいね」を外します。"; + /* Accessibility hint for the site header on the reader post card cell */ "reader.post.button.menu.accessibility.hint" = "その他のアクションのあるメニューを開きます。"; @@ -10917,6 +11011,12 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Title for the tooltip anchor. */ "readerDetail.tooltipAnchorTitle.accessibilityLabel" = "新規"; +/* The button title for the transfer footer view in Register Domain screen */ +"register.domain.transfer.button.title" = "ドメイン移管"; + +/* The title for the transfer footer view in Register Domain screen */ +"register.domain.transfer.title" = "すでにお持ちのドメインの転送を希望していますか ?"; + /* Information of what related post are and how they are presented */ "relatedPostsSettings.optionsFooter" = "「関連記事」は投稿の下にサイト内の関連コンテンツを表示します。"; @@ -11013,6 +11113,21 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Accessibility hint for actions when displaying media items. */ "siteMedia.cellAccessibilityHint" = "メディアを選択します。"; +/* Accessibility hint for media item preview for user's viewing an item in their media library */ +"siteMediaItem.contentViewAccessibilityHint" = "タップしてメディアをフルスクリーンで表示"; + +/* Accessibility label for media item preview for user's viewing an item in their media library */ +"siteMediaItem.contentViewAccessibilityLabel" = "メディアをプレビュー"; + +/* Title for confirmation navigation bar button item */ +"siteMediaPicker.add" = "追加"; + +/* Button selection media in media picker */ +"siteMediaPicker.deselect" = "選択を解除"; + +/* Button selection media in media picker */ +"siteMediaPicker.select" = "選択"; + /* Media screen navigation title */ "siteMediaPicker.title" = "メディア"; @@ -11020,7 +11135,7 @@ Example: given a notice format "Following %@" and empty site name, this will be "siteSettings.privacy.title" = "プライバシー"; /* Hint for users when hidden privacy setting is set */ -"siteVisibility.hidden.hint" = "誰でもサイトを閲覧可能にし、検索エンジンにはインデックスしないよう要求します。"; +"siteVisibility.hidden.hint" = "閲覧の準備が整うまでは「まもなく公開予定」の通知が表示され、訪問者はサイトを閲覧できないようになっています。"; /* Text for privacy settings: Hidden */ "siteVisibility.hidden.title" = "非表示"; @@ -11181,6 +11296,12 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Dismiss the AlertView */ "stockPhotos.strings.dismiss" = "削除"; +/* Subtitle for placeholder in Free Photos. The company name 'Pexels' should always be written as it is. */ +"stockPhotos.subtitle" = "写真提供元: Pexels"; + +/* Title for placeholder in Free Photos */ +"stockPhotos.title" = "無料の写真を検索して、メディアライブラリに追加してください。"; + /* Section title for prominent suggestions */ "suggestions.section.prominent" = "この会話内"; @@ -11328,6 +11449,9 @@ Example: given a notice format "Following %@" and empty site name, this will be /* View title for Help & Support page. */ "support.title" = "ヘルプ"; +/* Title for placeholder in Tenor picker */ +"tenor.welcomeMessage" = "GIF を検索して、メディアライブラリに追加してください。"; + /* Header of delete screen section listing things that will be deleted. */ "these items will be deleted:" = "次の項目が削除されます:"; @@ -11343,9 +11467,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Displayed in the confirmation alert when marking unread notifications as read. */ "unread" = "未読"; -/* Label displayed on video media items. */ -"video" = "動画"; - /* Portion of a message for Jetpack users that have multisite WP installation, thus Restore is not available. This part is a link, colored with a different color. */ "visit our documentation page" = "ドキュメントページに移動する"; diff --git a/WordPress/Resources/ko.lproj/Localizable.strings b/WordPress/Resources/ko.lproj/Localizable.strings index 4f728f8ef730..0d5a42f16621 100644 --- a/WordPress/Resources/ko.lproj/Localizable.strings +++ b/WordPress/Resources/ko.lproj/Localizable.strings @@ -1,4 +1,4 @@ -/* Translation-Revision-Date: 2023-10-19 09:54:08+0000 */ +/* Translation-Revision-Date: 2024-01-04 10:54:08+0000 */ /* Plural-Forms: nplurals=1; plural=0; */ /* Generator: GlotPress/4.0.0-alpha.11 */ /* Language: ko_KR */ @@ -152,9 +152,6 @@ /* Difference label for Period Overview stat, indicating change from previous period. Ex: +99.9K (5%) */ "%@%@ (%@%%)" = "%1$@%2$@(%3$@%%)"; -/* Accessibility label for a post in the post list. The parameters are the title, and date respectively. For example, \"Let it Go, 1 hour ago.\" */ -"%@, %@." = "%1$@, %2$@."; - /* Accessibility value for a Stats' Posting Activity Month if the user selected a day with posts. The first parameter is day (e.g. November 2019). The second parameter is the number of posts. */ "%@. %d posts." = "%1$@. %2$d개의 글이 있습니다."; @@ -210,9 +207,6 @@ /* Age between dates over one year. */ "%d years" = "%d 년"; -/* Max image size in pixels (e.g. 300x300px) */ -"%dx%dpx" = "%1$dx%2$d픽셀"; - /* One menu area available in the theme */ "%i menu area in this theme" = "%i 이 테마의 메뉴 영역"; @@ -271,6 +265,9 @@ translators: Block name. %s: The localized block name */ /* translators: %s: social link name e.g: \"Instagram\". */ "%s social icon" = "%s 소셜 아이콘"; +/* translators: displayed right after the classic block is converted to blocks. %s: The localized classic block name */ +"'%s' block converted to blocks" = "블록으로 '%s' 블록 변환됨"; + /* translators: Missing block alert title. %s: The localized block name */ "'%s' is not fully-supported" = "‘%s’을(를) 완전히 지원하지 않습니다"; @@ -478,13 +475,6 @@ translators: Block name. %s: The localized block name */ /* Label for the Activity Type filter button when there are more than 1 activity type selected */ "Activity Type (%1$d)" = "활동 유형 (%1$d)"; -/* Accessibility label for add button to add items to the user's media library - Remove asset from media picker list */ -"Add" = "추가"; - -/* Action for Media Picker to indicate selection of media. The argument in the string represents the number of elements (as numeric digits) selected */ -"Add %@" = "%@개 추가"; - /* No comment provided by engineer. */ "Add Block After" = "다음 블록 이후에 추가하기"; @@ -581,9 +571,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Add menu item to children" = "메뉴 항목을 하위 항목에 추가"; -/* Accessibility hint for add button to add items to the user's media library */ -"Add new media" = "새 미디어 추가"; - /* Screen reader text for Menus button that adds a new menu to a site. */ "Add new menu" = "새 메뉴 추가하기"; @@ -649,9 +636,6 @@ translators: Block name. %s: The localized block name */ /* Option to select the Airmail app when logging in with magic links */ "Airmail" = "에어메일"; -/* Description of albums in the photo libraries */ -"Albums" = "앨범"; - /* Image alignment option title. Title of the screen for choosing an image's alignment. */ "Alignment" = "정렬"; @@ -665,6 +649,9 @@ translators: Block name. %s: The localized block name */ Title of the drafts filter. This filter shows a list of draft posts. */ "All" = "모든"; +/* Information about redeeming domain credit on site dashboard. */ +"All WordPress.com annual plans include a custom domain name. Register your free domain now." = "모든 워드프레스닷컴 연간 요금제에는 사용자 정의 도메인 네임이 포함됩니다. 지금 무료 도메인을 등록하세요."; + /* Footer of the free domain registration section for a paid plan. Information about redeeming domain credit on site dashboard. */ "All WordPress.com plans include a custom domain name. Register your free premium domain now." = "모든 워드프레스닷컴 요금제에는 사용자 정의 도메인 네임이 포함됩니다. 지금 무료 프리미엄 도메인을 등록하세요."; @@ -730,10 +717,13 @@ translators: Block name. %s: The localized block name */ "Alt Text" = "대체 텍스트"; /* No comment provided by engineer. */ -"Alternatively, you can detach and edit these blocks separately by tapping “Detach patterns”." = "또는 \"일반 블록으로 변환\"을 눌러 이러한 블록을 분리하고 따로 편집할 수 있습니다."; +"Alternatively, you can convert the content to blocks." = "콘텐츠를 블록으로 변환할 수도 있습니다."; /* No comment provided by engineer. */ -"Alternatively, you can detach and edit this block separately by tapping “Detach pattern”." = "또는 \"일반 블록으로 변환\"을 눌러 이 블록을 분리하고 따로 편집할 수 있습니다."; +"Alternatively, you can detach and edit this block separately by tapping “Detach”." = "그 대신에 \"분리\"를 눌러 이 블록을 분리하고 따로 편집하실 수 있습니다."; + +/* translators: Alternative option included in a warning related to having blocks deeply nested. */ +"Alternatively, you can flatten the content by ungrouping the block." = "그 대신에 블록 그룹 해제를 통해 콘텐츠를 평평하게 하실 수 있습니다."; /* Instruction text to explain to help users type their password instead of using magic link login option. */ "Alternatively, you may enter the password for this account." = "또는 이 계정에 대한 비밀번호를 입력하셔도 됩니다."; @@ -882,15 +872,9 @@ translators: Block name. %s: The localized block name */ /* Message prompting the user to confirm that they want to disconnect Jetpack from the site. */ "Are you sure you want to disconnect Jetpack from the site?" = "사이트에서 젯팩 연결을 해제하시겠어요?"; -/* Message prompting the user to confirm that they want to permanently delete a group of media items. */ -"Are you sure you want to permanently delete these items?" = "선택한 항목을 영구 삭제하시겠습니까?"; - /* Message prompting the user to confirm that they want to permanently delete a media item. Should match Calypso. */ "Are you sure you want to permanently delete this item?" = "이 항목을 영구적으로 삭제하시겠습니까?"; -/* Message of the confirmation alert when deleting a page from the trash. */ -"Are you sure you want to permanently delete this page?" = "이 페이지를 영구적으로 삭제하시겠어요?"; - /* Message of the confirmation alert when deleting a post from the trash. */ "Are you sure you want to permanently delete this post?" = "이 글을 영구적으로 삭제하시겠습니까?"; @@ -922,9 +906,6 @@ translators: Block name. %s: The localized block name */ /* Title of message shown when user taps submit for review. */ "Are you sure you want to submit for review?" = "리뷰를 위해 제출하시겠습니까?"; -/* Message of the trash page confirmation alert. */ -"Are you sure you want to trash this page?" = "이 페이지를 휴지통으로 이동하시겠어요?"; - /* Message of the trash confirmation alert. */ "Are you sure you want to trash this post?" = "정말로 이 글을 삭제하기를 원하시나요?"; @@ -965,9 +946,6 @@ translators: Block name. %s: The localized block name */ /* translators: accessibility text. Empty Audio caption. */ "Audio caption. Empty" = "오디오 캡션. 비었음"; -/* Accessibility label for audio items in the media collection view. The parameter is the creation date of the audio. */ -"Audio, %@" = "오디오, %@"; - /* No comment provided by engineer. */ "Authenticating" = "인증하기"; @@ -1178,10 +1156,7 @@ translators: Block name. %s: The localized block name */ "Blocks menu" = "블록 메뉴"; /* translators: Warning related to having blocks deeply nested. %d: The deepest nesting level. */ -"Blocks nested deeper than %d levels may not render properly in the mobile editor. For this reason, we recommend flattening the content by ungrouping the block or editing the block using the web editor." = "%d 레벨보다 깊숙이 중첩된 블록은 모바일 편집기에서 올바르게 렌더링되지 않을 수 있습니다. 따라서 블록 그룹 해제를 통해 콘텐츠를 평평하게 하거나 웹 편집기를 사용하여 블록을 편집하는 것이 좋습니다."; - -/* translators: Warning related to having blocks deeply nested. %d: The deepest nesting level. */ -"Blocks nested deeper than %d levels may not render properly in the mobile editor. For this reason, we recommend flattening the content by ungrouping the block or editing the block using your web browser." = "%d 레벨보다 깊숙이 중첩된 블록은 모바일 편집기에서 올바르게 렌더링되지 않을 수 있습니다. 따라서 블록 그룹 해제를 통해 콘텐츠를 평평하게 하거나 웹 브라우저를 사용하여 블록을 편집하는 것이 좋습니다."; +"Blocks nested deeper than %d levels may not render properly in the mobile editor." = "%d 레벨보다 깊숙이 중첩된 블록은 모바일 편집기에서 올바르게 렌더링되지 않을 수 있습니다."; /* Title of a button that displays the WordPress.com blog */ "Blog" = "블로그"; @@ -1259,9 +1234,6 @@ translators: Block name. %s: The localized block name */ /* Label for the post author in the post detail. */ "By " = "작성자"; -/* Accessibility label for the post author in the post list. The parameter is the author name. For example, \"By Elsa.\" */ -"By %@." = "%@ 작성."; - /* Legal disclaimer for logging in. The underscores _..._ denote underline. */ "By continuing, you agree to our _Terms of Service_." = "계속하는 것은, _약관_에 동의하는 것입니다."; @@ -1281,8 +1253,7 @@ translators: Block name. %s: The localized block name */ /* Label for size of media while it's being calculated. */ "Calculating..." = "계산 중..."; -/* Accessibility label for taking an image or video with the camera on formatting toolbar. - Accessibility label for the camera tile in the collection view */ +/* Accessibility label for taking an image or video with the camera on formatting toolbar. */ "Camera" = "카메라"; /* Title of an alert informing the user the camera permission for the app is disabled and its needed to proceed */ @@ -1333,10 +1304,7 @@ translators: Block name. %s: The localized block name */ Cancels an Action Cancels an alert. Cancels the mark all as read action. - Dismiss the post action sheet Editing GIF alert cancel action button. - Label for a cancel button - Label for the auto-upload cancelation button in the post list. Tapping will prevent the app from auto-uploading the post. Menus cancel button for deleting a menu. Menus cancel button within text bar while editing items. Menus: Cancel button title for canceling an edited menu item. @@ -1353,10 +1321,6 @@ translators: Block name. %s: The localized block name */ Verb. Title for Jetpack Restore cancel button. */ "Cancel" = "취소"; -/* Label for the Post List option that cancels automatic uploading of a post. - Media Library option to cancel an in-progress or failed upload. */ -"Cancel Upload" = "업로드 취소"; - /* Menus alert button title to cancel discarding changes and not createa a new menu. Menus alert button title to cancel discarding changes and not select a new menu Menus alert button title to cancel discarding changes and not select a new menu location */ @@ -1415,9 +1379,6 @@ translators: Block name. %s: The localized block name */ Main title */ "Change Password" = "비밀번호 변경"; -/* Title of button that takes user to the system Settings section for the app */ -"Change Settings" = "설정 변경"; - /* Change Username title. */ "Change Username" = "사용자 이름 변경"; @@ -1557,9 +1518,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Choose file" = "파일 선택하기"; -/* Menu option for selecting media from the device's photo library. */ -"Choose from My Device" = "내 기기에서 선택"; - /* Explanatory text for Homepage Settings homepage type selection. */ "Choose from a homepage that displays your latest posts (classic blog) or a fixed \/ static page." = "최신 글(클래식 블로그) 또는 수정되었거나 정적인 페이지를 표시하는 홈페이지를 선택합니다."; @@ -1760,9 +1718,6 @@ translators: Block name. %s: The localized block name */ /* Community & Non-Profit site intent topic */ "Community & Non-Profit" = "공동체 및 비영리"; -/* Accessibility indication that the current Post List style is currently Compact. */ -"Compact" = "콤팩트"; - /* The action is completed */ "Completed" = "완료"; @@ -1948,10 +1903,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Copied block" = "복사된 블록"; -/* Copy the post url and paste anywhere in phone - Label for page copy link. Tapping copy the url of page */ -"Copy Link" = "링크 복사"; - /* Copy link to oomment button title */ "Copy Link to Comment" = "댓글로 링크 복사"; @@ -2060,9 +2011,6 @@ translators: Block name. %s: The localized block name */ /* Error title displayed when unable to close user account. */ "Couldn’t close account automatically" = "계정을 자동으로 닫을 수 없습니다."; -/* Message to show while media data source is finding the number of items available. */ -"Counting media items..." = "미디어 아이템 계산 중..."; - /* Period Stats 'Countries' header */ "Countries" = "국가"; @@ -2313,7 +2261,6 @@ translators: Block name. %s: The localized block name */ /* Delete Delete button title for the warning shown to the user when he refuses to re-login when the authToken is missing. Title for button that permanently deletes a media item (photo / video) - Title for button that permanently deletes one or more media items (photos / videos) Title of a delete button Title of the trash confirmation alert. */ "Delete" = "삭제"; @@ -2321,15 +2268,11 @@ translators: Block name. %s: The localized block name */ /* Menus confirmation button for deleting a menu. */ "Delete Menu" = "메뉴 삭제"; -/* Delete option in the confirmation alert when deleting a page from the trash. - Delete option in the confirmation alert when deleting a post from the trash. - Label for a button permanently deletes a page. - Label for the delete post option. Tapping permanently deletes a post. +/* Delete option in the confirmation alert when deleting a post from the trash. Title for button on the comment details page that deletes the comment when tapped. */ "Delete Permanently" = "영구적으로 삭제하기"; -/* Title of the confirmation alert when deleting a page from the trash. - Title of the confirmation alert when deleting a post from the trash. */ +/* Title of the confirmation alert when deleting a post from the trash. */ "Delete Permanently?" = "영구적으로 삭제할까요?"; /* Button label for deleting the current site @@ -2448,7 +2391,6 @@ translators: Block name. %s: The localized block name */ /* Accessibility label for button to dismiss a bottom sheet Accessibility label for the transparent space above the login dialog which acts as a button to dismiss the dialog. Accessibility label for the transparent space above the signup dialog which acts as a button to dismiss the dialog. - Action to show on alert when view asset fails. Dismiss a view. Verb */ "Dismiss" = "무시"; @@ -2466,12 +2408,6 @@ translators: Block name. %s: The localized block name */ User's Display Name */ "Display Name" = "대화명"; -/* Accessibility label for other media items in the media collection view. The parameter is the creation date of the document. */ -"Document, %@" = "문서, %@"; - -/* Accessibility label for other media items in the media collection view. The parameter is the filename file. */ -"Document: %@" = "문서: %@"; - /* Message shown when all tours have been completed */ "Doesn't it feel good to cross things off a list?" = "목록에서 항목을 지우는 것이 좋지 않나요?"; @@ -2632,8 +2568,7 @@ translators: Block name. %s: The localized block name */ /* Description of a Quick Start Tour */ "Draft and publish a post." = "임시글을 작성하여 글을 발행하세요."; -/* Title of the drafts filter. This filter shows a list of draft posts. - Title of the drafts header in search list. */ +/* Title of the drafts filter. This filter shows a list of draft posts. */ "Drafts" = "임시글"; /* No comment provided by engineer. */ @@ -2645,10 +2580,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Drag to adjust focal point" = "끌어서 초점 조정"; -/* Label for page duplicate option. Tapping creates a copy of the page. - Label for post duplicate option. Tapping creates a copy of the post. */ -"Duplicate" = "복제"; - /* No comment provided by engineer. */ "Duplicate block" = "블록 복제하기"; @@ -2661,13 +2592,9 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Each block has its own settings. To find them, tap on a block. Its settings will appear on the toolbar at the bottom of the screen." = "각 블록에는 자체 설정이 있습니다. 설정을 찾으려면 블록을 누르세요. 화면 하단의 도구 모음에 해당 설정이 표시됩니다."; -/* Accessibility label for edit button to enable multi selection mode in the user's media library - Edit the post. - Editing GIF alert default action button. +/* Editing GIF alert default action button. Edits a Comment Edits the comment - Label for a button that opens the Edit Page view controller - Label for the edit post button. Tapping displays the editor. User action to edit media details. Verb, edit a comment */ "Edit" = "편집"; @@ -2675,9 +2602,6 @@ translators: Block name. %s: The localized block name */ /* Title for the edit more button section */ "Edit \"More\" button" = "\"더 보기\" 버튼 편집"; -/* Button that displays the media editor to the user */ -"Edit %@" = "%@ 편집하기"; - /* Blocklist Keyword Edition Title */ "Edit Blocklist Word" = "차단 목록 단어 편집"; @@ -2870,9 +2794,6 @@ translators: Block name. %s: The localized block name */ /* Secondary message shown when there are no domains that match the user entered text. */ "Enter different words above and we'll look for an address that matches it." = "위에 다른 단어를 입력하면 일치하는 주소를 찾겠습니다."; -/* Accessibility hint for edit button to enable multi selection mode in the user's media library */ -"Enter edit mode to enable multi select to delete" = "편집 모드로 전환하여 삭제할 다중 선택 활성화하기"; - /* (placeholder) Help enter WordPress password Placeholder of a field to type a password to protect the post. */ "Enter password" = "비밀번호 입력"; @@ -3028,9 +2949,6 @@ translators: Block name. %s: The localized block name */ /* Short title telling the user they will receive a blogging reminder every day of the week. */ "Every day at %@" = "매일 %@"; -/* Label for the post author filter. This filter shows posts for all users on the blog. */ -"Everyone" = "모두 볼 수 있음"; - /* Example story title description */ "Example story title" = "예제 이야기 제목"; @@ -3040,9 +2958,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Excerpt length (words)" = "요약문 길이(단어)"; -/* Accessibility label for a post's excerpt in the post list. The parameter is the post excerpt. For example, \"Excerpt. This is the first paragraph.\" */ -"Excerpt. %@." = "요약입니다. %@."; - /* Should be the same as the text displayed if the user clicks the (i) in Calypso. */ "Excerpts are optional hand-crafted summaries of your content." = "원하는 경우 내 콘텐츠를 직접 요약한 발췌본을 작성할 수 있습니다."; @@ -3052,8 +2967,7 @@ translators: Block name. %s: The localized block name */ /* Accessibility Label for the exit full screen button on the full screen comment reply mode */ "Exit Full Screen" = "전체 화면 닫기"; -/* Accessibility indication that the current Post List style is currently Expanded. - Screen reader text to represent the expanded state of a UI control */ +/* Screen reader text to represent the expanded state of a UI control */ "Expanded" = "확장됨"; /* Accessibility hint */ @@ -3103,9 +3017,6 @@ translators: Block name. %s: The localized block name */ The action failed */ "Failed" = "실패함"; -/* Error title when picked media cannot be imported into stories. */ -"Failed Media Export" = "미디어 내보내기 실패"; - /* Message for mark all as read success notice */ "Failed marking Notifications as read" = "알림을 읽음으로 표시 실패"; @@ -3307,6 +3218,12 @@ translators: Block name. %s: The localized block name */ /* An example tag used in the login prologue screens. */ "Football" = "축구"; +/* translators: Recommendation included in a warning related to having blocks deeply nested. */ +"For this reason, we recommend editing the block using the web editor." = "따라서 웹 편집기를 사용하여 블록을 편집하는 것이 좋습니다."; + +/* translators: Recommendation included in a warning related to having blocks deeply nested. */ +"For this reason, we recommend editing the block using your web browser." = "따라서 웹 브라우저를 사용하여 블록을 편집하는 것이 좋습니다."; + /* Register Domain - Domain contact information section header description */ "For your convenience, we have pre-filled your WordPress.com contact information. Please review to be sure it’s the correct information you want to use for this domain." = "편의를 위해 워드프레스닷컴 연락처 정보를 미리 채웠습니다. 이 도메인에 사용하려는 정보가 정확한지 검토하여 확인하세요."; @@ -3624,8 +3541,7 @@ translators: Block name. %s: The localized block name */ "Home" = "홈"; /* Title for setting which shows the current page assigned as a site's homepage - Title for the homepage section in site settings screen - Title of the Homepage Badge */ + Title for the homepage section in site settings screen */ "Homepage" = "홈페이지"; /* Label for Homepage Settings site settings section @@ -3722,9 +3638,6 @@ translators: Block name. %s: The localized block name */ /* Hint for image title on image settings. */ "Image title" = "이미지 제목"; -/* Accessibility label for image thumbnails in the media collection view. The parameter is the creation date of the image. */ -"Image, %@" = "이미지, %@"; - /* Undated post time label */ "Immediately" = "즉시"; @@ -4210,9 +4123,6 @@ translators: Block name. %s: The localized block name */ Settings: Comments Approval settings */ "Links in comments" = "댓글의 링크"; -/* The accessibility label for the list style button in the Post List. */ -"List style" = "목록 스타일"; - /* Title of the screen that load selected the revisions. */ "Load" = "로드"; @@ -4228,18 +4138,12 @@ translators: Block name. %s: The localized block name */ /* Text displayed while loading the activity feed for a site */ "Loading Backups..." = "백업 로드 중…"; -/* Phrase to show when the user has searched for GIFs and they are being loaded. */ -"Loading GIFs..." = "GIF 로드 중..."; - /* Menus label text displayed while menus are loading */ "Loading Menus..." = "메뉴 로드 중..."; /* Text displayed while loading site People. */ "Loading People..." = "사용자 로드 중..."; -/* Phrase to show when the user has searched for images and they are being loaded. */ -"Loading Photos..." = "사진 로드 중..."; - /* Text displayed while loading plans details */ "Loading Plan..." = "요금제 로드 중..."; @@ -4300,8 +4204,7 @@ translators: Block name. %s: The localized block name */ /* Local Services site intent topic */ "Local Services" = "지역 봉사"; -/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. - Title of the Local Changes Badge */ +/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. */ "Local changes" = "로컬 변경"; /* Title for alert when a generic error happened when trying to find the location of the device */ @@ -4465,7 +4368,6 @@ translators: Block name. %s: The localized block name */ "Max Video Upload Size" = "최대 비디오 업로드 크기"; /* Accessibility label for the Me button in My Site. - Label for the post author filter. This filter shows posts only authored by the current user. Me page title Noun. Title. Links to the Me screen. The accessibility value of the me tab. @@ -4473,9 +4375,7 @@ translators: Block name. %s: The localized block name */ "Me" = "나"; /* Noun. Title. Links to the blog's Media library. - Tab bar title for the Media tab in Media Picker The menu item to select during a guided tour. - Title for Media Library section of the app. Title for the media section in site settings screen Title label for the media settings section in the app settings */ "Media" = "미디어"; @@ -4487,13 +4387,6 @@ translators: Block name. %s: The localized block name */ /* Label for size of media cache in the app. */ "Media Cache Size" = "미디어 캐시 크기"; -/* Title for alert when access to media capture is not granted */ -"Media Capture" = "미디어 캡처"; - -/* Title for alert when a generic error happened when loading media - Title for alert when access to the media library is not granted by the user */ -"Media Library" = "미디어 라이브러리"; - /* Title for action sheet with media options. */ "Media Options" = "미디어 옵션"; @@ -4516,9 +4409,6 @@ translators: Block name. %s: The localized block name */ /* translators: %s: block title e.g: \"Paragraph\". */ "Media options" = "미디어 옵션"; -/* Alert title when there is issues loading an asset to preview. */ -"Media preview failed." = "미디어 미리보기 실패함"; - /* Alert displayed to the user when multiple media items have uploaded successfully. */ "Media uploaded (%ld files)" = "미디어 업로드됨(%ld개 파일)"; @@ -4556,9 +4446,6 @@ translators: Block name. %s: The localized block name */ Label for the share message field on the post settings. */ "Message" = "메시지"; -/* Title of section containing image / video metadata such as size and file type */ -"Metadata" = "메타데이터"; - /* Option to select the Microsft Outlook app when logging in with magic links */ "Microsoft Outlook" = "마이크로소프트 아웃룩"; @@ -4578,13 +4465,11 @@ translators: Block name. %s: The localized block name */ "Months and Years" = "월 및 연도"; /* Accessibility label for more button in dashboard quick start card. - Accessibility label for the More button in Page List. Accessibility label for the More button in Post List (compact view). Accessibility label for the More button on formatting toolbar. Accessibility label for the More button on Reader Cell Accessibility label for the More button on Reader's post details - Action button to display more available options - Label for the more post button. Tapping displays an action sheet with post options. */ + Action button to display more available options */ "More" = "더"; /* Action button to display more available options @@ -4642,15 +4527,8 @@ translators: Block name. %s: The localized block name */ /* Screen reader text for button that will move the menu item */ "Move menu item" = "메뉴 항목 움직이기"; -/* Label for a button that moves a page to the draft folder - Label for an option that moves a post to the draft folder */ -"Move to Draft" = "임시글로 이동"; - -/* Label for a button that moves a page to the trash folder - Label for a option that moves a post to the trash folder - Title for button on the comment details page that moves the comment to trash when tapped. +/* Title for button on the comment details page that moves the comment to trash when tapped. Trash option in the trash confirmation alert. - Trash option in the trash page confirmation alert. Trashes the comment */ "Move to Trash" = "휴지통으로 이동"; @@ -4682,7 +4560,8 @@ translators: Block name. %s: The localized block name */ Title of My Site tab */ "My Site" = "내 사이트"; -/* Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ +/* Title for site picker screen. + Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ "My Sites" = "내 사이트"; /* Siri Suggestion to open My Sites */ @@ -4932,9 +4811,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title for the view when there aren't any Activities to display in the Activity Log for a given filter. */ "No matching events found." = "일치하는 이벤트를 찾을 수 없습니다."; -/* Message displayed when no results are returned from a media library search. Should match Calypso. - Phrase to show when the user search for images but there are no result to show. - Phrase to show when the user searches for GIFs but there are no result to show. */ +/* Message displayed when no results are returned from a media library search. Should match Calypso. */ "No media matching your search" = "검색과 일치하는 미디어 없음"; /* Menus text shown when no menus were available for loading the Menus editor. */ @@ -4952,8 +4829,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Displayed in the Notifications Tab as a title, when there are no notifications */ "No notifications yet" = "아직 알림이 없습니다"; -/* Displayed when the user is searching the pages list and there are no matching pages - Text displayed when there's no matching with the text search */ +/* Text displayed when there's no matching with the text search */ "No pages matching your search" = "검색어와 일치하는 페이지 없음"; /* Text displayed when search for plugins returns no results */ @@ -4974,9 +4850,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Message shown whent the reader finds no posts for the chosen tag */ "No posts have been made recently with this tag." = "최근에 이 태그로 작성한 글이 없습니다."; -/* Displayed when the user is searching the posts list and there are no matching posts */ -"No posts matching your search" = "검색어와 일치하는 글 없음"; - /* Accessibility value for a Stats' Posting Activity Month if there are no posts. */ "No posts." = "글이 없습니다."; @@ -5077,9 +4950,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* A message title */ "Nothing liked yet" = "좋아요 없음"; -/* Default message for empty media picker */ -"Nothing to show" = "보여줄 것이 없습니다."; - /* Notifications Details Accessibility Identifier */ "Notification Details Table" = "알림 세부정보 테이블"; @@ -5139,7 +5009,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Alert dismissal title Button title. Acknowledges a prompt. Button title. An acknowledgement of the message displayed in a prompt. - Confirmation of action Default action Dismisses the alert Marks all notifications as read. @@ -5201,9 +5070,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* No comment provided by engineer. */ "Only show excerpt" = "요악문만 표시"; -/* Message telling the user that they've only enabled limited photo library permissions for the app. */ -"Only the selected photos you've given access to are available." = "접근 권한을 부여한 선택한 사진만 사용할 수 있습니다."; - /* An informal exclaimation that means `something went wrong`. Title for the view when there's an error loading a comment. Title for the view when there's an error loading Activity Log @@ -5238,9 +5104,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title of a button that opens the apps settings in the system Settings.app */ "Open Settings" = "설정 열기"; -/* Editor button to swich the media picker from quick mode to full picker */ -"Open full media picker" = "전체 미디어 선택기 열기"; - /* No comment provided by engineer. */ "Open in Safari" = "사파리에서 열기"; @@ -5280,6 +5143,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Divider on initial auth view separating auth options. */ "Or" = "또는"; +/* Instruction text for other forms of two-factor auth methods. */ +"Or choose another form of authentication." = "또는 다른 인증 양식을 선택해 주세요."; + /* Label for button to log in using site address. Underscores _..._ denote underline. */ "Or log in by _entering your site address_." = "또는 사이트 주소를 입력하여 로그인하세요."; @@ -5338,15 +5204,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title shown when selecting a post type of Page from the Share Extension. */ "Page" = "페이지"; -/* Prompts the user that a restored page was moved to the drafts list. */ -"Page Restored to Drafts" = "페이지를 원본으로 복원"; - -/* Prompts the user that a restored page was moved to the published list. */ -"Page Restored to Published" = "페이지를 게시물로 복원"; - -/* Prompts the user that a restored page was moved to the scheduled list. */ -"Page Restored to Scheduled" = "페이지를 예약된 글로 복원"; - /* Name of the button to open the page settings The title of the Page Settings screen. */ "Page Settings" = "페이지 설정"; @@ -5363,9 +5220,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of notification displayed when a page has failed to upload. */ "Page failed to upload" = "페이지 업로드 실패"; -/* A short message explaining that a page was moved to the trash bin. */ -"Page moved to trash." = "페이지를 휴지통으로 이동했습니다."; - /* Title of notification displayed when a page has been successfully saved as a draft. */ "Page pending review" = "페이지 검토 대기 중"; @@ -5437,8 +5291,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title of pending Comments filter. */ "Pending" = "대기중"; -/* Name for the status of a post pending review. - Title of the Pending Review Badge */ +/* Name for the status of a post pending review. */ "Pending review" = "대기중인 리뷰"; /* Noun. Title of the people management feature. @@ -5467,12 +5320,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Photography site intent topic */ "Photography" = "사진"; -/* Tab bar title for the Photos tab in Media Picker */ -"Photos" = "사진"; - -/* Subtitle for placeholder in Free Photos. The company name 'Pexels' should always be written as it is. */ -"Photos provided by Pexels" = "Pexels 제공 사진"; - /* Title for selecting a new username in the site creation flow. */ "Pick username" = "사용자명 선택"; @@ -5565,7 +5412,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Please enter the password for your WordPress.com account to log in with your Apple ID." = "Apple ID로 로그인하려면 워드프레스닷컴 계정의 비밀번호를 입력하세요."; /* Instruction text on the two-factor screen. */ -"Please enter the verification code from your authenticator app, or tap the link below to receive a code via SMS." = "인증 앱의 확인 코드를 입력하시거나, 아래의 링크를 눌러 SMS를 통해 받으시기 바랍니다."; +"Please enter the verification code from your authenticator app." = "OTP 앱의 확인 코드를 입력해 주세요."; /* Popup message to ask for user credentials (fields shown below). */ "Please enter your credentials" = "자격을 입력하세요"; @@ -5660,15 +5507,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ The post formats available for the post. Should be the same as in core WP. */ "Post Format" = "글 형식"; -/* Prompts the user that a restored post was moved to the drafts list. */ -"Post Restored to Drafts" = "글이 임시글 목록으로 복원됨"; - -/* Prompts the user that a restored post was moved to the published list. */ -"Post Restored to Published" = "글이 발행 목록으로 복원됨"; - -/* Prompts the user that a restored post was moved to the scheduled list. */ -"Post Restored to Scheduled" = "글이 예약 목록으로 복원됨"; - /* Name of the button to open the post settings The title of the Post Settings screen. */ "Post Settings" = "글 설정"; @@ -5688,9 +5526,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of notification displayed when a post has failed to upload. */ "Post failed to upload" = "글 업로드 실패"; -/* A short message explaining that a post was moved to the trash bin. */ -"Post moved to trash." = "글을 휴지통으로 이동했습니다."; - /* Title of notification displayed when a post has been successfully saved as a draft. */ "Post pending review" = "글 검토 대기 중"; @@ -5749,9 +5584,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Period Stats 'Posts and Pages' header */ "Posts and Pages" = "게시물과 페이지"; -/* Title of the Posts Page Badge */ -"Posts page" = "글 페이지"; - /* Message informing the user that their static homepage for posts was set successfully */ "Posts page successfully updated" = "글 페이지 업데이트됨"; @@ -5764,9 +5596,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* A message explaining the Posts I Like feature in the reader */ "Posts that you like will appear here." = "좋아요를 누른 글이 여기에 표시됩니다."; -/* Subtitle for placeholder in Tenor picker. `The company name 'Tenor' should always be written as it is. */ -"Powered by Tenor" = "테너가 제공합니다"; - /* Browse premium themes selection title */ "Premium" = "프리미엄"; @@ -5785,18 +5614,12 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title for screen to preview a static content. */ "Preview" = "미리보기"; -/* Action for Media Picker to preview the selected media items. The argument in the string represents the number of elements (as numeric digits) selected */ -"Preview %@" = "%@ 미리보기"; - /* Title for web preview device switching button */ "Preview Device" = "미리보기 장치"; /* Title on display preview error */ "Preview Unavailable" = "미리보기를 사용할 수 없음"; -/* Accessibility label for media item preview for user's viewing an item in their media library */ -"Preview media" = "미디어 미리보기"; - /* No comment provided by engineer. */ "Preview page" = "페이지 미리 보기"; @@ -5843,8 +5666,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Privacy notice for California users" = "캘리포니아 사용자를 위한 개인정보 공지"; /* Name for the status of a post that is marked private. - Privacy setting for posts set to 'Private'. Should be the same as in core WP. - Title of the Private Badge */ + Privacy setting for posts set to 'Private'. Should be the same as in core WP. */ "Private" = "개인용"; /* No comment provided by engineer. */ @@ -5894,12 +5716,10 @@ translators: %s: Select control button label e.g. \"Button width\" */ Label for the publish date button. */ "Publish Date" = "발행일"; -/* A short phrase indicating a post is due to be immedately published. - Label for a button that moves a page to the published folder, publishing with the current date/time. */ +/* A short phrase indicating a post is due to be immedately published. */ "Publish Immediately" = "즉시 발행"; /* Label for a button that publishes the post - Label for an option that moves a publishes a post immediately Title of button allowing the user to immediately publish the post they are editing. */ "Publish Now" = "지금 게시"; @@ -5917,8 +5737,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Name for the status of a published post. Period Stats 'Published' header - Title of the published filter. This filter shows a list of posts that the user has published. - Title of the published header in search list. */ + Title of the published filter. This filter shows a list of posts that the user has published. */ "Published" = "발행됨"; /* Precedes the name of the blog just posted on */ @@ -6060,8 +5879,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of the completion screen of the Blogging Reminders Settings screen when the reminders are removed. */ "Reminders removed" = "알림 제거됨"; -/* Add asset to media picker list - Alert button to confirm a plugin to be removed +/* Alert button to confirm a plugin to be removed Button label when removing a blog Label for the Remove Feature Image icon. Tapping will show a confirmation screen for removing the feature image from the post. Remove Action @@ -6214,9 +6032,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of secondary button on alert prompting verify their accounts while attempting to publish */ "Resend" = "재전송"; -/* Title of the reset button */ -"Reset" = "재설정"; - /* Accessibility label for the reset activity type button */ "Reset Activity Type filter" = "활동 유형 필터 재설정하기"; @@ -6271,12 +6086,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ Button title, displayed when media has failed to upload. Allows the user to try the upload again. Button title. Retries uploading a post. If a user taps the button with this label, the action that evinced this error view will be retried. - Label for a button that attempts to re-upload a page that previously failed to upload. - Label for the retry post upload button. Tapping attempts to upload the post again. Opens the media library . Retries the upload of a user's post. Retry updating User's Role - Retry uploading the post. Retry. Action Retry. Verb – retry a failed media upload. The Jetpack view button title used when an error occurred @@ -6288,9 +6100,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Button title that triggers a scan */ "Retry Scan" = "검사하기 다시 시도하기"; -/* User action to retry media upload. */ -"Retry Upload" = "다시 업로드"; - /* User action to retry all failed media uploads. */ "Retry all" = "모두 재시도"; @@ -6388,9 +6197,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Accessibility label for the 'Save Post' button when a post has been saved. */ "Saved Post" = "글이 저장됨"; -/* Text displayed in HUD when a media item's metadata (title, etc) is saved successfully. */ -"Saved!" = "저장되었습니다!"; - /* Accessibility hint for the 'Save Post' button. */ "Saves this post for later." = "나중에 사용할 수 있게 이 글 저장"; @@ -6401,7 +6207,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Saving post…" = "글을 저장하는 중…"; /* Menus save button title while it is saving a Menu. - Text displayed in HUD while a media item's metadata (title, etc) is being saved. Text displayed in HUD while a post is being saved as a draft. */ "Saving..." = "저장중..."; @@ -6492,21 +6297,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* No comment provided by engineer. */ "Search or type URL" = "검색하거나 URL 입력하기"; -/* Text displayed when the search controller will be presented */ -"Search pages" = "페이지 검색"; - -/* Text displayed when the search controller will be presented */ -"Search posts" = "글 검색"; - /* No comment provided by engineer. */ "Search settings" = "검색 설정"; -/* Title for placeholder in Tenor picker */ -"Search to find GIFs to add to your Media Library!" = "GIF를 검색하여 미디어 라이브러리에 추가하세요!"; - -/* Title for placeholder in Free Photos */ -"Search to find free photos to add to your Media Library!" = "무료 사진을 검색하여 미디어 라이브러리에 추가하세요!"; - /* Menus search bar placeholder text. */ "Search..." = "검색..."; @@ -6577,9 +6370,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Register Domain - Domain contact information field placeholder for Country */ "Select Country" = "국가 선택"; -/* Title of button that allows the user to select more photos to access within the app */ -"Select More" = "더 많이 선택"; - /* Blog Picker's Title */ "Select Site" = "사이트 선택"; @@ -6601,9 +6391,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Register domain - Title for the Choose domain button of Suggested domains screen */ "Select domain" = "도메인 선택"; -/* Accessibility hint for actions when displaying media items. */ -"Select media." = "미디어를 선택합니다."; - /* Accessibility label for selecting paragraph style button on formatting toolbar. */ "Select paragraph style" = "단락 스타일 선택"; @@ -6707,19 +6494,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label for connected service in Publicize stat. */ "Service" = "서비스"; -/* Label for a button that opens the Set Parent options view controller - Navigation title displayed on the navigation bar */ +/* Navigation title displayed on the navigation bar */ "Set Parent" = "상위 항목 설정"; /* No comment provided by engineer. */ "Set as Featured Image" = "특성 이미지로 설정"; -/* Label for a button that sets the selected page as the site's Homepage */ -"Set as Homepage" = "홈페이지로 설정"; - -/* Label for a button that sets the selected page as the site's Posts page */ -"Set as Posts Page" = "글 페이지로 설정"; - /* Notice confirming that an image has been set as the post's featured image. */ "Set as featured image" = "특성 이미지로 설정"; @@ -6763,7 +6543,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Accessibility label for share buttons in nav bars Button label to share a post Button label to share a web page - Share the post. Shares the comment URL Spoken accessibility label Title for a button that allows the user to share their answer to the prompt. @@ -7149,8 +6928,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Name of setting configured when a site uses a static page as its homepage */ "Static Homepage" = "정적인 홈페이지"; -/* Label for post stats option. Tapping displays statistics for a post. - Noun. Abbreviation of Statistics. Name of the Stats feature +/* Noun. Abbreviation of Statistics. Name of the Stats feature Noun. Abbv. of Statistics. Links to a blog's Stats screen. Stats 3D Touch Shortcut Stats window title @@ -7181,9 +6959,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label text that defines a post marked as sticky */ "Sticky" = "붙박이"; -/* Accessibility label for a sticky post in the post list. */ -"Sticky." = "붙박이입니다."; - /* User action to stop upload. */ "Stop upload" = "업로드 중지"; @@ -7240,7 +7015,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ Theme Support action title */ "Support" = "지원"; -/* Button used to switch site */ +/* Title for back button that leads to the site picker screen. */ "Switch Site" = "사이트 전환"; /* Switches the Editor to HTML Mode */ @@ -7328,9 +7103,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label explaining why users might want to add tags. */ "Tags help tell readers what a post is about. Separate different tags with commas." = "태그는 독자에게 무엇에 관한 글인지 말하도록 돕습니다. 다른 태그는 쉼표로 구분합니다."; -/* Menu option for taking an image or video with the device's camera. */ -"Take Photo or Video" = "사진 찍기 또는 동영상 촬영"; - /* No comment provided by engineer. */ "Take a Photo" = "사진 촬영"; @@ -7401,12 +7173,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Accessibility hint */ "Tap to select the previous period" = "눌러서 이전 기간 선택"; -/* Accessibility hint for button used to switch site */ -"Tap to switch to another site, or add a new site" = "눌러서 다른 사이트로 전환하거나 새 사이트 추가하기"; - -/* Accessibility hint for media item preview for user's viewing an item in their media library */ -"Tap to view media in full screen" = "미디어를 눌러 전체 화면에서 보기"; - /* Accessibility hint for a button that opens a new view with more details. */ "Tap to view more details." = "추가 상세 정보를 보려면 누르세요."; @@ -7452,10 +7218,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* No comment provided by engineer. */ "Text formatting controls are located within the toolbar positioned above the keyboard while editing a text block" = "텍스트 서식 지정 컨트롤은 텍스트 블록을 편집하는 동안 키보드 위에 배치되는 도구 모음 내에 있습니다."; -/* Button title - The button's title text to send a 2FA code via SMS text message. */ +/* Button title */ "Text me a code instead" = "코드를 문자로 대신 받기"; +/* The button's title text to send a 2FA code via SMS text message. */ +"Text me a code via SMS" = "나에게 SMS 문자 메시지로 코드 보내기"; + /* Message of alert when theme activation succeeds */ "Thanks for choosing %@ by %@" = "%1$@(%2$@ 제공)을(를) 선택해 주셔서 감사합니다."; @@ -7483,9 +7251,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Error message shown to a user who is trying to share to Facebook but does not have any available Facebook Pages. */ "The Facebook connection cannot find any Pages. Publicize cannot connect to Facebook Profiles, only published Pages." = "페이스북 페이지를 찾을 수 없습니다. 페이스북 프로필은 연결할 수 없으며 공개된 페이지로만 연결할 수 있습니다."; -/* Message shown when a GIF failed to load while trying to add it to the Media library. */ -"The GIF could not be added to the Media Library." = "GIF를 미디어 라이브러리에 추가할 수 없습니다."; - /* Description shown when a user logs in with Google but no matching WordPress.com account is found */ "The Google account \"%@\" doesn't match any account on WordPress.com" = "Google 계정 \"%@\"이(가) 워드프레스닷컴의 계정과 일치하지 않습니다."; @@ -7613,7 +7378,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Error message shown when user attempts to remove the site owner. */ "The user you are trying to remove is the owner of this site. Please contact support for assistance." = "제거하려는 사용자가 이 사이트의 소유자입니다. 도움이 필요한 경우 지원 팀에 문의하세요."; -/* Error message informing a user about an invalid password. */ +/* No comment provided by engineer. */ "The username or password stored in the app may be out of date. Please re-enter your password in the settings and try again." = "앱에 저장된 사용자명이나 비밀번호가 유효기간이 지났습니다. 설정에서 비밀번호를 재입력하고 다시 하세요. "; /* Message shown when a video failed to load while trying to add it to the Media library. */ @@ -7681,9 +7446,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* A short error message letting the user know about a problem displaying a post. */ "There was a problem displaying this post." = "글을 표시하는 데 문제가 있습니다."; -/* Error message displayed when the Media Library is unable to load a full sized preview of an item. */ -"There was a problem loading the media item." = "미디어 항목을 로드하는 데 문제가 발생했습니다."; - /* The loading view subtitle displayed when an error occurred */ "There was a problem loading your data, refresh your page to try again." = "데이터 로드 중에 문제가 발생했습니다. 페이지를 새로 고치고 다시 시도하세요."; @@ -7696,9 +7458,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Explaining to the user there was an error trying to obtain the current location of the user. */ "There was a problem when trying to access your location. Please try again later." = "위치 액세스 시도 중 문제가 발생했습니다. 나중에 다시 시도해 주세요."; -/* Explaining to the user there was an generic error accesing media. */ -"There was a problem when trying to access your media. Please try again later." = "미디어 액세스 시도 중 문제가 발생했습니다. 나중에 다시 시도해 주세요."; - /* Message for stories unknown error. */ "There was a problem with the Stories editor. If the problem persists you can contact us via the Me > Help & Support screen." = "스토리 편집기에서 문제가 발생했습니다. 문제가 지속될 경우 내 계정 > 도움 & 지원 화면을 통해 문의할 수 있습니다."; @@ -7769,9 +7528,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* A description informing the user in order to proceed with this feature we will need camera permissions, and how to enable it. */ "This app needs permission to access the Camera to scan login codes, tap on the Open Settings button to enable it." = "이 앱에서 카메라에 접근하려면 로그인 코드를 스캔하는 권한이 필요합니다. 설정 열기 버튼을 눌러 권한을 활성화하세요."; -/* Explaining to the user why the app needs access to the device media library. */ -"This app needs permission to access your device media library in order to add photos and\/or video to your posts. Please change the privacy settings if you wish to allow this." = "글에 사진 및\/또는 비디오를 추가하려면 이 앱이 장치의 미디어 라이브러리에 액세스할 수 있어야 합니다. 액세스를 허용하려면 프라이버시 설정을 변경하세요."; - /* No comment provided by engineer. */ "This color combination may be hard for people to read. Try using a brighter background color and\/or a darker text color." = "이 색상 조합은 사람들이 읽기 힘들 수 있습니다. 더 밝은 배경 색상 및\/또는 더 어두운 텍스트 색상을 사용해 보세요."; @@ -7881,6 +7637,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Description of a Quick Start Tour */ "Time to finish setting up your site! Our checklist walks you through the next steps." = "사이트 설정을 완료할 시간입니다! 체크리스트가 다음 단계를 안내합니다."; +/* Error when the uses takes more than 1 minute to submit a security key. */ +"Time's up, but don't worry, your security is our priority. Please try again!" = "시간이 다 되었지만, 걱정하지 마세요. 보안이 무엇보다 중요합니다. 다시 시도해 보세요!"; + /* WordPress.com Marketing Footer Text */ "Tips for getting the most out of WordPress.com." = "워드프레스닷컴을 최대한 활용하기 위한 팁."; @@ -8004,24 +7763,20 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title for the traffic section in site settings screen */ "Traffic" = "트래픽"; +/* Describes a domain that was transferred from elsewhere to wordpress.com */ +"Transferred Domain" = "이전된 도메인"; + /* translators: %s: block title e.g: \"Paragraph\". */ "Transform %s to" = "다음으로 %s 변형"; /* No comment provided by engineer. */ "Transform block…" = "블록 변형…"; -/* Accessibility label for trash button to delete items from the user's media library - Accessibility label for trash buttons in nav bars +/* Accessibility label for trash buttons in nav bars Trashes a comment Trashes the comment */ "Trash" = "휴지통"; -/* Accessibility hint for trash button to delete items from the user's media library */ -"Trash selected media" = "선택한 미디어 삭제"; - -/* Title of the trash page confirmation alert. */ -"Trash this page?" = "이 페이지를 휴지통으로 이동할까요?"; - /* Title of the trash confirmation alert. */ "Trash this post?" = "이 글을 휴지통에 버릴까요?"; @@ -8139,9 +7894,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Shown when a user logs in with Google but it subsequently fails to work as login to WordPress.com */ "Unable To Connect" = "연결할 수 없음"; -/* An error message. */ -"Unable to Connect" = "연결할 수 없음"; - /* Title for stories unknown error. */ "Unable to Create Stories Editor" = "스토리 편집기를 만들 수 없습니다."; @@ -8157,9 +7909,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* An error message shown when there is an issue creating new invite links. */ "Unable to create new invite links." = "새 초대 링크를 만들 수 없습니다."; -/* Text displayed in HUD if there was an error attempting to delete a group of media items. */ -"Unable to delete all media items." = "모든 미디어 항목을 삭제할 수 없습니다."; - /* Text displayed in HUD if there was an error attempting to delete a media item. */ "Unable to delete media item." = "미디어 항목을 삭제할 수 없습니다."; @@ -8223,12 +7972,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Message displayed when sharing a link to the downloadable backup fails. */ "Unable to share link" = "링크를 공유할 수 없음"; -/* Message that appears when a user tries to trash a page while their device is offline. */ -"Unable to trash pages while offline. Please try again later." = "오프라인 상태에서 글을 휴지통으로 이동할 수 없습니다. 나중에 다시 시도하세요."; - -/* Message that appears when a user tries to trash a post while their device is offline. */ -"Unable to trash posts while offline. Please try again later." = "오프라인 상태에서 글을 휴지통으로 이동할 수 없음 나중에 다시 시도하세요."; - /* Notice title when turning site notifications off fails. */ "Unable to turn off site notifications" = "사이트 알림을 끌 수 없음"; @@ -8301,8 +8044,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ Button title. Reverts the previous notification operation Revert an operation Revert enabling notification after successfully subcribing to the comments for the post. - The title of an 'undo' button. Tapping the button moves a trashed page out of the trash folder. - The title of an 'undo' button. Tapping the button moves a trashed post out of the trash folder. Undo action */ "Undo" = "실행 취소"; @@ -8345,9 +8086,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title for Unknown HTML Editor */ "Unknown HTML" = "알 수 없는 HTML"; -/* Label to use when creation date from media asset is not know. */ -"Unknown creation date" = "알 수 없는 생성일"; - /* No comment provided by engineer. */ "Unknown error" = "알려지지 않은 에러"; @@ -8513,6 +8251,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of a row displayed on the debug screen used to configure the sandbox store use in the App. */ "Use Sandbox Store" = "샌드박스 스토어 사용"; +/* The button's title text to use a security key. */ +"Use a security key" = "보안 키 사용"; + /* Option to enable the block editor for new posts */ "Use block editor" = "블록 편집기 사용"; @@ -8588,15 +8329,10 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of an alert informing users that the video they are trying to select is not allowed. */ "Video not uploaded" = "비디오 업로드 안 됨"; -/* Accessibility label for video thumbnails in the media collection view. The parameter is the creation date of the video. */ -"Video, %@" = "비디오, %@"; - /* Period Stats 'Videos' header */ "Videos" = "비디오"; /* Button title. Displays a summary / sharing screen for a specific post. - Label for a button that opens the page when tapped. - Label for the view post button. Tapping displays the post as it appears on the web. Opens the post epilogue screen to allow sharing / viewing of a post. Theme View action title Verb. The screen title shown when viewing a post inside the app. */ @@ -8711,6 +8447,10 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Message shown on screen while waiting for Google to finish its signup process. */ "Waiting for Google to complete…" = "Google에서 완료하기를 기다리는 중…"; +/* Text while the webauthn signature is being verified + Text while waiting for a security key challenge */ +"Waiting for security key" = "보안 키 기다리는 중"; + /* View title during the Google auth process. */ "Waiting..." = "기다리는 중..."; @@ -9082,6 +8822,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* An error message shown when a wpcom user provides the wrong password. */ "Whoops, something went wrong and we couldn't log you in. Please try again!" = "문제가 발생하여 로그인할 수 없습니다. 다시 시도하세요."; +/* Generic error on the 2FA screen */ +"Whoops, something went wrong. Please try again!" = "오류가 발생했습니다. 다시 시도해 보세요!"; + +/* Error when the uses chooses an invalid security key on the 2FA screen. */ +"Whoops, that security key does not seem valid. Please try again with another one" = "죄송합니다. 보안 키가 유효하지 않은 것 같습니다. 다른 것으로 시도해 보세요."; + /* Error message shown when an incorrect two factor code is provided. */ "Whoops, that's not a valid two-factor verification code. Double-check your code and try again!" = "2단계 인증코드가 맞지 않습니다. 코드를 다시 확인하시고 입력하여 주시기 바랍니다."; @@ -9109,9 +8855,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Siri Suggestion to open Support */ "WordPress Help" = "워드프레스 도움말"; -/* Name for the WordPress Media Library */ -"WordPress Media" = "워드프레스 미디어"; - /* Accessibility label for selecting an image or video from the user's WordPress media library on formatting toolbar. */ "WordPress Media Library" = "워드프레스 미디어 라이브러리"; @@ -9426,9 +9169,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Share extension error dialog text. */ "Your account does not have permission to upload media to this site. The Site Administrator can change these permissions." = "회원님의 계정은 이 사이트에 미디어를 업로드할 권한이 없습니다. 사이트 관리자가 이러한 권한을 변경할 수 있습니다."; -/* Explaining to the user why the app needs access to the device media library. */ -"Your app is not authorized to access media library due to active restrictions such as parental controls. Please check your parental control settings in this device." = "회원님의 앱은 자녀 보호와 같은 활성 제한 조건으로 인해 미디어 라이브러리에 액세스할 권한이 없습니다. 이 기기의 자녀 보호 설정을 확인해 주세요."; - /* Title for the Jetpack Backup Complete message. */ "Your backup is now available for download" = "백업을 이제 다운로드할 수 있습니다."; @@ -9447,9 +9187,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of the site address section in the Domains Dashboard. */ "Your free WordPress.com address is" = "무료 워드프레스닷컴 주소:"; -/* Error message when picked media cannot be imported into stories. */ -"Your media could not be exported. If the problem persists you can contact us via the Me > Help & Support screen." = "미디어를 내보낼 수 없음. If the problem persists you can contact us via the Me > 도움 및 지원 화면입니다."; - /* Details about recently acquired domain on domain credit redemption success screen */ "Your new domain %@ is being set up. It may take up to 30 minutes for your domain to start working." = "새 도메인(%@)을 설정하는 중입니다. 도메인이 작동하기 시작하려면 30분 정도 걸릴 수 있습니다."; @@ -9573,8 +9310,38 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* This is the string we display when prompting the user to review the WordPress app */ "appRatings.wordpress.prompt" = "워드프레스를 어떻게 생각하시나요?"; -/* Label displayed on audio media items. */ -"audio" = "오디오"; +/* Option to enable the optimization of images when uploading. */ +"appSettings.media.imageOptimizationRow" = "이미지 최적화"; + +/* Indicates an image will use high quality when uploaded. */ +"appSettings.media.imageQuality.high" = "높음"; + +/* Indicates an image will use low quality when uploaded. */ +"appSettings.media.imageQuality.low" = "낮음"; + +/* Indicates an image will use maximum quality when uploaded. */ +"appSettings.media.imageQuality.maximum" = "최대"; + +/* Indicates an image will use medium quality when uploaded. */ +"appSettings.media.imageQuality.medium" = "중간"; + +/* The quality of image used when uploading */ +"appSettings.media.imageQuality.title" = "품질"; + +/* Title for the image quality settings option. */ +"appSettings.media.imageQualityRow" = "이미지 품질"; + +/* Message of an alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.message" = "이미지 최적화에서는 더 빠른 업로드를 위해 이미지가 축소됩니다.\n\n이 옵션은 기본적으로 활성화되어 있지만, 언제든지 앱 설정에서 변경하실 수 있습니다."; + +/* Title of an alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.title" = "이미지를 계속 최적화하시겠어요?"; + +/* Title of button for turning off image optimization, displayed in the alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.turnOff" = "아니요. 끄기"; + +/* Title of button for leaving on image optimization, displayed in the alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.turnOn" = "예. 그대로 두기"; /* translators: displays audio file extension. e.g. MP3 audio file */ "audio file" = "오디오 파일"; @@ -9687,8 +9454,42 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Context menu button title */ "blogHeader.actionCopyURL" = "URL 복사"; -/* Context menu button title */ -"blogHeader.actionOpenInBrowser" = "브라우저에서 열기"; +/* Title for a button that, when tapped, shows more info about participating in Bloganuary. */ +"bloganuary.dashboard.card.button.learnMore" = "더 알아보기"; + +/* Short description for the Bloganuary event, shown right below the title. */ +"bloganuary.dashboard.card.description" = "새해에 블로깅 습관을 들이는 커뮤니티 챌린지인 Bloganuary에서 1월 한 달 동안 블로깅 프롬프트가 제공됩니다."; + +/* Title for the Bloganuary dashboard card while Bloganuary is running. */ +"bloganuary.dashboard.card.runningTitle" = "Bloganuary가 여기에 있습니다!"; + +/* Title for the Bloganuary dashboard card. */ +"bloganuary.dashboard.card.title" = "Bloganuary가 다가오고 있습니다!"; + +/* Title of a button that calls the user to enable the Blogging Prompts feature. */ +"bloganuary.learnMore.modal.button.promptsDisabled" = "블로깅 프롬프트 켜기"; + +/* Title of a button that will dismiss the Bloganuary modal when tapped. +Note that the word 'go' here should have a closer meaning to 'start' rather than 'move forward'. */ +"bloganuary.learnMore.modal.button.promptsEnabled" = "지금 시작하세요!"; + +/* The second line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.description.second" = "응답을 공개하세요."; + +/* The third line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.description.third" = "다른 블로거의 응답을 읽으며 영감을 얻고 새로운 연결 고리를 만드세요."; + +/* The first line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.descriptions.first" = "영감을 주는 새로운 프롬프트를 매일 받아보세요."; + +/* An additional piece of information shown in case the user has the Blogging Prompts feature disabled. */ +"bloganuary.learnMore.modal.footer.addition" = "Bloganuary에 참여하려면 블로깅 프롬프트를 활성화해야 합니다."; + +/* An informative excerpt shown in a subtler tone. */ +"bloganuary.learnMore.modal.footer.text" = "Bloganuary에서 일일 블로깅 프롬프트를 사용하여 1월 한 달 동안의 게시글을 보내드립니다."; + +/* The headline text of the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.headline" = "한 달 동안 진행되는 글쓰기 챌린지 참여"; /* Verb. Dismisses the blogging prompt notification. */ "bloggingPrompt.pushNotification.customActionDescription.dismiss" = "무시"; @@ -9717,6 +9518,12 @@ Example: Comment on Example: Reply to Pamela Nguyen */ "comment.header.subText.reply" = "%1$@에 회신"; +/* Error message informing a user about an invalid password. */ +"common.reEnterPasswordMessage" = "앱에 저장된 사용자 이름 또는 비밀번호가 만료되었을 수 있습니다. 설정에서 비밀번호를 재입력 후 다시 시도해 보세요."; + +/* An error message. */ +"common.unableToConnect" = "연결할 수 없음"; + /* Footnote for the privacy compliance popover. */ "compliance.analytics.popover.footnote" = "이러한 쿠키를 통해 당사에서는 사용자가 당사 웹사이트와 상호 작용하는 방식에 대한 정보를 수집하여 성능을 최적화할 수 있습니다."; @@ -9867,50 +9674,92 @@ Example: Reply to Pamela Nguyen */ /* Title for a menu action in the context menu on the Jetpack install card. */ "domain.dashboard.card.menu.hide" = "숨기기"; -/* Domain Purchase Completion footer */ -"domain.purchase.preview.footer" = "사용자 정의 도메인이 작동하기 시작하려면 30분 정도 걸릴 수 있습니다."; +/* Search domain - Title for the Suggested domains screen */ +"domain.management.addDomain.search.title" = "도메인 검색"; -/* Domain Purchase Completion description (only for FREE domains). */ -"domain.purchase.preview.free.description" = "이제부터 탐색할 수 있도록 도와드리겠습니다."; +/* Domain management buy domain card button title */ +"domain.management.buy.card.button.title" = "도메인 받기"; -/* Domain Purchase Completion description (only for PAID domains). */ -"domain.purchase.preview.paid.description" = "영수증을 이메일로 보내드렸습니다. 이제부터 모든 사용자를 위해 준비하도록 도와드리겠습니다."; +/* Domain management buy domain card subtitle */ +"domain.management.buy.card.subtitle" = "나중에 사이트를 추가하세요."; -/* Reflects that site is live when domain purchase feature flag is ON. */ -"domain.purchase.preview.title" = "사이트가 활성화되었습니다."; +/* Domain management buy domain card title */ +"domain.management.buy.card.title" = "도메인만 구매"; + +/* The expired label of the domain card in All Domains screen. */ +"domain.management.card.expired.label" = "만료됨"; + +/* The renews label of the domain card in All Domains screen. */ +"domain.management.card.renews.label" = "갱신"; -/* Status of a domain in `Action Required` state */ -"domain.status.action.required" = "조치 필요"; +/* The empty state button title in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.button.title" = "도메인 찾기"; -/* Status of a domain in `Active` state */ -"domain.status.active" = "활성"; +/* The empty state description in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.description" = "아래를 눌러서 알맞은 도메인을 찾아보세요."; -/* Status of a domain in `Complete Setup` state */ -"domain.status.complete.setup" = "설정 완료"; +/* The empty state title in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.title" = "도메인이 없습니다."; -/* Status of a domain in `Error` state */ -"domain.status.error" = "오류"; +/* The empty state description in All Domains screen when an error occurs */ +"domain.management.error.empty.state.description" = "도메인을 불러오는 중 오류가 발생했습니다. 문제가 지속되면 지원팀에 문의하세요."; -/* Status of a domain in `Expired` state */ -"domain.status.expired" = "만료됨"; +/* The empty state title in All Domains screen when an error occurs */ +"domain.management.error.empty.state.title" = "문제가 발생했습니다."; -/* Status of a domain in `Expiring Soon` state */ -"domain.status.expiring.soon" = "곧 만료됨"; +/* The empty state button title in All Domains screen when an error occurs */ +"domain.management.error.state.button.title" = "다시 시도"; -/* Status of a domain in `Failed` state */ -"domain.status.failed" = "실패"; +/* The empty state description in All Domains screen when the user is offline */ +"domain.management.offline.empty.state.description" = "네트워크 연결을 확인하고 다시 시도하세요."; -/* Status of a domain in `In Progress` state */ -"domain.status.in.progress" = "진행 중"; +/* The empty state title in All Domains screen when the user is offline */ +"domain.management.offline.empty.state.title" = "인터넷에 연결되지 않음"; -/* Status of a domain in `Renew` state */ -"domain.status.renew" = "갱신"; +/* Domain management choose site card button title */ +"domain.management.purchase.footer" = "*모든 유료 연간 요금제에는 1년 무료 도메인이 포함됩니다."; -/* Status of a domain in `Verify Email` state */ -"domain.status.verify.email" = "이메일 확인"; +/* Domain management purchase domain screen title */ +"domain.management.purchase.subtitle" = "나중에 사이트를 쉽게 추가할 수 있으니 걱정하지 마세요."; + +/* Domain management purchase domain screen title. */ +"domain.management.purchase.title" = "도메인 사용 방법 선택"; + +/* The search bar title in All Domains screen. */ +"domain.management.search-bar.title" = "도메인 검색"; + +/* The empty state description in All Domains screen when the are no domains matching the search criteria */ +"domain.management.search.empty.state.description" = "'%@' 검색과 일치하는 도메인 없음"; + +/* The empty state title in All Domains screen when the are no domains matching the search criteria */ +"domain.management.search.empty.state.title" = "일치하는 도메인 없음"; + +/* Domain management choose site card button title */ +"domain.management.site.card.button.title" = "사이트 선택"; + +/* Domain management choose site card subtitle */ +"domain.management.site.card.footer" = "최초 1년 무료 도메인*"; + +/* Domain management choose site card subtitle */ +"domain.management.site.card.subtitle" = "이미 시작한 사이트와 함께 사용하세요."; + +/* Domain management choose site card title */ +"domain.management.site.card.title" = "기존 워드프레스닷컴 사이트"; + +/* Domain Management Screen Title */ +"domain.management.title" = "모든 도메인"; + +/* Domain Purchase Completion footer */ +"domain.purchase.preview.footer" = "사용자 정의 도메인이 작동하기 시작하려면 30분 정도 걸릴 수 있습니다."; + +/* Domain Purchase Completion description (only for FREE domains). */ +"domain.purchase.preview.free.description" = "이제부터 탐색할 수 있도록 도와드리겠습니다."; + +/* Domain Purchase Completion description (only for PAID domains). */ +"domain.purchase.preview.paid.description" = "영수증을 이메일로 보내드렸습니다. 이제부터 모든 사용자를 위해 준비하도록 도와드리겠습니다."; -/* Status of a domain in `Verifying` state */ -"domain.status.verifying" = "확인 중"; +/* Reflects that site is live when domain purchase feature flag is ON. */ +"domain.purchase.preview.title" = "사이트가 활성화되었습니다."; /* The 'Best Alternative' label under the domain name in 'Choose a domain' screen */ "domain.suggestions.row.best-alternative" = "최선의 대안"; @@ -9933,12 +9782,24 @@ Example: Reply to Pamela Nguyen */ /* The text to display for paid domains in 'Site Creation > Choose a domain' screen */ "domain.suggestions.row.yearly" = "\/년"; +/* Title for the checkout screen. */ +"domains.checkout.title" = "체크아웃"; + /* Action shown in a bottom notice to dismiss it. */ "domains.failure.dismiss" = "해제"; /* Content show when the domain selection action fails. */ "domains.failure.title" = "죄송합니다. 추가하려는 도메인을 현재 잿팩에서 구매할 수 없습니다."; +/* Title for the screen where the user can choose how to use the domain they're end up purchasing. */ +"domains.purchase.choice.title" = "도메인 구매"; + +/* Back button title that navigates back to the search domains screen. */ +"domains.search.backButton.title" = "검색"; + +/* Title of screen where user chooses a site to connect to their selected domain */ +"domains.sitePicker.title" = "사이트 선택"; + /* No comment provided by engineer. */ "double-tap to change unit" = "두 번 눌러 단위 변경하기"; @@ -9956,6 +9817,12 @@ Example: Reply to Pamela Nguyen */ Site Address placeholder */ "example.com" = "example.com"; +/* Title for confirmation navigation bar button item */ +"externalMediaPicker.add" = "추가"; + +/* Bottom toolbar title in the selection mode */ +"externalMediaPicker.toolbarViewSelected" = "선택한 내용 보기(%@)"; + /* Title of screen the displays the details of an advertisement campaign. */ "feature.blaze.campaignDetails.title" = "캠페인 상세"; @@ -10055,9 +9922,6 @@ Example: Reply to Pamela Nguyen */ /* (placeholder) Help the user enter a URL into the field */ "http:\/\/my-site-address (URL)" = "http:\/\/my-site-address (URL)"; -/* Label displayed on image media items. */ -"image" = "이미지"; - /* Sentence to justify why the app is asking permission from the user to use their camera. */ "infoplist.NSCameraUsageDescription" = "글에 사용할 사진이나 비디오를 촬영"; @@ -10358,6 +10222,9 @@ Example: Reply to Pamela Nguyen */ /* Indicating that referrer was marked as spam */ "marked as spam" = "스팸으로 표시됨"; +/* Products header text in Me Screen. */ +"me.products.header" = "상품"; + /* Title of error prompt shown when a sync fails. */ "media.syncFailed" = "미디어 동기화 불가"; @@ -10370,18 +10237,24 @@ Example: Reply to Pamela Nguyen */ /* Message of an alert informing users that the video they are trying to select is not allowed. */ "mediaExporter.videoLimitExceededError" = "5분을 초과하는 비디오를 업로드하려면 유료 요금제가 필요합니다."; -/* Verb. User action to dismiss error alert when failing to load media item. */ -"mediaItemTable.errorAlert.dismissButton" = "무시"; - /* Accessibility hint for add button to add items to the user's media library */ "mediaLibrary.addButtonAccessibilityHint" = "새 미디어 추가"; /* Accessibility label for add button to add items to the user's media library */ "mediaLibrary.addButtonAccessibilityLabel" = "추가"; +/* Button name in the more menu */ +"mediaLibrary.aspectRatioGrid" = "화면 비율 그리드"; + +/* Context menu button */ +"mediaLibrary.buttonDelete" = "삭제"; + /* Media screen navigation bar button Select title */ "mediaLibrary.buttonSelect" = "선택"; +/* Context menu button */ +"mediaLibrary.buttonShare" = "공유"; + /* Verb. Button title. Tapping cancels an action. */ "mediaLibrary.deleteConfirmationCancel" = "취소"; @@ -10415,6 +10288,12 @@ Example: Reply to Pamela Nguyen */ /* Message displayed when no results are returned from a media library search. Should match Calypso. */ "mediaLibrary.searchResultsEmptyTitle" = "검색과 일치하는 미디어 없음"; +/* Text displayed in HUD if there was an error attempting to share a group of media items. */ +"mediaLibrary.sharingFailureMessage" = "선택한 아이템을 공유할 수 없습니다."; + +/* Button name in the more menu */ +"mediaLibrary.squareGrid" = "정사각형 그리드"; + /* Media screen navigation title */ "mediaLibrary.title" = "미디어"; @@ -10436,6 +10315,12 @@ Example: Reply to Pamela Nguyen */ /* The title of the button to dismiss the alert shown when the picked media cannot be imported into stories. */ "mediaPicker.failedMediaExportAlert.dismissButton" = "무시"; +/* Error message when picked media cannot be imported into stories. */ +"mediaPicker.failedMediaExportAlert.message" = "미디어를 내보낼 수 없습니다. 문제가 지속될 경우 내 계정 > 도움말 및 지원 화면을 통해 문의하실 수 있습니다."; + +/* Error title when picked media cannot be imported into stories. */ +"mediaPicker.failedMediaExportAlert.title" = "미디어 내보내기 실패"; + /* Message for alert when access to camera is not granted */ "mediaPicker.noCameraAccessMessage" = "새 미디어를 캡처하려면 카메라에 접근하는 권한이 이 앱에 필요합니다. 접근을 허용하려면 프라이버시 설정을 변경하세요."; @@ -10469,6 +10354,9 @@ Example: Reply to Pamela Nguyen */ /* The name of the action in the context menu */ "mediaPicker.takeVideo" = "비디오 촬영"; +/* Max image size in pixels (e.g. 300x300px) */ +"mediaSizeSlider.valueFormat" = "%1$d×%2$dpx"; + /* The description in the Delete WordPress screen */ "migration.deleteWordpress.description" = "아직 워드프레스 앱이 설치되어 있는 것 같습니다."; @@ -10481,9 +10369,6 @@ Example: Reply to Pamela Nguyen */ /* The title in the Delete WordPress screen */ "migration.deleteWordpress.title" = "더는 기기에 워드프레스 앱이 필요하지 않습니다."; -/* Primary button title in the migration done screen. */ -"migration.done.actions.primary.title" = "마침"; - /* Footer for the migration done screen. */ "migration.done.footer" = "데이터가 충돌하지 않도록 기기에서 워드프레스 앱을 제거하는 것이 좋습니다."; @@ -10493,6 +10378,9 @@ Example: Reply to Pamela Nguyen */ /* Primary description in the migration done screen. */ "migration.done.primaryDescription" = "모든 데이터와 설정을 이전하겠습니다. 모든 것이 그대로 다 남아 있습니다."; +/* Secondary description (second paragraph) in the migration done screen. */ +"migration.done.secondaryDescription" = "젯팩 앱에서 워드프레스 여정을 계속하실 때가 되었습니다!"; + /* Title of the migration done screen. */ "migration.done.title" = "젯팩으로 전환해 주셔서 감사합니다!"; @@ -10541,6 +10429,9 @@ Example: Reply to Pamela Nguyen */ /* The title in the migration welcome screen */ "migration.welcome.title" = "젯팩에 오신 것을 환영합니다!"; +/* Primary button title in the migration done screen. */ +"migrationDone.actions.primaryTitle" = "지금 시작"; + /* Description for the static screen displayed prompting users to switch the Jetpack app. */ "movedToJetpack.description" = "젯팩 앱에서는 워드프레스 앱의 모든 기능을 보유하며, 이제 통계, 리더, 알림 등에 독점적으로 접근할 수 있습니다."; @@ -10616,6 +10507,21 @@ Example: Reply to Pamela Nguyen */ /* Message title for when a user has no sites. */ "mySite.noSites.title" = "사이트가 없습니다"; +/* Menu title for the add site option */ +"mySite.siteActions.addSite" = "사이트 추가"; + +/* Button that reveals more site actions */ +"mySite.siteActions.button" = "사이트 조치"; + +/* Accessibility hint for button used to show more site actions */ +"mySite.siteActions.hint" = "눌러서 추가 사이트 조치 표시"; + +/* Menu title for the personalize home option */ +"mySite.siteActions.personalizeHome" = "홈 개인 설정"; + +/* Menu title for the change site title option */ +"mySite.siteActions.siteTitle" = "사이트 제목 변경"; + /* Dismiss button title. */ "noResultsViewController.dismissButton" = "해제"; @@ -10631,14 +10537,20 @@ Example: Reply to Pamela Nguyen */ /* This is one of the buttons we display when prompting the user for a review */ "notifications.appRatings.sendFeedback.yes.buttonTitle" = "피드백 보내기"; -/* Word separating the current index from the total amount. I.e.: 7 of 9 */ -"of" = "\/"; +/* Badge for page cells */ +"pageList.badgeHomepage" = "홈페이지"; + +/* Badge for page cells */ +"pageList.badgeLocalChanges" = "로컬 변경"; -/* Label displayed on media items that are not video, image, or audio. */ -"other" = "기타"; +/* Badge for page cells */ +"pageList.badgePendingReview" = "검토 보류 중"; -/* Promote the page with Blaze. */ -"pages.blaze.actionTitle" = "Blaze로 홍보"; +/* Badge for page cells */ +"pageList.badgePosts" = "글 페이지"; + +/* Badge for page cells */ +"pageList.badgePrivate" = "비공개"; /* Subtitle of the theme template homepage cell */ "pages.template.subtitle" = "홈페이지에서 테마 템플릿이 사용되고 있으며 웹 편집기에서 홈페이지가 열립니다."; @@ -10646,6 +10558,36 @@ Example: Reply to Pamela Nguyen */ /* Title of the theme template homepage cell */ "pages.template.title" = "홈페이지"; +/* Message informing the user that their static homepage page was set successfully */ +"pages.updatePage.successTitle" = "페이지 업데이트됨"; + +/* Delete option in the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.actionTitle" = "영구 삭제"; + +/* Message of the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.alertMessage" = "이 페이지를 영구적으로 삭제하시겠어요?"; + +/* Title of the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.alertTitle" = "영구적으로 삭제할까요?"; + +/* Menu option for filtering posts by everyone */ +"pagesList.pagesByEveryone" = "모두가 작성한 페이지"; + +/* Menu option for filtering posts by me */ +"pagesList.pagesByMe" = "내가 작성한 페이지"; + +/* Trash option in the trash page confirmation alert. */ +"pagesList.trash.actionTitle" = "휴지통으로 이동"; + +/* Message of the trash page confirmation alert. */ +"pagesList.trash.alertMessage" = "이 페이지를 휴지통으로 이동하시겠어요?"; + +/* Title of the trash page confirmation alert. */ +"pagesList.trash.alertTitle" = "이 페이지를 휴지통으로 이동할까요?"; + +/* Cancels an Action */ +"pagesList.trash.cancel" = "취소"; + /* No comment provided by engineer. */ "password" = "비밀번호"; @@ -10685,6 +10627,51 @@ Example: Reply to Pamela Nguyen */ /* Register Domain - Domain contact information field Phone */ "phone number" = "전화번호"; +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.createdTimeAgo" = "%@ 생성됨"; + +/* Status mesasge for post cells */ +"post.deletingPostPermanentlyStatusMessage" = "글 삭제 중..."; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.editedTimeAgo" = "%@ 편집됨"; + +/* Status mesasge for post cells */ +"post.movingToTrashStatusMessage" = "휴지통으로 글 이동 중..."; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.publishedTimeAgo" = "%@ 공개됨"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.scheduledForDate" = "%@ 예약됨"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.trashedTimeAgo" = "%@ 삭제됨"; + +/* Accessibility label for the post author in the post list. The parameter is the author name. For example, \"By Elsa.\" */ +"postList.a11y.authorChunkFormat" = "%@ 작성."; + +/* Accessibility label for a post's excerpt in the post list. The parameter is the post excerpt. For example, \"Excerpt. This is the first paragraph.\" */ +"postList.a11y.exerptChunkFormat" = "요약입니다. %@."; + +/* Accessibility label for a sticky post in the post list. */ +"postList.a11y.sticky" = "붙박이입니다."; + +/* Accessibility label for a post in the post list. The first placeholder is the post title. The second placeholder is the date. */ +"postList.a11y.titleAndDateChunkFormat" = "%1$@, %2$@."; + +/* Title for the 'Trash' post list row swipe action */ +"postList.swipeActionDelete" = "휴지통"; + +/* Title for the 'Delete' post list row swipe action */ +"postList.swipeActionDeletePermanently" = "삭제"; + +/* Title for the 'Share' post list row swipe action */ +"postList.swipeActionShare" = "공유"; + +/* Title for the 'View' post list row swipe action */ +"postList.swipeActionView" = "보기"; + /* User action to dismiss featured media options. */ "postSettings.featuredImageUploadActionSheet.dismiss" = "해제"; @@ -10703,9 +10690,84 @@ Example: Reply to Pamela Nguyen */ /* Button in Post Settings */ "postSettings.setFeaturedImageButton" = "특성 이미지 설정"; +/* Error message on post/page settings screen */ +"postSettings.updateFailedMessage" = "글 설정 업데이트 실패"; + /* Promote the post with Blaze. */ "posts.blaze.actionTitle" = "Blaze로 홍보"; +/* Label for the Post List option that cancels automatic uploading of a post. */ +"posts.cancelUpload.actionTitle" = "업로드 취소"; + +/* Label for post comments option. Tapping displays comments for a post. */ +"posts.comments.actionTitle" = "댓글"; + +/* Label for the delete post option. Tapping permanently deletes a post. */ +"posts.delete.actionTitle" = "영구 삭제"; + +/* Label for an option that moves a post to the draft folder */ +"posts.draft.actionTitle" = "임시글로 이동"; + +/* Label for post duplicate option. Tapping creates a copy of the post. */ +"posts.duplicate.actionTitle" = "복제"; + +/* Opens a submenu for page attributes. */ +"posts.pageAttributes.actionTitle" = "페이지 속성"; + +/* Label for the preview post button. Tapping displays the post as it appears on the web. */ +"posts.preview.actionTitle" = "미리보기"; + +/* Label for an option that moves a publishes a post immediately */ +"posts.publish.actionTitle" = "지금 공개"; + +/* Retry uploading the post. */ +"posts.retry.actionTitle" = "다시 시도"; + +/* Set the selected page as the homepage. */ +"posts.setHomepage.actionTitle" = "홈페이지로 설정"; + +/* Set the parent page for the selected page. */ +"posts.setParent.actionTitle" = "상위 항목 설정"; + +/* Set the selected page as a posts page. */ +"posts.setPostsPage.actionTitle" = "글 페이지로 설정"; + +/* Set the selected page as a regular page. */ +"posts.setRegularPage.actionTitle" = "일반 페이지로 설정"; + +/* Label for post settings option. Tapping displays settings for a post. */ +"posts.settings.actionTitle" = "설정"; + +/* Share the post. */ +"posts.share.actionTitle" = "공유"; + +/* Label for post stats option. Tapping displays statistics for a post. */ +"posts.stats.actionTitle" = "통계"; + +/* Label for a option that moves a post to the trash folder */ +"posts.trash.actionTitle" = "휴지통으로 이동"; + +/* Label for the view post button. Tapping displays the post as it appears on the web. */ +"posts.view.actionTitle" = "보기"; + +/* A short message explaining that a page was deleted permanently. */ +"postsList.deletePage.message" = "페이지 영구 삭제됨"; + +/* A short message explaining that a post was deleted permanently. */ +"postsList.deletePost.message" = "글 영구 삭제됨"; + +/* A short message explaining that a page was moved to the trash bin. */ +"postsList.movePageToTrash.message" = "휴지통으로 페이지 이동됨"; + +/* A short message explaining that a post was moved to the trash bin. */ +"postsList.movePostToTrash.message" = "휴지통으로 글 이동됨"; + +/* Menu option for filtering posts by everyone */ +"postsList.postsByEveryone" = "모두가 작성한 글"; + +/* Menu option for filtering posts by me */ +"postsList.postsByMe" = "내가 작성한 글"; + /* Title for the button to subscribe to Jetpack Social on the remaining shares view */ "postsettings.social.remainingshares.subscribe" = "더 많은 정보를 공유하려면 지금 구독하세요"; @@ -10850,13 +10912,15 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Text for the 'Like' button on the reader post card cell. */ "reader.post.button.like" = "좋아요"; -/* Accessibility hint for the like button on the reader post card cell - Accessibility hint for the liked button on the reader post card cell */ +/* Accessibility hint for the like button on the reader post card cell */ "reader.post.button.like.accessibility.hint" = "게시글을 좋아합니다."; /* Text for the 'Liked' button on the reader post card cell. */ "reader.post.button.liked" = "좋아함"; +/* Accessibility hint for the liked button on the reader post card cell */ +"reader.post.button.liked.accessibility.hint" = "글에 대한 좋아요를 취소합니다."; + /* Accessibility hint for the site header on the reader post card cell */ "reader.post.button.menu.accessibility.hint" = "추가 작업이 포함된 메뉴가 열립니다."; @@ -10920,6 +10984,12 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Title for the tooltip anchor. */ "readerDetail.tooltipAnchorTitle.accessibilityLabel" = "신규"; +/* The button title for the transfer footer view in Register Domain screen */ +"register.domain.transfer.button.title" = "도메인 이전"; + +/* The title for the transfer footer view in Register Domain screen */ +"register.domain.transfer.title" = "이미 소유한 도메인을 이전하시겠습니까?"; + /* Information of what related post are and how they are presented */ "relatedPostsSettings.optionsFooter" = "관련 게시물은 글 아래에 사이트의 관련 콘텐츠를 표시합니다."; @@ -11019,6 +11089,18 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Accessibility hint for actions when displaying media items. */ "siteMedia.cellAccessibilityHint" = "미디어를 선택합니다."; +/* Accessibility hint for media item preview for user's viewing an item in their media library */ +"siteMediaItem.contentViewAccessibilityHint" = "미디어를 눌러 전체 화면에서 보기"; + +/* Accessibility label for media item preview for user's viewing an item in their media library */ +"siteMediaItem.contentViewAccessibilityLabel" = "미디어 미리보기"; + +/* Button selection media in media picker */ +"siteMediaPicker.deselect" = "선택 해제"; + +/* Button selection media in media picker */ +"siteMediaPicker.select" = "선택"; + /* Media screen navigation title */ "siteMediaPicker.title" = "미디어"; @@ -11026,7 +11108,7 @@ Example: given a notice format "Following %@" and empty site name, this will be "siteSettings.privacy.title" = "개인정보"; /* Hint for users when hidden privacy setting is set */ -"siteVisibility.hidden.hint" = "사이트가 모든 사람에게 공개되지만 검색 엔진에서 사이트가 검색되지 않도록 지정하세요."; +"siteVisibility.hidden.hint" = "사이트가 완성될 때까지 “Coming Soon” 알림 뒤로 사이트를 숨겨둡니다."; /* Text for privacy settings: Hidden */ "siteVisibility.hidden.title" = "숨김"; @@ -11187,6 +11269,9 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Dismiss the AlertView */ "stockPhotos.strings.dismiss" = "무시"; +/* Title for placeholder in Free Photos */ +"stockPhotos.title" = "미디어 라이브러리에 추가할 무료 사진을 검색하여 찾으세요!"; + /* Section title for prominent suggestions */ "suggestions.section.prominent" = "대화"; @@ -11334,6 +11419,9 @@ Example: given a notice format "Following %@" and empty site name, this will be /* View title for Help & Support page. */ "support.title" = "도움말"; +/* Title for placeholder in Tenor picker */ +"tenor.welcomeMessage" = "미디어 라이브러리에 추가할 GIF를 검색하여 찾으세요!"; + /* Header of delete screen section listing things that will be deleted. */ "these items will be deleted:" = "다음 항목이 삭제됩니다."; @@ -11349,9 +11437,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Displayed in the confirmation alert when marking unread notifications as read. */ "unread" = "읽지 않음"; -/* Label displayed on video media items. */ -"video" = "비디오"; - /* Portion of a message for Jetpack users that have multisite WP installation, thus Restore is not available. This part is a link, colored with a different color. */ "visit our documentation page" = "설명서 페이지 방문"; diff --git a/WordPress/Resources/nb.lproj/Localizable.strings b/WordPress/Resources/nb.lproj/Localizable.strings index cd0130b6c74e..5d63214c08dd 100644 --- a/WordPress/Resources/nb.lproj/Localizable.strings +++ b/WordPress/Resources/nb.lproj/Localizable.strings @@ -95,9 +95,6 @@ /* Difference label for Period Overview stat, indicating change from previous period. Ex: +99.9K (5%) */ "%@%@ (%@%%)" = "%1$@%2$@ (%3$@%%)"; -/* Accessibility label for a post in the post list. The parameters are the title, and date respectively. For example, \"Let it Go, 1 hour ago.\" */ -"%@, %@." = "%1$@, %2$@."; - /* Accessibility value for a Stats' Posting Activity Month if the user selected a day with posts. The first parameter is day (e.g. November 2019). The second parameter is the number of posts. */ "%@. %d posts." = "%1$@. %2$d innlegg."; @@ -150,9 +147,6 @@ /* Age between dates over one year. */ "%d years" = "%d år"; -/* Max image size in pixels (e.g. 300x300px) */ -"%dx%dpx" = "%1$dx%2$dpx"; - /* One menu area available in the theme */ "%i menu area in this theme" = "%i menyområde i dette temaet"; @@ -325,13 +319,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Activity Logs" = "Aktivitetslogger"; -/* Accessibility label for add button to add items to the user's media library - Remove asset from media picker list */ -"Add" = "Legg til"; - -/* Action for Media Picker to indicate selection of media. The argument in the string represents the number of elements (as numeric digits) selected */ -"Add %@" = "Legg til %@"; - /* No comment provided by engineer. */ "Add Block After" = "Legg til blokk etter"; @@ -445,9 +432,6 @@ translators: Block name. %s: The localized block name */ /* Option to select the Airmail app when logging in with magic links */ "Airmail" = "Airmail"; -/* Description of albums in the photo libraries */ -"Albums" = "Albumer"; - /* Image alignment option title. Title of the screen for choosing an image's alignment. */ "Alignment" = "Justering"; @@ -617,9 +601,6 @@ translators: Block name. %s: The localized block name */ /* Message prompting the user to confirm that they want to disconnect Jetpack from the site. */ "Are you sure you want to disconnect Jetpack from the site?" = "Er du sikker på at du vil koble fra Jetpack fra denne siden?"; -/* Message prompting the user to confirm that they want to permanently delete a group of media items. */ -"Are you sure you want to permanently delete these items?" = "Er du sikker på at du vil slette disse objektene permanent?"; - /* Message prompting the user to confirm that they want to permanently delete a media item. Should match Calypso. */ "Are you sure you want to permanently delete this item?" = "Er du sikker på at du vil slette dette objektet permanent?"; @@ -667,9 +648,6 @@ translators: Block name. %s: The localized block name */ /* Alert option to embed a doc link into a blog post. */ "Attach File as Link" = "Legg ved fil som lenke"; -/* Accessibility label for audio items in the media collection view. The parameter is the creation date of the audio. */ -"Audio, %@" = "Lyd, %@"; - /* No comment provided by engineer. */ "Authenticating" = "Autentiserer"; @@ -867,9 +845,6 @@ translators: Block name. %s: The localized block name */ /* Label for the post author in the post detail. */ "By " = "Av"; -/* Accessibility label for the post author in the post list. The parameter is the author name. For example, \"By Elsa.\" */ -"By %@." = "Av %@."; - /* Terms of Service link displayed when a user is registering domain. Text inside tags will be highlighted. */ "By registering this domain you agree to our Terms and Conditions<\/a>." = "Ved å registrere dette domenet godtar du våre regler og vilkår<\/a>."; @@ -886,8 +861,7 @@ translators: Block name. %s: The localized block name */ /* Label for size of media while it's being calculated. */ "Calculating..." = "Kalkulerer..."; -/* Accessibility label for taking an image or video with the camera on formatting toolbar. - Accessibility label for the camera tile in the collection view */ +/* Accessibility label for taking an image or video with the camera on formatting toolbar. */ "Camera" = "Kamera"; /* Title of an alert letting the user know */ @@ -935,10 +909,7 @@ translators: Block name. %s: The localized block name */ Cancels an Action Cancels an alert. Cancels the mark all as read action. - Dismiss the post action sheet Editing GIF alert cancel action button. - Label for a cancel button - Label for the auto-upload cancelation button in the post list. Tapping will prevent the app from auto-uploading the post. Menus cancel button for deleting a menu. Menus cancel button within text bar while editing items. Menus: Cancel button title for canceling an edited menu item. @@ -955,10 +926,6 @@ translators: Block name. %s: The localized block name */ Verb. Title for Jetpack Restore cancel button. */ "Cancel" = "Avbryt"; -/* Label for the Post List option that cancels automatic uploading of a post. - Media Library option to cancel an in-progress or failed upload. */ -"Cancel Upload" = "Avbryt opplastning"; - /* Menus alert button title to cancel discarding changes and not createa a new menu. Menus alert button title to cancel discarding changes and not select a new menu Menus alert button title to cancel discarding changes and not select a new menu location */ @@ -1072,9 +1039,6 @@ translators: Block name. %s: The localized block name */ Title of a Quick Start Tour */ "Choose a theme" = "Velg et tema"; -/* Menu option for selecting media from the device's photo library. */ -"Choose from My Device" = "Velg fra min enhet"; - /* No comment provided by engineer. */ "Choose from device" = "Velg fra enheten"; @@ -1211,9 +1175,6 @@ translators: Block name. %s: The localized block name */ /* Setting: WordPress.com Community */ "Community" = "Samfunn"; -/* Accessibility indication that the current Post List style is currently Compact. */ -"Compact" = "Kompakt"; - /* The action is completed */ "Completed" = "Fullført"; @@ -1333,10 +1294,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Copied block" = "Kopierte blokk"; -/* Copy the post url and paste anywhere in phone - Label for page copy link. Tapping copy the url of page */ -"Copy Link" = "Kopier lenke"; - /* Copy link to oomment button title */ "Copy Link to Comment" = "Kopier lenke til kommentar"; @@ -1400,9 +1357,6 @@ translators: Block name. %s: The localized block name */ /* The title for an alert that says to the user that the featured image he selected couldn't be uploaded. */ "Couldn't upload the featured image" = "Kunne ikke laste opp fremhevet bilde"; -/* Message to show while media data source is finding the number of items available. */ -"Counting media items..." = "Teller medieobjekter..."; - /* Period Stats 'Countries' header */ "Countries" = "Land"; @@ -1572,7 +1526,6 @@ translators: Block name. %s: The localized block name */ /* Delete Delete button title for the warning shown to the user when he refuses to re-login when the authToken is missing. Title for button that permanently deletes a media item (photo / video) - Title for button that permanently deletes one or more media items (photos / videos) Title of a delete button Title of the trash confirmation alert. */ "Delete" = "Slett"; @@ -1580,15 +1533,11 @@ translators: Block name. %s: The localized block name */ /* Menus confirmation button for deleting a menu. */ "Delete Menu" = "Slett meny"; -/* Delete option in the confirmation alert when deleting a page from the trash. - Delete option in the confirmation alert when deleting a post from the trash. - Label for a button permanently deletes a page. - Label for the delete post option. Tapping permanently deletes a post. +/* Delete option in the confirmation alert when deleting a post from the trash. Title for button on the comment details page that deletes the comment when tapped. */ "Delete Permanently" = "Slett permanent"; -/* Title of the confirmation alert when deleting a page from the trash. - Title of the confirmation alert when deleting a post from the trash. */ +/* Title of the confirmation alert when deleting a post from the trash. */ "Delete Permanently?" = "Slette permanent?"; /* Button label for deleting the current site @@ -1692,7 +1641,6 @@ translators: Block name. %s: The localized block name */ /* Accessibility label for button to dismiss a bottom sheet Accessibility label for the transparent space above the login dialog which acts as a button to dismiss the dialog. Accessibility label for the transparent space above the signup dialog which acts as a button to dismiss the dialog. - Action to show on alert when view asset fails. Dismiss a view. Verb */ "Dismiss" = "Ignorer"; @@ -1704,9 +1652,6 @@ translators: Block name. %s: The localized block name */ User's Display Name */ "Display Name" = "Visningsnavn"; -/* Accessibility label for other media items in the media collection view. The parameter is the filename file. */ -"Document: %@" = "Dokument: %@"; - /* Register Domain - Domain contact information section header title */ "Domain contact information" = "Kontaktinformasjon for domene"; @@ -1791,20 +1736,15 @@ translators: Block name. %s: The localized block name */ /* Name for the status of a draft post. */ "Draft" = "Kladd"; -/* Title of the drafts filter. This filter shows a list of draft posts. - Title of the drafts header in search list. */ +/* Title of the drafts filter. This filter shows a list of draft posts. */ "Drafts" = "Kladder"; /* No comment provided by engineer. */ "Duplicate block" = "Dupliser blokk"; -/* Accessibility label for edit button to enable multi selection mode in the user's media library - Edit the post. - Editing GIF alert default action button. +/* Editing GIF alert default action button. Edits a Comment Edits the comment - Label for a button that opens the Edit Page view controller - Label for the edit post button. Tapping displays the editor. User action to edit media details. Verb, edit a comment */ "Edit" = "Rediger"; @@ -1812,9 +1752,6 @@ translators: Block name. %s: The localized block name */ /* Title for the edit more button section */ "Edit \"More\" button" = "Rediger \"Mer\"-knapp"; -/* Button that displays the media editor to the user */ -"Edit %@" = "Rediger %@"; - /* Blocklist Keyword Edition Title */ "Edit Blocklist Word" = "Rediger blokkeringslisteord"; @@ -2051,26 +1988,19 @@ translators: Block name. %s: The localized block name */ /* Short title telling the user they will receive a blogging reminder every day of the week. */ "Every day" = "Hver dag"; -/* Label for the post author filter. This filter shows posts for all users on the blog. */ -"Everyone" = "Alle"; - /* Label for the excerpt field. Should be the same as WP core. */ "Excerpt" = "Utdrag"; /* No comment provided by engineer. */ "Excerpt length (words)" = "Lengde på utdrag (ord)"; -/* Accessibility label for a post's excerpt in the post list. The parameter is the post excerpt. For example, \"Excerpt. This is the first paragraph.\" */ -"Excerpt. %@." = "Utdrag. %@"; - /* Should be the same as the text displayed if the user clicks the (i) in Calypso. */ "Excerpts are optional hand-crafted summaries of your content." = "Utdrag er valgfrie, håndskrevne oppsummeringer av innholdet ditt."; /* Accessibility Label for the exit full screen button on the full screen comment reply mode */ "Exit Full Screen" = "Gå ut av fullskjerm"; -/* Accessibility indication that the current Post List style is currently Expanded. - Screen reader text to represent the expanded state of a UI control */ +/* Screen reader text to represent the expanded state of a UI control */ "Expanded" = "Utvidet"; /* Accessibility hint */ @@ -2434,8 +2364,7 @@ translators: Block name. %s: The localized block name */ "Hold for Moderation" = "Hold til moderering"; /* Title for setting which shows the current page assigned as a site's homepage - Title for the homepage section in site settings screen - Title of the Homepage Badge */ + Title for the homepage section in site settings screen */ "Homepage" = "Hjemmeside"; /* Label for Homepage Settings site settings section @@ -2487,9 +2416,6 @@ translators: Block name. %s: The localized block name */ /* Hint for image title on image settings. */ "Image title" = "Bildetittel"; -/* Accessibility label for image thumbnails in the media collection view. The parameter is the creation date of the image. */ -"Image, %@" = "Bilde, %@"; - /* Undated post time label */ "Immediately" = "Umiddelbart"; @@ -2780,9 +2706,6 @@ translators: Block name. %s: The localized block name */ Settings: Comments Approval settings */ "Links in comments" = "Lenker i kommentarer"; -/* The accessibility label for the list style button in the Post List. */ -"List style" = "Listestil"; - /* Title of the screen that load selected the revisions. */ "Load" = "Last"; @@ -2792,18 +2715,12 @@ translators: Block name. %s: The localized block name */ /* Text displayed while loading the activity feed for a site */ "Loading Activities..." = "Laster inn aktiviteter..."; -/* Phrase to show when the user has searched for GIFs and they are being loaded. */ -"Loading GIFs..." = "Laster GIFer..."; - /* Menus label text displayed while menus are loading */ "Loading Menus..." = "Laster inn menyer..."; /* Text displayed while loading site People. */ "Loading People..." = "Laster inn personer..."; -/* Phrase to show when the user has searched for images and they are being loaded. */ -"Loading Photos..." = "Laster bilder..."; - /* Text displayed while loading plans details */ "Loading Plan..." = "Laster inn abonnement..."; @@ -2849,8 +2766,7 @@ translators: Block name. %s: The localized block name */ /* Status for Media object that is only exists locally. */ "Local" = "Lokal"; -/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. - Title of the Local Changes Badge */ +/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. */ "Local changes" = "Lokale endringer"; /* Title for alert when a generic error happened when trying to find the location of the device */ @@ -2965,7 +2881,6 @@ translators: Block name. %s: The localized block name */ "Max Video Upload Size" = "Maksstørrelse på videoopplasting"; /* Accessibility label for the Me button in My Site. - Label for the post author filter. This filter shows posts only authored by the current user. Me page title Noun. Title. Links to the Me screen. The accessibility value of the me tab. @@ -2973,9 +2888,7 @@ translators: Block name. %s: The localized block name */ "Me" = "Meg"; /* Noun. Title. Links to the blog's Media library. - Tab bar title for the Media tab in Media Picker The menu item to select during a guided tour. - Title for Media Library section of the app. Title for the media section in site settings screen Title label for the media settings section in the app settings */ "Media" = "Media"; @@ -2983,13 +2896,6 @@ translators: Block name. %s: The localized block name */ /* Label for size of media cache in the app. */ "Media Cache Size" = "Størrelse på mediemellomlager"; -/* Title for alert when access to media capture is not granted */ -"Media Capture" = "Medieopptak"; - -/* Title for alert when a generic error happened when loading media - Title for alert when access to the media library is not granted by the user */ -"Media Library" = "Mediebibliotek"; - /* Title for action sheet with media options. */ "Media Options" = "Medievalg"; @@ -3009,9 +2915,6 @@ translators: Block name. %s: The localized block name */ /* translators: %s: block title e.g: \"Paragraph\". */ "Media options" = "Media-innstillinger"; -/* Alert title when there is issues loading an asset to preview. */ -"Media preview failed." = "Medieforhåndsvisning feilet."; - /* Alert displayed to the user when multiple media items have uploaded successfully. */ "Media uploaded (%ld files)" = "Mediefiler lastet opp (%ld filer)"; @@ -3040,9 +2943,6 @@ translators: Block name. %s: The localized block name */ Label for the share message field on the post settings. */ "Message" = "Melding"; -/* Title of section containing image / video metadata such as size and file type */ -"Metadata" = "Metadata"; - /* Option to select the Microsft Outlook app when logging in with magic links */ "Microsoft Outlook" = "Microsoft Outlook"; @@ -3059,13 +2959,11 @@ translators: Block name. %s: The localized block name */ "Months and Years" = "Måneder og år"; /* Accessibility label for more button in dashboard quick start card. - Accessibility label for the More button in Page List. Accessibility label for the More button in Post List (compact view). Accessibility label for the More button on formatting toolbar. Accessibility label for the More button on Reader Cell Accessibility label for the More button on Reader's post details - Action button to display more available options - Label for the more post button. Tapping displays an action sheet with post options. */ + Action button to display more available options */ "More" = "Mer"; /* Action button to display more available options @@ -3111,15 +3009,8 @@ translators: Block name. %s: The localized block name */ /* Option to move Insight down in the view. */ "Move down" = "Flytt ned"; -/* Label for a button that moves a page to the draft folder - Label for an option that moves a post to the draft folder */ -"Move to Draft" = "Flytt til Kladd"; - -/* Label for a button that moves a page to the trash folder - Label for a option that moves a post to the trash folder - Title for button on the comment details page that moves the comment to trash when tapped. +/* Title for button on the comment details page that moves the comment to trash when tapped. Trash option in the trash confirmation alert. - Trash option in the trash page confirmation alert. Trashes the comment */ "Move to Trash" = "Flytt til papirkurv"; @@ -3142,7 +3033,8 @@ translators: Block name. %s: The localized block name */ Title of My Site tab */ "My Site" = "Mitt nettsted"; -/* Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ +/* Title for site picker screen. + Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ "My Sites" = "Mine nettsteder"; /* Siri Suggestion to open My Sites */ @@ -3324,9 +3216,7 @@ translators: Block name. %s: The localized block name */ /* Displayed in the Notifications Tab as a title, when the Likes Filter shows no notifications */ "No likes yet" = "Ingen liker enda"; -/* Message displayed when no results are returned from a media library search. Should match Calypso. - Phrase to show when the user search for images but there are no result to show. - Phrase to show when the user searches for GIFs but there are no result to show. */ +/* Message displayed when no results are returned from a media library search. Should match Calypso. */ "No media matching your search" = "Ingen medier matcher ditt søk"; /* Menus text shown when no menus were available for loading the Menus editor. */ @@ -3335,8 +3225,7 @@ translators: Block name. %s: The localized block name */ /* Displayed in the Notifications Tab as a title, when there are no notifications */ "No notifications yet" = "Ingen varslinger ennå"; -/* Displayed when the user is searching the pages list and there are no matching pages - Text displayed when there's no matching with the text search */ +/* Text displayed when there's no matching with the text search */ "No pages matching your search" = "Ingen sider passer ditt søk"; /* Text displayed when search for plugins returns no results */ @@ -3357,9 +3246,6 @@ translators: Block name. %s: The localized block name */ /* Message shown whent the reader finds no posts for the chosen tag */ "No posts have been made recently with this tag." = "Ingen nylige innlegg har dette stikkordet."; -/* Displayed when the user is searching the posts list and there are no matching posts */ -"No posts matching your search" = "Ingen innlegg passer ditt søk"; - /* Accessibility value for a Stats' Posting Activity Month if there are no posts. */ "No posts." = "Ingen innlegg."; @@ -3421,9 +3307,6 @@ translators: Block name. %s: The localized block name */ /* A message title */ "Nothing liked yet" = "Intet er likt ennå"; -/* Default message for empty media picker */ -"Nothing to show" = "Ingenting å vise"; - /* Notifications Details Accessibility Identifier */ "Notification Details Table" = "Tabell for varseldetaljer"; @@ -3468,7 +3351,6 @@ translators: Block name. %s: The localized block name */ Alert dismissal title Button title. Acknowledges a prompt. Button title. An acknowledgement of the message displayed in a prompt. - Confirmation of action Default action Dismisses the alert Marks all notifications as read. @@ -3540,9 +3422,6 @@ translators: Block name. %s: The localized block name */ Title of a button that opens the apps settings in the system Settings.app */ "Open Settings" = "Åpne innstillinger"; -/* Editor button to swich the media picker from quick mode to full picker */ -"Open full media picker" = "Åpne full medievelger"; - /* No comment provided by engineer. */ "Open in Safari" = "Åpne i Safari"; @@ -3619,15 +3498,6 @@ translators: Block name. %s: The localized block name */ Title shown when selecting a post type of Page from the Share Extension. */ "Page" = "Side"; -/* Prompts the user that a restored page was moved to the drafts list. */ -"Page Restored to Drafts" = "Side tilbakestilt til Kladd"; - -/* Prompts the user that a restored page was moved to the published list. */ -"Page Restored to Published" = "Side tilbakestilt til Publisert"; - -/* Prompts the user that a restored page was moved to the scheduled list. */ -"Page Restored to Scheduled" = "Side tilbakestilt til Planlagt"; - /* Name of the button to open the page settings The title of the Page Settings screen. */ "Page Settings" = "Sideinnstillinger"; @@ -3644,9 +3514,6 @@ translators: Block name. %s: The localized block name */ /* Title of notification displayed when a page has failed to upload. */ "Page failed to upload" = "Side kunne ikke lastes opp"; -/* A short message explaining that a page was moved to the trash bin. */ -"Page moved to trash." = "Side flyttet til papirkurven."; - /* Title of notification displayed when a page has been successfully saved as a draft. */ "Page pending review" = "Side venter på gjennomgang"; @@ -3700,8 +3567,7 @@ translators: Block name. %s: The localized block name */ Title of pending Comments filter. */ "Pending" = "Venter"; -/* Name for the status of a post pending review. - Title of the Pending Review Badge */ +/* Name for the status of a post pending review. */ "Pending review" = "Venter på gjennomgang"; /* Noun. Title of the people management feature. @@ -3721,12 +3587,6 @@ translators: Block name. %s: The localized block name */ /* Accessibility label for selecting an image or video from the device's photo library on formatting toolbar. */ "Photo Library" = "Bildebibliotek"; -/* Tab bar title for the Photos tab in Media Picker */ -"Photos" = "Fotografier"; - -/* Subtitle for placeholder in Free Photos. The company name 'Pexels' should always be written as it is. */ -"Photos provided by Pexels" = "Bilder levert av Pexels"; - /* Title for selecting a new username in the site creation flow. */ "Pick username" = "Velg brukernavn"; @@ -3884,15 +3744,6 @@ translators: Block name. %s: The localized block name */ The post formats available for the post. Should be the same as in core WP. */ "Post Format" = "Innleggsformat"; -/* Prompts the user that a restored post was moved to the drafts list. */ -"Post Restored to Drafts" = "Innlegg tilbakestilt til Kladd"; - -/* Prompts the user that a restored post was moved to the published list. */ -"Post Restored to Published" = "Innlegg tilbakestilt til Publisert"; - -/* Prompts the user that a restored post was moved to the scheduled list. */ -"Post Restored to Scheduled" = "Innlegg tilbakestilt til Planlagt"; - /* Name of the button to open the post settings The title of the Post Settings screen. */ "Post Settings" = "Innleggsinnstillinger"; @@ -3912,9 +3763,6 @@ translators: Block name. %s: The localized block name */ /* Title of notification displayed when a post has failed to upload. */ "Post failed to upload" = "Innlegg kunne ikke lastes opp"; -/* A short message explaining that a post was moved to the trash bin. */ -"Post moved to trash." = "Innlegg flyttet til papirkurven."; - /* Title of notification displayed when a post has been successfully saved as a draft. */ "Post pending review" = "Innlegg venter på gjennomgang"; @@ -3964,9 +3812,6 @@ translators: Block name. %s: The localized block name */ Period Stats 'Posts and Pages' header */ "Posts and Pages" = "Innlegg og sider"; -/* Title of the Posts Page Badge */ -"Posts page" = "Innleggsside"; - /* Posts per Page Title */ "Posts per Page" = "Innlegg per side"; @@ -3976,9 +3821,6 @@ translators: Block name. %s: The localized block name */ /* A message explaining the Posts I Like feature in the reader */ "Posts that you like will appear here." = "Innlegg du liker vil dukke opp her."; -/* Subtitle for placeholder in Tenor picker. `The company name 'Tenor' should always be written as it is. */ -"Powered by Tenor" = "Drevet av Tenor"; - /* Browse premium themes selection title */ "Premium" = "Premium"; @@ -3994,9 +3836,6 @@ translators: Block name. %s: The localized block name */ Title for screen to preview a static content. */ "Preview" = "Forhåndsvisning"; -/* Action for Media Picker to preview the selected media items. The argument in the string represents the number of elements (as numeric digits) selected */ -"Preview %@" = "Forhåndsvis %@"; - /* Title for web preview device switching button */ "Preview Device" = "Forhåndsvisningsenhet"; @@ -4037,8 +3876,7 @@ translators: Block name. %s: The localized block name */ "Privacy notice for California users" = "Personvernnotis for brukere i California"; /* Name for the status of a post that is marked private. - Privacy setting for posts set to 'Private'. Should be the same as in core WP. - Title of the Private Badge */ + Privacy setting for posts set to 'Private'. Should be the same as in core WP. */ "Private" = "Privat"; /* Error message title informing the user that reader content could not be loaded. */ @@ -4074,12 +3912,10 @@ translators: Block name. %s: The localized block name */ Label for the publish date button. */ "Publish Date" = "Publiseringsdato"; -/* A short phrase indicating a post is due to be immedately published. - Label for a button that moves a page to the published folder, publishing with the current date/time. */ +/* A short phrase indicating a post is due to be immedately published. */ "Publish Immediately" = "Publiser umiddelbart"; /* Label for a button that publishes the post - Label for an option that moves a publishes a post immediately Title of button allowing the user to immediately publish the post they are editing. */ "Publish Now" = "Publiser nå"; @@ -4094,8 +3930,7 @@ translators: Block name. %s: The localized block name */ /* Name for the status of a published post. Period Stats 'Published' header - Title of the published filter. This filter shows a list of posts that the user has published. - Title of the published header in search list. */ + Title of the published filter. This filter shows a list of posts that the user has published. */ "Published" = "Publisert"; /* Precedes the name of the blog just posted on */ @@ -4213,8 +4048,7 @@ translators: Block name. %s: The localized block name */ /* Label for selecting the related posts options */ "Related Posts" = "Relaterte innlegg"; -/* Add asset to media picker list - Alert button to confirm a plugin to be removed +/* Alert button to confirm a plugin to be removed Button label when removing a blog Label for the Remove Feature Image icon. Tapping will show a confirmation screen for removing the feature image from the post. Remove Action @@ -4327,9 +4161,6 @@ translators: Block name. %s: The localized block name */ /* Title of secondary button on alert prompting verify their accounts while attempting to publish */ "Resend" = "Send på nytt"; -/* Title of the reset button */ -"Reset" = "Nullstill"; - /* The button title for a secondary call-to-action button. When the user can't remember their password. */ "Reset your password" = "Tilbakestill passordet ditt"; @@ -4353,12 +4184,9 @@ translators: Block name. %s: The localized block name */ Button title, displayed when media has failed to upload. Allows the user to try the upload again. Button title. Retries uploading a post. If a user taps the button with this label, the action that evinced this error view will be retried. - Label for a button that attempts to re-upload a page that previously failed to upload. - Label for the retry post upload button. Tapping attempts to upload the post again. Opens the media library . Retries the upload of a user's post. Retry updating User's Role - Retry uploading the post. Retry. Action Retry. Verb – retry a failed media upload. The Jetpack view button title used when an error occurred @@ -4367,9 +4195,6 @@ translators: Block name. %s: The localized block name */ User action to retry media upload. */ "Retry" = "Prøv igjen"; -/* User action to retry media upload. */ -"Retry Upload" = "Prøv opplasting igjen"; - /* User action to retry all failed media uploads. */ "Retry all" = "Prøv alle igjen"; @@ -4449,9 +4274,6 @@ translators: Block name. %s: The localized block name */ /* Accessibility label for the 'Save Post' button when a post has been saved. */ "Saved Post" = "Lagret innlegg"; -/* Text displayed in HUD when a media item's metadata (title, etc) is saved successfully. */ -"Saved!" = "Lagret!"; - /* Accessibility hint for the 'Save Post' button. */ "Saves this post for later." = "Lagrer dette innlegget til senere."; @@ -4459,7 +4281,6 @@ translators: Block name. %s: The localized block name */ "Saving post…" = "Lagrer innlegg..."; /* Menus save button title while it is saving a Menu. - Text displayed in HUD while a media item's metadata (title, etc) is being saved. Text displayed in HUD while a post is being saved as a draft. */ "Saving..." = "Lagrer…"; @@ -4499,21 +4320,9 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Search blocks" = "Søk blokker"; -/* Text displayed when the search controller will be presented */ -"Search pages" = "Søk sider"; - -/* Text displayed when the search controller will be presented */ -"Search posts" = "Søk i innlegg"; - /* No comment provided by engineer. */ "Search settings" = "Søkeinnstillinger"; -/* Title for placeholder in Tenor picker */ -"Search to find GIFs to add to your Media Library!" = "Søk for å finne GIF-bilder du kan legge til i Mediebiblioteket ditt!"; - -/* Title for placeholder in Free Photos */ -"Search to find free photos to add to your Media Library!" = "Søk etter gratis bilder du kan legge til i ditt mediebibliotek!"; - /* Menus search bar placeholder text. */ "Search..." = "Søk..."; @@ -4578,9 +4387,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Select a layout" = "Velg et oppsett"; -/* Accessibility hint for actions when displaying media items. */ -"Select media." = "Velge medier."; - /* Accessibility label for selecting paragraph style button on formatting toolbar. */ "Select paragraph style" = "Velg avsnittsstil"; @@ -4656,16 +4462,9 @@ translators: Block name. %s: The localized block name */ /* Label for connected service in Publicize stat. */ "Service" = "Tjeneste"; -/* Label for a button that opens the Set Parent options view controller - Navigation title displayed on the navigation bar */ +/* Navigation title displayed on the navigation bar */ "Set Parent" = "Sett forelder"; -/* Label for a button that sets the selected page as the site's Homepage */ -"Set as Homepage" = "Sett som hjemmeside"; - -/* Label for a button that sets the selected page as the site's Posts page */ -"Set as Posts Page" = "Sett som innleggsside"; - /* The Jetpack view button title for the success state */ "Set up" = "Sett opp"; @@ -4697,7 +4496,6 @@ translators: Block name. %s: The localized block name */ /* Accessibility label for share buttons in nav bars Button label to share a post Button label to share a web page - Share the post. Shares the comment URL Spoken accessibility label Title for a button that allows the user to share their answer to the prompt. @@ -5016,8 +4814,7 @@ translators: Block name. %s: The localized block name */ /* Name of setting configured when a site uses a static page as its homepage */ "Static Homepage" = "Statisk hjemmeside"; -/* Label for post stats option. Tapping displays statistics for a post. - Noun. Abbreviation of Statistics. Name of the Stats feature +/* Noun. Abbreviation of Statistics. Name of the Stats feature Noun. Abbv. of Statistics. Links to a blog's Stats screen. Stats 3D Touch Shortcut Stats window title @@ -5039,9 +4836,6 @@ translators: Block name. %s: The localized block name */ /* Label text that defines a post marked as sticky */ "Sticky" = "Klebrig"; -/* Accessibility label for a sticky post in the post list. */ -"Sticky." = "Klebrig."; - /* User action to stop upload. */ "Stop upload" = "Stans opplasting"; @@ -5080,7 +4874,7 @@ translators: Block name. %s: The localized block name */ Theme Support action title */ "Support" = "Support"; -/* Button used to switch site */ +/* Title for back button that leads to the site picker screen. */ "Switch Site" = "Bytt nettsted"; /* Switches the Editor to HTML Mode */ @@ -5156,9 +4950,6 @@ translators: Block name. %s: The localized block name */ /* Displayed when the user views tags in blog settings and there are no tags */ "Tags created here can be quickly added to new posts" = "Stikkord opprettet her kan enkelt legges til nye innlegg"; -/* Menu option for taking an image or video with the device's camera. */ -"Take Photo or Video" = "Ta et bilde eller video"; - /* No comment provided by engineer. */ "Take a Photo" = "Ta et bilde"; @@ -5217,8 +5008,7 @@ translators: Block name. %s: The localized block name */ /* Title of a button style */ "Text Only" = "Kun tekst"; -/* Button title - The button's title text to send a 2FA code via SMS text message. */ +/* Button title */ "Text me a code instead" = "Send koden på tekstmelding i stedet"; /* Message of alert when theme activation succeeds */ @@ -5248,9 +5038,6 @@ translators: Block name. %s: The localized block name */ /* Error message shown to a user who is trying to share to Facebook but does not have any available Facebook Pages. */ "The Facebook connection cannot find any Pages. Publicize cannot connect to Facebook Profiles, only published Pages." = "Facebook-tilkoblingen kan ikke finne noen sider. Publisering kan ikke koble til Facebook-profiler, kun publiserte sider."; -/* Message shown when a GIF failed to load while trying to add it to the Media library. */ -"The GIF could not be added to the Media Library." = "GIF-filen kunne ikke legges til i Mediebiblioteket."; - /* Description shown when a user logs in with Google but no matching WordPress.com account is found */ "The Google account \"%@\" doesn't match any account on WordPress.com" = "Google-kontoen \"%@\" matcher ikke en WordPress.com-konto"; @@ -5333,7 +5120,7 @@ translators: Block name. %s: The localized block name */ /* Error message shown when user attempts to remove the site owner. */ "The user you are trying to remove is the owner of this site. Please contact support for assistance." = "Brukeren du prøver å fjerne er eieren av siden. Vennligst kontakt kundestøtten for hjelp."; -/* Error message informing a user about an invalid password. */ +/* No comment provided by engineer. */ "The username or password stored in the app may be out of date. Please re-enter your password in the settings and try again." = "Brukernavnet eller passordet som er lagret i appen er muligens utløpt. Vennligst skriv inn ditt passord i innstillingene og prøv igjen."; /* Message shown when a video failed to load while trying to add it to the Media library. */ @@ -5395,9 +5182,6 @@ translators: Block name. %s: The localized block name */ /* A short error message letting the user know about a problem displaying a post. */ "There was a problem displaying this post." = "Det oppstod et problem under visning av innlegget."; -/* Error message displayed when the Media Library is unable to load a full sized preview of an item. */ -"There was a problem loading the media item." = "Det oppstod et problem under lasting av medieobjektet."; - /* The loading view subtitle displayed when an error occurred */ "There was a problem loading your data, refresh your page to try again." = "Det oppstod et problem med lasting av dine data, last inn siden på nytt og prøv igjen."; @@ -5410,9 +5194,6 @@ translators: Block name. %s: The localized block name */ /* Explaining to the user there was an error trying to obtain the current location of the user. */ "There was a problem when trying to access your location. Please try again later." = "Det oppstod et problem under forsøk på å få tilgang til din plassering. Prøv igjen senere."; -/* Explaining to the user there was an generic error accesing media. */ -"There was a problem when trying to access your media. Please try again later." = "Det oppstod et problem under forsøk på å få tilgang til dine medier. Vennligst prøv igjen senere."; - /* Message for stories unknown error. */ "There was a problem with the Stories editor. If the problem persists you can contact us via the Me > Help & Support screen." = "Det oppstod er problem under følging av nettstedet. Hvis problemer fortsetter kan du kontakte oss via Meg > Hjelp og støtte-skjermen."; @@ -5459,9 +5240,6 @@ translators: Block name. %s: The localized block name */ /* An error message display if the users device does not have a camera input available */ "This app needs permission to access the Camera to capture new media, please change the privacy settings if you wish to allow this." = "Denne appen trenger tilgang til kameraet for å ta bilder og video, vennligst endre personverninnstillingene hvis du ønsker dette."; -/* Explaining to the user why the app needs access to the device media library. */ -"This app needs permission to access your device media library in order to add photos and\/or video to your posts. Please change the privacy settings if you wish to allow this." = "Denne appen trenger tilgang til enhetens mediebibliotek for å legge til bilder og\/eller videoer i innleggene dine. Vennligst endre personverninnstillingene hvis du ønsker dette."; - /* An error message informing the user the email address they entered did not match a WordPress.com account. */ "This email address is not registered on WordPress.com." = "Denne epostadressen er ikke registrert på WordPress.com."; @@ -5628,8 +5406,7 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Transform block…" = "Omform blokk..."; -/* Accessibility label for trash button to delete items from the user's media library - Accessibility label for trash buttons in nav bars +/* Accessibility label for trash buttons in nav bars Trashes a comment Trashes the comment */ "Trash" = "Slett"; @@ -5706,18 +5483,12 @@ translators: Block name. %s: The localized block name */ /* Shown when a user logs in with Google but it subsequently fails to work as login to WordPress.com */ "Unable To Connect" = "Klarte ikke å koble til"; -/* An error message. */ -"Unable to Connect" = "Kunne ikke koble til"; - /* Title of a prompt saying the app needs an internet connection before it can load posts */ "Unable to Load Posts" = "Kunne ikke laste inn innlegg"; /* Title of error prompt shown when a sync the user initiated fails. */ "Unable to Sync" = "Synkronosering feilet"; -/* Text displayed in HUD if there was an error attempting to delete a group of media items. */ -"Unable to delete all media items." = "Kunne ikke slette alle medieobjekter."; - /* Text displayed in HUD if there was an error attempting to delete a media item. */ "Unable to delete media item." = "Kunne ikke slette medieobjekt."; @@ -5751,9 +5522,6 @@ translators: Block name. %s: The localized block name */ /* Text displayed in HUD when a media item's metadata (title, etc) couldn't be saved. */ "Unable to save media item." = "Kunne ikke lagre medieobjekt."; -/* Message that appears when a user tries to trash a post while their device is offline. */ -"Unable to trash posts while offline. Please try again later." = "Kan ikke kaste innlegg i papirkurven når du er offline. Prøv igjen senere."; - /* This is an error message that could be shown when updating Plans in the app. */ "Unable to update plan prices. There is a problem with the supplied blog." = "Kunne ikke oppdatere abonnementspriser. Det er et problem med denne bloggen."; @@ -5808,8 +5576,6 @@ translators: Block name. %s: The localized block name */ Button title. Reverts the previous notification operation Revert an operation Revert enabling notification after successfully subcribing to the comments for the post. - The title of an 'undo' button. Tapping the button moves a trashed page out of the trash folder. - The title of an 'undo' button. Tapping the button moves a trashed post out of the trash folder. Undo action */ "Undo" = "Angre"; @@ -5843,9 +5609,6 @@ translators: Block name. %s: The localized block name */ /* Title for Unknown HTML Editor */ "Unknown HTML" = "Ukjent HTML"; -/* Label to use when creation date from media asset is not know. */ -"Unknown creation date" = "Ukjent opprettelsesdato"; - /* No comment provided by engineer. */ "Unknown error" = "Ukjent feil"; @@ -6038,15 +5801,10 @@ translators: Block name. %s: The localized block name */ /* Message shown if a video export is canceled by the user. */ "Video export canceled." = "Videoeksport avbrutt."; -/* Accessibility label for video thumbnails in the media collection view. The parameter is the creation date of the video. */ -"Video, %@" = "Video, %@"; - /* Period Stats 'Videos' header */ "Videos" = "Filmer"; /* Button title. Displays a summary / sharing screen for a specific post. - Label for a button that opens the page when tapped. - Label for the view post button. Tapping displays the post as it appears on the web. Opens the post epilogue screen to allow sharing / viewing of a post. Theme View action title Verb. The screen title shown when viewing a post inside the app. */ @@ -6377,9 +6135,6 @@ translators: Block name. %s: The localized block name */ /* Siri Suggestion to open Support */ "WordPress Help" = "Hjelp med WordPress"; -/* Name for the WordPress Media Library */ -"WordPress Media" = "WordPress-medier"; - /* Accessibility label for selecting an image or video from the user's WordPress media library on formatting toolbar. */ "WordPress Media Library" = "WordPress-mediebiblioteket"; @@ -6603,9 +6358,6 @@ translators: Block name. %s: The localized block name */ /* Share extension error dialog text. */ "Your account does not have permission to upload media to this site. The Site Administrator can change these permissions." = "Kontoen din har ikke tillatelse til å laste opp mediefiler til dette nettstedet. Nettstedets administrator kan endre disse tillatelsene."; -/* Explaining to the user why the app needs access to the device media library. */ -"Your app is not authorized to access media library due to active restrictions such as parental controls. Please check your parental control settings in this device." = "Appen har ikke tilgang til mediebiblioteket på grunn av aktive restriksjoner som foreldrekontroll. Vennligst sjekk innstillingene for foreldrekontroll på denne enheten."; - /* Instructional text that displays the current username and display name. */ "Your current username is %@. With few exceptions, others will only ever see your display name, %@." = "Ditt gjeldende brukernavn er %1$@. Med få unntak vil andre kun se visningsnavnet ditt, %2$@."; @@ -6666,9 +6418,6 @@ translators: Block name. %s: The localized block name */ /* Age between dates equaling one hour. */ "an hour" = "én time"; -/* Label displayed on audio media items. */ -"audio" = "lyd"; - /* Used when displaying author of a plugin. */ "by %@" = "av %@"; @@ -6692,9 +6441,6 @@ translators: Block name. %s: The localized block name */ /* (placeholder) Help the user enter a URL into the field */ "http:\/\/my-site-address (URL)" = "http:\/\/min-nettsteds-adresse (URL)"; -/* Label displayed on image media items. */ -"image" = "bilde"; - /* This text is used when the user is configuring the iOS widget to suggest them to select the site to configure the widget for */ "ios-widget.gpCwrM" = "Select Site"; @@ -6704,12 +6450,6 @@ translators: Block name. %s: The localized block name */ /* Later today */ "later today" = "senere i dag"; -/* Word separating the current index from the total amount. I.e.: 7 of 9 */ -"of" = "av"; - -/* Label displayed on media items that are not video, image, or audio. */ -"other" = "annet"; - /* No comment provided by engineer. */ "password" = "passord"; @@ -6722,9 +6462,6 @@ translators: Block name. %s: The localized block name */ /* Used when the response doesn't have a valid url to display */ "unknown url" = "ukjent url"; -/* Label displayed on video media items. */ -"video" = "video"; - /* Title of best views ever label in all time widget */ "widget.alltime.bestviews.label" = "Beste visninger noensinne"; diff --git a/WordPress/Resources/nl.lproj/Localizable.strings b/WordPress/Resources/nl.lproj/Localizable.strings index 992994709556..0e51f31cf752 100644 --- a/WordPress/Resources/nl.lproj/Localizable.strings +++ b/WordPress/Resources/nl.lproj/Localizable.strings @@ -1,4 +1,4 @@ -/* Translation-Revision-Date: 2023-10-18 14:54:08+0000 */ +/* Translation-Revision-Date: 2024-01-03 14:54:08+0000 */ /* Plural-Forms: nplurals=2; plural=n != 1; */ /* Generator: GlotPress/4.0.0-alpha.11 */ /* Language: nl */ @@ -152,9 +152,6 @@ /* Difference label for Period Overview stat, indicating change from previous period. Ex: +99.9K (5%) */ "%@%@ (%@%%)" = "%1$@%2$@ (%3$@%%)"; -/* Accessibility label for a post in the post list. The parameters are the title, and date respectively. For example, \"Let it Go, 1 hour ago.\" */ -"%@, %@." = "%1$@, %2$@."; - /* Accessibility value for a Stats' Posting Activity Month if the user selected a day with posts. The first parameter is day (e.g. November 2019). The second parameter is the number of posts. */ "%@. %d posts." = "%1$@. %2$d berichten."; @@ -210,9 +207,6 @@ /* Age between dates over one year. */ "%d years" = "%d jaren"; -/* Max image size in pixels (e.g. 300x300px) */ -"%dx%dpx" = "%1$dx%2$dpx"; - /* One menu area available in the theme */ "%i menu area in this theme" = "%i menugebied in dit thema"; @@ -268,6 +262,9 @@ translators: Block name. %s: The localized block name */ /* translators: %s: social link name e.g: \"Instagram\". */ "%s social icon" = "%s social pictogram"; +/* translators: displayed right after the classic block is converted to blocks. %s: The localized classic block name */ +"'%s' block converted to blocks" = "Het blok '%s' is omgezet naar blokken"; + /* translators: Missing block alert title. %s: The localized block name */ "'%s' is not fully-supported" = "'%s' wordt niet volledig ondersteund"; @@ -475,13 +472,6 @@ translators: Block name. %s: The localized block name */ /* Label for the Activity Type filter button when there are more than 1 activity type selected */ "Activity Type (%1$d)" = "Type activiteit (%1$d)"; -/* Accessibility label for add button to add items to the user's media library - Remove asset from media picker list */ -"Add" = "Toevoegen"; - -/* Action for Media Picker to indicate selection of media. The argument in the string represents the number of elements (as numeric digits) selected */ -"Add %@" = "Voeg %@ toe"; - /* No comment provided by engineer. */ "Add Block After" = "Blok toevoegen na"; @@ -578,9 +568,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Add menu item to children" = "Menu-item aan onderliggende toevoegen"; -/* Accessibility hint for add button to add items to the user's media library */ -"Add new media" = "Nieuwe media toevoegen"; - /* Screen reader text for Menus button that adds a new menu to a site. */ "Add new menu" = "Nieuw menu toevoegen"; @@ -646,9 +633,6 @@ translators: Block name. %s: The localized block name */ /* Option to select the Airmail app when logging in with magic links */ "Airmail" = "Airmail"; -/* Description of albums in the photo libraries */ -"Albums" = "Albums"; - /* Image alignment option title. Title of the screen for choosing an image's alignment. */ "Alignment" = "Uitlijning"; @@ -662,6 +646,9 @@ translators: Block name. %s: The localized block name */ Title of the drafts filter. This filter shows a list of draft posts. */ "All" = "Alles"; +/* Information about redeeming domain credit on site dashboard. */ +"All WordPress.com annual plans include a custom domain name. Register your free domain now." = "Alle jaarlijkse abonnementen van WordPress.com zijn inclusief aangepaste domeinnaam. Registreer nu je gratis domein."; + /* Footer of the free domain registration section for a paid plan. Information about redeeming domain credit on site dashboard. */ "All WordPress.com plans include a custom domain name. Register your free premium domain now." = "Alle WordPress.com abonnementen zijn inclusief een aangepaste domeinnaam. Registreer je gratis domein nu."; @@ -727,10 +714,10 @@ translators: Block name. %s: The localized block name */ "Alt Text" = "Alt-tekst"; /* No comment provided by engineer. */ -"Alternatively, you can detach and edit these blocks separately by tapping “Detach patterns”." = "Als alternatief, kan je deze blokken ook losmaken en afzonderlijk bewerken door op 'Patronen losmaken' te tikken."; +"Alternatively, you can convert the content to blocks." = "Je kan ook de inhoud naar blokken omzetten."; -/* No comment provided by engineer. */ -"Alternatively, you can detach and edit this block separately by tapping “Detach pattern”." = "Als alternatief, kan je dit blok ook losmaken en afzonderlijk bewerken door op 'Patroon losmaken' te tikken."; +/* translators: Alternative option included in a warning related to having blocks deeply nested. */ +"Alternatively, you can flatten the content by ungrouping the block." = "Alternatively, you can flatten the content by ungrouping the block."; /* Instruction text to explain to help users type their password instead of using magic link login option. */ "Alternatively, you may enter the password for this account." = "Je kunt ook het wachtwoord voor dit account invoeren."; @@ -879,15 +866,9 @@ translators: Block name. %s: The localized block name */ /* Message prompting the user to confirm that they want to disconnect Jetpack from the site. */ "Are you sure you want to disconnect Jetpack from the site?" = "Weet je zeker dat je Jetpack wilt loskoppelen van jouw site?"; -/* Message prompting the user to confirm that they want to permanently delete a group of media items. */ -"Are you sure you want to permanently delete these items?" = "Weet je zeker dat je deze items definitief wilt verwijderen?"; - /* Message prompting the user to confirm that they want to permanently delete a media item. Should match Calypso. */ "Are you sure you want to permanently delete this item?" = "Weet je zeker dat je dit item permanent wilt verwijderen?"; -/* Message of the confirmation alert when deleting a page from the trash. */ -"Are you sure you want to permanently delete this page?" = "Weet je zeker dat je deze pagina permanent wil verwijderen?"; - /* Message of the confirmation alert when deleting a post from the trash. */ "Are you sure you want to permanently delete this post?" = "Weet je zeker dat je dit bericht permanent wilt verwijderen?"; @@ -919,9 +900,6 @@ translators: Block name. %s: The localized block name */ /* Title of message shown when user taps submit for review. */ "Are you sure you want to submit for review?" = "Weet je zeker dat deze wilt verzenden voor controle?"; -/* Message of the trash page confirmation alert. */ -"Are you sure you want to trash this page?" = "Weet je zeker dat je deze pagina naar de prullenbak wil verplaatsen?"; - /* Message of the trash confirmation alert. */ "Are you sure you want to trash this post?" = "Weet je zeker dat je dit bericht naar de prullenbak wilt verplaatsen?"; @@ -962,9 +940,6 @@ translators: Block name. %s: The localized block name */ /* translators: accessibility text. Empty Audio caption. */ "Audio caption. Empty" = "Audio bijschrift. Leeg"; -/* Accessibility label for audio items in the media collection view. The parameter is the creation date of the audio. */ -"Audio, %@" = "Audio, %@"; - /* No comment provided by engineer. */ "Authenticating" = "Authenticatie"; @@ -1175,10 +1150,7 @@ translators: Block name. %s: The localized block name */ "Blocks menu" = "Blokkenmenu"; /* translators: Warning related to having blocks deeply nested. %d: The deepest nesting level. */ -"Blocks nested deeper than %d levels may not render properly in the mobile editor. For this reason, we recommend flattening the content by ungrouping the block or editing the block using the web editor." = "Blokken die dieper dan %d niveaus genesteld zijn worden mogelijk niet goed weergegeven in de mobiele editor. Hierom raden we aan de content platter te maken door het blok niet te groeperen of het blok te bewerken in je webbrowser."; - -/* translators: Warning related to having blocks deeply nested. %d: The deepest nesting level. */ -"Blocks nested deeper than %d levels may not render properly in the mobile editor. For this reason, we recommend flattening the content by ungrouping the block or editing the block using your web browser." = "Blokken die dieper dan %d niveaus genesteld zijn worden mogelijk niet goed weergegeven in de mobiele editor. Hierom raden we aan de content platter te maken door het blok niet te groeperen of het blok te bewerken in je webbrowser."; +"Blocks nested deeper than %d levels may not render properly in the mobile editor." = "Blokken die dieper dan %d niveaus genesteld zijn worden mogelijk niet goed weergegeven in de mobiele editor."; /* Title of a button that displays the WordPress.com blog */ "Blog" = "Blog"; @@ -1256,9 +1228,6 @@ translators: Block name. %s: The localized block name */ /* Label for the post author in the post detail. */ "By " = "Door "; -/* Accessibility label for the post author in the post list. The parameter is the author name. For example, \"By Elsa.\" */ -"By %@." = "Door %@."; - /* Legal disclaimer for logging in. The underscores _..._ denote underline. */ "By continuing, you agree to our _Terms of Service_." = "Als je doorgaat, ga je akkoord met onze _algemene voorwaarden_."; @@ -1278,8 +1247,7 @@ translators: Block name. %s: The localized block name */ /* Label for size of media while it's being calculated. */ "Calculating..." = "Wordt berekend ..."; -/* Accessibility label for taking an image or video with the camera on formatting toolbar. - Accessibility label for the camera tile in the collection view */ +/* Accessibility label for taking an image or video with the camera on formatting toolbar. */ "Camera" = "Camera"; /* Title of an alert informing the user the camera permission for the app is disabled and its needed to proceed */ @@ -1330,10 +1298,7 @@ translators: Block name. %s: The localized block name */ Cancels an Action Cancels an alert. Cancels the mark all as read action. - Dismiss the post action sheet Editing GIF alert cancel action button. - Label for a cancel button - Label for the auto-upload cancelation button in the post list. Tapping will prevent the app from auto-uploading the post. Menus cancel button for deleting a menu. Menus cancel button within text bar while editing items. Menus: Cancel button title for canceling an edited menu item. @@ -1350,10 +1315,6 @@ translators: Block name. %s: The localized block name */ Verb. Title for Jetpack Restore cancel button. */ "Cancel" = "Annuleren"; -/* Label for the Post List option that cancels automatic uploading of a post. - Media Library option to cancel an in-progress or failed upload. */ -"Cancel Upload" = "Annuleer upload"; - /* Menus alert button title to cancel discarding changes and not createa a new menu. Menus alert button title to cancel discarding changes and not select a new menu Menus alert button title to cancel discarding changes and not select a new menu location */ @@ -1412,9 +1373,6 @@ translators: Block name. %s: The localized block name */ Main title */ "Change Password" = "Wijzig wachtwoord"; -/* Title of button that takes user to the system Settings section for the app */ -"Change Settings" = "Wijzig instellingen"; - /* Change Username title. */ "Change Username" = "Wijzig gebruikersnaam"; @@ -1554,9 +1512,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Choose file" = "Kies bestand"; -/* Menu option for selecting media from the device's photo library. */ -"Choose from My Device" = "Kies van mijn apparaat"; - /* Explanatory text for Homepage Settings homepage type selection. */ "Choose from a homepage that displays your latest posts (classic blog) or a fixed \/ static page." = "Kies uit een homepage waarop je nieuwste berichten (klassiek blog) of een vaste\/statische pagina wordt weergegeven."; @@ -1757,9 +1712,6 @@ translators: Block name. %s: The localized block name */ /* Community & Non-Profit site intent topic */ "Community & Non-Profit" = "Community en non-profit"; -/* Accessibility indication that the current Post List style is currently Compact. */ -"Compact" = "Compact"; - /* The action is completed */ "Completed" = "Afgerond"; @@ -1945,10 +1897,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Copied block" = "Gekopieerd blok"; -/* Copy the post url and paste anywhere in phone - Label for page copy link. Tapping copy the url of page */ -"Copy Link" = "Link kopiëren"; - /* Copy link to oomment button title */ "Copy Link to Comment" = "Kopieer link naar reactie"; @@ -2057,9 +2005,6 @@ translators: Block name. %s: The localized block name */ /* Error title displayed when unable to close user account. */ "Couldn’t close account automatically" = "Kon account niet automatisch sluiten"; -/* Message to show while media data source is finding the number of items available. */ -"Counting media items..." = "Media-items tellen..."; - /* Period Stats 'Countries' header */ "Countries" = "Landen"; @@ -2310,7 +2255,6 @@ translators: Block name. %s: The localized block name */ /* Delete Delete button title for the warning shown to the user when he refuses to re-login when the authToken is missing. Title for button that permanently deletes a media item (photo / video) - Title for button that permanently deletes one or more media items (photos / videos) Title of a delete button Title of the trash confirmation alert. */ "Delete" = "Verwijderen"; @@ -2318,15 +2262,11 @@ translators: Block name. %s: The localized block name */ /* Menus confirmation button for deleting a menu. */ "Delete Menu" = "Menu verwijderen"; -/* Delete option in the confirmation alert when deleting a page from the trash. - Delete option in the confirmation alert when deleting a post from the trash. - Label for a button permanently deletes a page. - Label for the delete post option. Tapping permanently deletes a post. +/* Delete option in the confirmation alert when deleting a post from the trash. Title for button on the comment details page that deletes the comment when tapped. */ "Delete Permanently" = "Permanent verwijderen"; -/* Title of the confirmation alert when deleting a page from the trash. - Title of the confirmation alert when deleting a post from the trash. */ +/* Title of the confirmation alert when deleting a post from the trash. */ "Delete Permanently?" = "Permanent verwijderen?"; /* Button label for deleting the current site @@ -2445,7 +2385,6 @@ translators: Block name. %s: The localized block name */ /* Accessibility label for button to dismiss a bottom sheet Accessibility label for the transparent space above the login dialog which acts as a button to dismiss the dialog. Accessibility label for the transparent space above the signup dialog which acts as a button to dismiss the dialog. - Action to show on alert when view asset fails. Dismiss a view. Verb */ "Dismiss" = "Afbreken"; @@ -2463,12 +2402,6 @@ translators: Block name. %s: The localized block name */ User's Display Name */ "Display Name" = "Weergavenaam"; -/* Accessibility label for other media items in the media collection view. The parameter is the creation date of the document. */ -"Document, %@" = "Document, %@"; - -/* Accessibility label for other media items in the media collection view. The parameter is the filename file. */ -"Document: %@" = "Document: %@"; - /* Message shown when all tours have been completed */ "Doesn't it feel good to cross things off a list?" = "Voelt het niet goed om dingen van een lijst af te strepen?"; @@ -2629,8 +2562,7 @@ translators: Block name. %s: The localized block name */ /* Description of a Quick Start Tour */ "Draft and publish a post." = "Stel een bericht op en publiceer het."; -/* Title of the drafts filter. This filter shows a list of draft posts. - Title of the drafts header in search list. */ +/* Title of the drafts filter. This filter shows a list of draft posts. */ "Drafts" = "Concepten"; /* No comment provided by engineer. */ @@ -2642,10 +2574,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Drag to adjust focal point" = "Slepen om het brandpunt aan te passen"; -/* Label for page duplicate option. Tapping creates a copy of the page. - Label for post duplicate option. Tapping creates a copy of the post. */ -"Duplicate" = "Dupliceer"; - /* No comment provided by engineer. */ "Duplicate block" = "Dupliceer blok"; @@ -2658,13 +2586,9 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Each block has its own settings. To find them, tap on a block. Its settings will appear on the toolbar at the bottom of the screen." = "Elk blok heeft zijn eigen instellingen. Om ze te vinden, tik op een blok. Zijn instellingen zullen verschijnen in de gereedschapsbalk onderaan het scherm."; -/* Accessibility label for edit button to enable multi selection mode in the user's media library - Edit the post. - Editing GIF alert default action button. +/* Editing GIF alert default action button. Edits a Comment Edits the comment - Label for a button that opens the Edit Page view controller - Label for the edit post button. Tapping displays the editor. User action to edit media details. Verb, edit a comment */ "Edit" = "Bewerken"; @@ -2672,9 +2596,6 @@ translators: Block name. %s: The localized block name */ /* Title for the edit more button section */ "Edit \"More\" button" = "Wijzig \"Meer\" knop"; -/* Button that displays the media editor to the user */ -"Edit %@" = "Bewerk %@"; - /* Blocklist Keyword Edition Title */ "Edit Blocklist Word" = "Bewerk bloklijst woord"; @@ -2867,9 +2788,6 @@ translators: Block name. %s: The localized block name */ /* Secondary message shown when there are no domains that match the user entered text. */ "Enter different words above and we'll look for an address that matches it." = "Voer hierboven andere woorden in, zodat wij op zoek kunnen gaan naar een adres dat overeenkomt."; -/* Accessibility hint for edit button to enable multi selection mode in the user's media library */ -"Enter edit mode to enable multi select to delete" = "Bewerken openen om gelijktijdig meerdere te verwijderen"; - /* (placeholder) Help enter WordPress password Placeholder of a field to type a password to protect the post. */ "Enter password" = "Voer wachtwoord in"; @@ -3025,9 +2943,6 @@ translators: Block name. %s: The localized block name */ /* Short title telling the user they will receive a blogging reminder every day of the week. */ "Every day at %@" = "Elke dag om %@"; -/* Label for the post author filter. This filter shows posts for all users on the blog. */ -"Everyone" = "Iedereen"; - /* Example story title description */ "Example story title" = "Voorbeeld verhaaltitel"; @@ -3037,9 +2952,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Excerpt length (words)" = "Lengte samenvatting (woorden)"; -/* Accessibility label for a post's excerpt in the post list. The parameter is the post excerpt. For example, \"Excerpt. This is the first paragraph.\" */ -"Excerpt. %@." = "Samenvatting. %@."; - /* Should be the same as the text displayed if the user clicks the (i) in Calypso. */ "Excerpts are optional hand-crafted summaries of your content." = "Samenvattingen zijn optionele uittreksels van je inhoud."; @@ -3049,8 +2961,7 @@ translators: Block name. %s: The localized block name */ /* Accessibility Label for the exit full screen button on the full screen comment reply mode */ "Exit Full Screen" = "Volledig scherm afsluiten"; -/* Accessibility indication that the current Post List style is currently Expanded. - Screen reader text to represent the expanded state of a UI control */ +/* Screen reader text to represent the expanded state of a UI control */ "Expanded" = "Uitgeklapt"; /* Accessibility hint */ @@ -3100,9 +3011,6 @@ translators: Block name. %s: The localized block name */ The action failed */ "Failed" = "Mislukt"; -/* Error title when picked media cannot be imported into stories. */ -"Failed Media Export" = "Media export mislukt"; - /* Message for mark all as read success notice */ "Failed marking Notifications as read" = "Meldingen als gelezen markeren mislukt"; @@ -3304,6 +3212,12 @@ translators: Block name. %s: The localized block name */ /* An example tag used in the login prologue screens. */ "Football" = "Voetbal"; +/* translators: Recommendation included in a warning related to having blocks deeply nested. */ +"For this reason, we recommend editing the block using the web editor." = "Hierom raden we aan de content platter te maken door het blok niet te groeperen of het blok te bewerken in je webbrowser."; + +/* translators: Recommendation included in a warning related to having blocks deeply nested. */ +"For this reason, we recommend editing the block using your web browser." = "Hierom raden we aan de content platter te maken door het blok niet te groeperen of het blok te bewerken in je webbrowser."; + /* Register Domain - Domain contact information section header description */ "For your convenience, we have pre-filled your WordPress.com contact information. Please review to be sure it’s the correct information you want to use for this domain." = "We hebben je contactgegevens van WordPress.com alvast ingevuld om je op weg te helpen. Controleer of dit de juiste gegevens zijn die je voor dit domein wilt gebruiken."; @@ -3621,8 +3535,7 @@ translators: Block name. %s: The localized block name */ "Home" = "Home"; /* Title for setting which shows the current page assigned as a site's homepage - Title for the homepage section in site settings screen - Title of the Homepage Badge */ + Title for the homepage section in site settings screen */ "Homepage" = "Homepage"; /* Label for Homepage Settings site settings section @@ -3719,9 +3632,6 @@ translators: Block name. %s: The localized block name */ /* Hint for image title on image settings. */ "Image title" = "Titel voor afbeelding"; -/* Accessibility label for image thumbnails in the media collection view. The parameter is the creation date of the image. */ -"Image, %@" = "Afbeelding, %@"; - /* Undated post time label */ "Immediately" = "Onmiddellijk"; @@ -4207,9 +4117,6 @@ translators: Block name. %s: The localized block name */ Settings: Comments Approval settings */ "Links in comments" = "Links in reacties"; -/* The accessibility label for the list style button in the Post List. */ -"List style" = "Lijststijl"; - /* Title of the screen that load selected the revisions. */ "Load" = "Laden"; @@ -4225,18 +4132,12 @@ translators: Block name. %s: The localized block name */ /* Text displayed while loading the activity feed for a site */ "Loading Backups..." = "Back-ups aan het laden..."; -/* Phrase to show when the user has searched for GIFs and they are being loaded. */ -"Loading GIFs..." = "GIF's laden..."; - /* Menus label text displayed while menus are loading */ "Loading Menus..." = "Menu's worden geladen..."; /* Text displayed while loading site People. */ "Loading People..." = "Mensen laden..."; -/* Phrase to show when the user has searched for images and they are being loaded. */ -"Loading Photos..." = "Foto's laden..."; - /* Text displayed while loading plans details */ "Loading Plan..." = "Abonnement wordt geladen..."; @@ -4297,8 +4198,7 @@ translators: Block name. %s: The localized block name */ /* Local Services site intent topic */ "Local Services" = "Plaatselijke diensten"; -/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. - Title of the Local Changes Badge */ +/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. */ "Local changes" = "Lokale wijzigingen"; /* Title for alert when a generic error happened when trying to find the location of the device */ @@ -4462,7 +4362,6 @@ translators: Block name. %s: The localized block name */ "Max Video Upload Size" = "Max Video upload-grootte"; /* Accessibility label for the Me button in My Site. - Label for the post author filter. This filter shows posts only authored by the current user. Me page title Noun. Title. Links to the Me screen. The accessibility value of the me tab. @@ -4470,9 +4369,7 @@ translators: Block name. %s: The localized block name */ "Me" = "Ik"; /* Noun. Title. Links to the blog's Media library. - Tab bar title for the Media tab in Media Picker The menu item to select during a guided tour. - Title for Media Library section of the app. Title for the media section in site settings screen Title label for the media settings section in the app settings */ "Media" = "Media"; @@ -4484,13 +4381,6 @@ translators: Block name. %s: The localized block name */ /* Label for size of media cache in the app. */ "Media Cache Size" = "Media cache grootte"; -/* Title for alert when access to media capture is not granted */ -"Media Capture" = "Mediaregistratie"; - -/* Title for alert when a generic error happened when loading media - Title for alert when access to the media library is not granted by the user */ -"Media Library" = "Mediabibliotheek"; - /* Title for action sheet with media options. */ "Media Options" = "Media opties"; @@ -4513,9 +4403,6 @@ translators: Block name. %s: The localized block name */ /* translators: %s: block title e.g: \"Paragraph\". */ "Media options" = "Media opties"; -/* Alert title when there is issues loading an asset to preview. */ -"Media preview failed." = "Voorvertoning media mislukt."; - /* Alert displayed to the user when multiple media items have uploaded successfully. */ "Media uploaded (%ld files)" = "Media geüpload (%ld bestanden)"; @@ -4553,9 +4440,6 @@ translators: Block name. %s: The localized block name */ Label for the share message field on the post settings. */ "Message" = "Bericht"; -/* Title of section containing image / video metadata such as size and file type */ -"Metadata" = "Metadata"; - /* Option to select the Microsft Outlook app when logging in with magic links */ "Microsoft Outlook" = "Microsoft Outlook"; @@ -4575,13 +4459,11 @@ translators: Block name. %s: The localized block name */ "Months and Years" = "Maanden en jaren"; /* Accessibility label for more button in dashboard quick start card. - Accessibility label for the More button in Page List. Accessibility label for the More button in Post List (compact view). Accessibility label for the More button on formatting toolbar. Accessibility label for the More button on Reader Cell Accessibility label for the More button on Reader's post details - Action button to display more available options - Label for the more post button. Tapping displays an action sheet with post options. */ + Action button to display more available options */ "More" = "Meer"; /* Action button to display more available options @@ -4639,15 +4521,8 @@ translators: Block name. %s: The localized block name */ /* Screen reader text for button that will move the menu item */ "Move menu item" = "Verplaats menu-item"; -/* Label for a button that moves a page to the draft folder - Label for an option that moves a post to the draft folder */ -"Move to Draft" = "Verplaatsen naar concept"; - -/* Label for a button that moves a page to the trash folder - Label for a option that moves a post to the trash folder - Title for button on the comment details page that moves the comment to trash when tapped. +/* Title for button on the comment details page that moves the comment to trash when tapped. Trash option in the trash confirmation alert. - Trash option in the trash page confirmation alert. Trashes the comment */ "Move to Trash" = "Verplaatsen naar prullenbak"; @@ -4679,7 +4554,8 @@ translators: Block name. %s: The localized block name */ Title of My Site tab */ "My Site" = "Mijn site"; -/* Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ +/* Title for site picker screen. + Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ "My Sites" = "Mijn sites"; /* Siri Suggestion to open My Sites */ @@ -4929,9 +4805,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title for the view when there aren't any Activities to display in the Activity Log for a given filter. */ "No matching events found." = "Geen overeenkomende evenementen gevonden."; -/* Message displayed when no results are returned from a media library search. Should match Calypso. - Phrase to show when the user search for images but there are no result to show. - Phrase to show when the user searches for GIFs but there are no result to show. */ +/* Message displayed when no results are returned from a media library search. Should match Calypso. */ "No media matching your search" = "Er komen geen media overeen met je zoekopdracht"; /* Menus text shown when no menus were available for loading the Menus editor. */ @@ -4949,8 +4823,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Displayed in the Notifications Tab as a title, when there are no notifications */ "No notifications yet" = "Nog geen meldingen"; -/* Displayed when the user is searching the pages list and there are no matching pages - Text displayed when there's no matching with the text search */ +/* Text displayed when there's no matching with the text search */ "No pages matching your search" = "Geen pagina's voldoen aan je zoekopdracht"; /* Text displayed when search for plugins returns no results */ @@ -4971,9 +4844,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Message shown whent the reader finds no posts for the chosen tag */ "No posts have been made recently with this tag." = "Er zijn onlangs geen berichten gemaakt met deze tag."; -/* Displayed when the user is searching the posts list and there are no matching posts */ -"No posts matching your search" = "Geen berichten voldoen aan je zoekopdracht"; - /* Accessibility value for a Stats' Posting Activity Month if there are no posts. */ "No posts." = "Geen berichten."; @@ -5074,9 +4944,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* A message title */ "Nothing liked yet" = "Nog niks leuk gevonden"; -/* Default message for empty media picker */ -"Nothing to show" = "Niets te tonen"; - /* Notifications Details Accessibility Identifier */ "Notification Details Table" = "Melding details tabel"; @@ -5136,7 +5003,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Alert dismissal title Button title. Acknowledges a prompt. Button title. An acknowledgement of the message displayed in a prompt. - Confirmation of action Default action Dismisses the alert Marks all notifications as read. @@ -5198,9 +5064,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* No comment provided by engineer. */ "Only show excerpt" = "Toon alleen samenvatting"; -/* Message telling the user that they've only enabled limited photo library permissions for the app. */ -"Only the selected photos you've given access to are available." = "Alleen de geselecteerde foto's waar je toegang voor hebt zijn beschikbaar."; - /* An informal exclaimation that means `something went wrong`. Title for the view when there's an error loading a comment. Title for the view when there's an error loading Activity Log @@ -5235,9 +5098,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title of a button that opens the apps settings in the system Settings.app */ "Open Settings" = "Open Instellingen"; -/* Editor button to swich the media picker from quick mode to full picker */ -"Open full media picker" = "Open de volledige mediakiezer"; - /* No comment provided by engineer. */ "Open in Safari" = "Open in Safari"; @@ -5335,15 +5195,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title shown when selecting a post type of Page from the Share Extension. */ "Page" = "Pagina"; -/* Prompts the user that a restored page was moved to the drafts list. */ -"Page Restored to Drafts" = "Pagina teruggezet naar Concepten"; - -/* Prompts the user that a restored page was moved to the published list. */ -"Page Restored to Published" = "Pagina teruggezet naar Gepubliceerd"; - -/* Prompts the user that a restored page was moved to the scheduled list. */ -"Page Restored to Scheduled" = "Pagina teruggezet naar Gepland"; - /* Name of the button to open the page settings The title of the Page Settings screen. */ "Page Settings" = "Pagina-instellingen"; @@ -5360,9 +5211,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of notification displayed when a page has failed to upload. */ "Page failed to upload" = "Uploaden van pagina mislukt"; -/* A short message explaining that a page was moved to the trash bin. */ -"Page moved to trash." = "Pagina verplaatst naar prullenbak."; - /* Title of notification displayed when a page has been successfully saved as a draft. */ "Page pending review" = "Pagina wacht op beoordeling"; @@ -5434,8 +5282,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title of pending Comments filter. */ "Pending" = "In afwachting"; -/* Name for the status of a post pending review. - Title of the Pending Review Badge */ +/* Name for the status of a post pending review. */ "Pending review" = "Afwachting van de moderatie"; /* Noun. Title of the people management feature. @@ -5464,12 +5311,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Photography site intent topic */ "Photography" = "Fotografie"; -/* Tab bar title for the Photos tab in Media Picker */ -"Photos" = "Foto's"; - -/* Subtitle for placeholder in Free Photos. The company name 'Pexels' should always be written as it is. */ -"Photos provided by Pexels" = "Foto's aangeboden door Pexels"; - /* Title for selecting a new username in the site creation flow. */ "Pick username" = "Kies een gebruikersnaam"; @@ -5562,7 +5403,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Please enter the password for your WordPress.com account to log in with your Apple ID." = "Vul het wachtwoord voor je WordPress.com account in om met je Apple ID in te loggen."; /* Instruction text on the two-factor screen. */ -"Please enter the verification code from your authenticator app, or tap the link below to receive a code via SMS." = "Geef de verificatie code in van de authentificatie app, of tik op de link hieronder om een code via SMS te ontvangen."; +"Please enter the verification code from your authenticator app." = "Voer de verificatiecode uit je Authenticator-app in."; /* Popup message to ask for user credentials (fields shown below). */ "Please enter your credentials" = "Voer alsjeblieft je gegevens in"; @@ -5657,15 +5498,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ The post formats available for the post. Should be the same as in core WP. */ "Post Format" = "Bericht format"; -/* Prompts the user that a restored post was moved to the drafts list. */ -"Post Restored to Drafts" = "Bericht teruggezet naar Concepten"; - -/* Prompts the user that a restored post was moved to the published list. */ -"Post Restored to Published" = "Bericht teruggezet naar Gepubliceerd"; - -/* Prompts the user that a restored post was moved to the scheduled list. */ -"Post Restored to Scheduled" = "Bericht teruggezet naar Gepland"; - /* Name of the button to open the post settings The title of the Post Settings screen. */ "Post Settings" = "Bericht instellingen"; @@ -5685,9 +5517,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of notification displayed when a post has failed to upload. */ "Post failed to upload" = "Uploaden van bericht mislukt"; -/* A short message explaining that a post was moved to the trash bin. */ -"Post moved to trash." = "Bericht verplaatst naar prullenbak."; - /* Title of notification displayed when a post has been successfully saved as a draft. */ "Post pending review" = "Bericht wacht op beoordeling"; @@ -5746,9 +5575,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Period Stats 'Posts and Pages' header */ "Posts and Pages" = "Berichten en pagina's"; -/* Title of the Posts Page Badge */ -"Posts page" = "Berichtenpagina"; - /* Message informing the user that their static homepage for posts was set successfully */ "Posts page successfully updated" = "Berichtenpagina met succes bijgewerkt"; @@ -5761,9 +5587,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* A message explaining the Posts I Like feature in the reader */ "Posts that you like will appear here." = "Berichten die je liket, verschijnen hier."; -/* Subtitle for placeholder in Tenor picker. `The company name 'Tenor' should always be written as it is. */ -"Powered by Tenor" = "Mogelijk gemaakt door Tenor"; - /* Browse premium themes selection title */ "Premium" = "Premium"; @@ -5782,18 +5605,12 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title for screen to preview a static content. */ "Preview" = "Voorbeeld"; -/* Action for Media Picker to preview the selected media items. The argument in the string represents the number of elements (as numeric digits) selected */ -"Preview %@" = "Voorbeeld %@"; - /* Title for web preview device switching button */ "Preview Device" = "Voorbeeld apparaat"; /* Title on display preview error */ "Preview Unavailable" = "Voorbeeld niet beschikbaar"; -/* Accessibility label for media item preview for user's viewing an item in their media library */ -"Preview media" = "Een voorbeeld van de media bekijken"; - /* No comment provided by engineer. */ "Preview page" = "Voorvertoning pagina"; @@ -5840,8 +5657,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Privacy notice for California users" = "Privacybericht voor gebruikers in Californie"; /* Name for the status of a post that is marked private. - Privacy setting for posts set to 'Private'. Should be the same as in core WP. - Title of the Private Badge */ + Privacy setting for posts set to 'Private'. Should be the same as in core WP. */ "Private" = "Prive"; /* No comment provided by engineer. */ @@ -5891,12 +5707,10 @@ translators: %s: Select control button label e.g. \"Button width\" */ Label for the publish date button. */ "Publish Date" = "Publicatiedatum"; -/* A short phrase indicating a post is due to be immedately published. - Label for a button that moves a page to the published folder, publishing with the current date/time. */ +/* A short phrase indicating a post is due to be immedately published. */ "Publish Immediately" = "Direct publiceren"; /* Label for a button that publishes the post - Label for an option that moves a publishes a post immediately Title of button allowing the user to immediately publish the post they are editing. */ "Publish Now" = "Nu publiceren"; @@ -5914,8 +5728,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Name for the status of a published post. Period Stats 'Published' header - Title of the published filter. This filter shows a list of posts that the user has published. - Title of the published header in search list. */ + Title of the published filter. This filter shows a list of posts that the user has published. */ "Published" = "Gepubliceerd"; /* Precedes the name of the blog just posted on */ @@ -6057,8 +5870,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of the completion screen of the Blogging Reminders Settings screen when the reminders are removed. */ "Reminders removed" = "Herinneringen verwijderd"; -/* Add asset to media picker list - Alert button to confirm a plugin to be removed +/* Alert button to confirm a plugin to be removed Button label when removing a blog Label for the Remove Feature Image icon. Tapping will show a confirmation screen for removing the feature image from the post. Remove Action @@ -6211,9 +6023,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of secondary button on alert prompting verify their accounts while attempting to publish */ "Resend" = "Verstuur opnieuw"; -/* Title of the reset button */ -"Reset" = "Opnieuw instellen"; - /* Accessibility label for the reset activity type button */ "Reset Activity Type filter" = "Herstel activiteit type filter"; @@ -6268,12 +6077,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ Button title, displayed when media has failed to upload. Allows the user to try the upload again. Button title. Retries uploading a post. If a user taps the button with this label, the action that evinced this error view will be retried. - Label for a button that attempts to re-upload a page that previously failed to upload. - Label for the retry post upload button. Tapping attempts to upload the post again. Opens the media library . Retries the upload of a user's post. Retry updating User's Role - Retry uploading the post. Retry. Action Retry. Verb – retry a failed media upload. The Jetpack view button title used when an error occurred @@ -6285,9 +6091,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Button title that triggers a scan */ "Retry Scan" = "Scan opnieuw proberen"; -/* User action to retry media upload. */ -"Retry Upload" = "Upload opnieuw proberen"; - /* User action to retry all failed media uploads. */ "Retry all" = "Probeer alles opnieuw"; @@ -6385,9 +6188,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Accessibility label for the 'Save Post' button when a post has been saved. */ "Saved Post" = "Opgeslagen bericht"; -/* Text displayed in HUD when a media item's metadata (title, etc) is saved successfully. */ -"Saved!" = "Opgeslagen!"; - /* Accessibility hint for the 'Save Post' button. */ "Saves this post for later." = "Bewaart dit bericht voor later."; @@ -6398,7 +6198,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Saving post…" = "Bericht bewaren…"; /* Menus save button title while it is saving a Menu. - Text displayed in HUD while a media item's metadata (title, etc) is being saved. Text displayed in HUD while a post is being saved as a draft. */ "Saving..." = "Opslaan..."; @@ -6489,21 +6288,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* No comment provided by engineer. */ "Search or type URL" = "Zoek of typ URL"; -/* Text displayed when the search controller will be presented */ -"Search pages" = "Zoek pagina's"; - -/* Text displayed when the search controller will be presented */ -"Search posts" = "Berichten zoeken"; - /* No comment provided by engineer. */ "Search settings" = "Zoek instellingen"; -/* Title for placeholder in Tenor picker */ -"Search to find GIFs to add to your Media Library!" = "Zoek naar GIF's om aan je mediabibliotheek toe te voegen!"; - -/* Title for placeholder in Free Photos */ -"Search to find free photos to add to your Media Library!" = "Zoek gratis foto's om toe te voegen aan je mediabibliotheek!"; - /* Menus search bar placeholder text. */ "Search..." = "Zoeken..."; @@ -6574,9 +6361,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Register Domain - Domain contact information field placeholder for Country */ "Select Country" = "Selecteer land"; -/* Title of button that allows the user to select more photos to access within the app */ -"Select More" = "Meer selecteren"; - /* Blog Picker's Title */ "Select Site" = "Site selecteren"; @@ -6598,9 +6382,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Register domain - Title for the Choose domain button of Suggested domains screen */ "Select domain" = "Selecteer domein"; -/* Accessibility hint for actions when displaying media items. */ -"Select media." = "Selecteer media."; - /* Accessibility label for selecting paragraph style button on formatting toolbar. */ "Select paragraph style" = "Alineastijl selecteren"; @@ -6704,19 +6485,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label for connected service in Publicize stat. */ "Service" = "Dienst"; -/* Label for a button that opens the Set Parent options view controller - Navigation title displayed on the navigation bar */ +/* Navigation title displayed on the navigation bar */ "Set Parent" = "Instellen als bovenliggend item"; /* No comment provided by engineer. */ "Set as Featured Image" = "Instellen als uitgelichte afbeelding"; -/* Label for a button that sets the selected page as the site's Homepage */ -"Set as Homepage" = "Instellen als homepage"; - -/* Label for a button that sets the selected page as the site's Posts page */ -"Set as Posts Page" = "Instellen als berichten pagina"; - /* Notice confirming that an image has been set as the post's featured image. */ "Set as featured image" = "Instellen als uitgelichte afbeelding"; @@ -6760,7 +6534,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Accessibility label for share buttons in nav bars Button label to share a post Button label to share a web page - Share the post. Shares the comment URL Spoken accessibility label Title for a button that allows the user to share their answer to the prompt. @@ -7146,8 +6919,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Name of setting configured when a site uses a static page as its homepage */ "Static Homepage" = "Statische homepage"; -/* Label for post stats option. Tapping displays statistics for a post. - Noun. Abbreviation of Statistics. Name of the Stats feature +/* Noun. Abbreviation of Statistics. Name of the Stats feature Noun. Abbv. of Statistics. Links to a blog's Stats screen. Stats 3D Touch Shortcut Stats window title @@ -7178,9 +6950,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label text that defines a post marked as sticky */ "Sticky" = "Sticky"; -/* Accessibility label for a sticky post in the post list. */ -"Sticky." = "Sticky."; - /* User action to stop upload. */ "Stop upload" = "Stop upload"; @@ -7237,7 +7006,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ Theme Support action title */ "Support" = "Ondersteuning"; -/* Button used to switch site */ +/* Title for back button that leads to the site picker screen. */ "Switch Site" = "Site wisselen"; /* Switches the Editor to HTML Mode */ @@ -7322,9 +7091,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label explaining why users might want to add tags. */ "Tags help tell readers what a post is about. Separate different tags with commas." = "Tags vertellen een bezoeker waar een bericht over gaat. Scheid verschillende tags met komma's."; -/* Menu option for taking an image or video with the device's camera. */ -"Take Photo or Video" = "Neem foto of video"; - /* No comment provided by engineer. */ "Take a Photo" = "Neem een foto"; @@ -7395,12 +7161,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Accessibility hint */ "Tap to select the previous period" = "Tik om de vorige periode te selecteren"; -/* Accessibility hint for button used to switch site */ -"Tap to switch to another site, or add a new site" = "Tik om te wisselen naar een andere site, of voeg een nieuwe site toe"; - -/* Accessibility hint for media item preview for user's viewing an item in their media library */ -"Tap to view media in full screen" = "Klik om media op volledig scherm te bekijken"; - /* Accessibility hint for a button that opens a new view with more details. */ "Tap to view more details." = "Tik hier voor meer informatie."; @@ -7446,10 +7206,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* No comment provided by engineer. */ "Text formatting controls are located within the toolbar positioned above the keyboard while editing a text block" = "De tekstopmaak besturingen bevinden zich in de werkbalk boven het toetsenbord tijdens het bewerken van een tekstblok"; -/* Button title - The button's title text to send a 2FA code via SMS text message. */ +/* Button title */ "Text me a code instead" = "SMS een code naar me toe"; +/* The button's title text to send a 2FA code via SMS text message. */ +"Text me a code via SMS" = "Stuur me een code via sms"; + /* Message of alert when theme activation succeeds */ "Thanks for choosing %@ by %@" = "Bedankt voor het kiezen van %1$@ door %2$@"; @@ -7477,9 +7239,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Error message shown to a user who is trying to share to Facebook but does not have any available Facebook Pages. */ "The Facebook connection cannot find any Pages. Publicize cannot connect to Facebook Profiles, only published Pages." = "De Facebook verbinding kan geen pagina's vinden. Publiceren kan niet verbinden met Facebook profielen, alleen met gepubliceerde pagina's."; -/* Message shown when a GIF failed to load while trying to add it to the Media library. */ -"The GIF could not be added to the Media Library." = "De GIF kon niet worden toegevoegd aan de mediabibliotheek."; - /* Description shown when a user logs in with Google but no matching WordPress.com account is found */ "The Google account \"%@\" doesn't match any account on WordPress.com" = "Het Google account \"%@\" komt niet overeen met een account op WordPress.com"; @@ -7607,7 +7366,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Error message shown when user attempts to remove the site owner. */ "The user you are trying to remove is the owner of this site. Please contact support for assistance." = "De gebruiker die je wilt verwijderen is de eigenaar van de site. Neem contact met ondersteuning op voor assistentie."; -/* Error message informing a user about an invalid password. */ +/* No comment provided by engineer. */ "The username or password stored in the app may be out of date. Please re-enter your password in the settings and try again." = "De gebruiker of wachtwoord opgeslagen in de app kunnen verouderd zijn. Voer alsjeblieft je wachtwoord in bij instellingen en probeer het opnieuw."; /* Message shown when a video failed to load while trying to add it to the Media library. */ @@ -7675,9 +7434,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* A short error message letting the user know about a problem displaying a post. */ "There was a problem displaying this post." = "Er is een probleem opgetreden met de weergave van dit bericht."; -/* Error message displayed when the Media Library is unable to load a full sized preview of an item. */ -"There was a problem loading the media item." = "Niet gelukt om het media-item te laden."; - /* The loading view subtitle displayed when an error occurred */ "There was a problem loading your data, refresh your page to try again." = "Er is een probleem opgetreden bij het laden van je gegevens. Vernieuw je pagina om het opnieuw te proberen."; @@ -7690,9 +7446,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Explaining to the user there was an error trying to obtain the current location of the user. */ "There was a problem when trying to access your location. Please try again later." = "Er is een probleem opgetreden bij het ophalen van je locatie. Probeer het later opnieuw."; -/* Explaining to the user there was an generic error accesing media. */ -"There was a problem when trying to access your media. Please try again later." = "Er is een probleem opgetreden bij het openen van je media. Probeer het later opnieuw."; - /* Message for stories unknown error. */ "There was a problem with the Stories editor. If the problem persists you can contact us via the Me > Help & Support screen." = "Er was een probleem met de verhaal editor. Als het probleem zich blijft voordoen, kun je contact met ons opnemen via het scherm Mij > Help & Support."; @@ -7763,9 +7516,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* A description informing the user in order to proceed with this feature we will need camera permissions, and how to enable it. */ "This app needs permission to access the Camera to scan login codes, tap on the Open Settings button to enable it." = "Je moet deze app toegang tot je camera geven om inlogcodes te scannen. Tik op 'Instellingen openen' om de toegang te verlenen."; -/* Explaining to the user why the app needs access to the device media library. */ -"This app needs permission to access your device media library in order to add photos and\/or video to your posts. Please change the privacy settings if you wish to allow this." = "Deze app heeft toestemming nodig om de mediabibliotheek van je apparaat te kunnen openen voor het toevoegen van foto's en\/of video's aan je berichten. Wijzig de privacyinstellingen als je dit wilt toestaan."; - /* No comment provided by engineer. */ "This color combination may be hard for people to read. Try using a brighter background color and\/or a darker text color." = "Deze kleurencombinatie is mogelijk lastig te lezen. Gebruik een lichtere achtergrondkleur en\/of een donkerdere tekstkleur."; @@ -7875,6 +7625,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Description of a Quick Start Tour */ "Time to finish setting up your site! Our checklist walks you through the next steps." = "Tijd om je site af te ronden! Onze checklist leidt je door de volgende stappen."; +/* Error when the uses takes more than 1 minute to submit a security key. */ +"Time's up, but don't worry, your security is our priority. Please try again!" = "De tijd is om, maar maak je geen zorgen, jouw beveiliging is van het hoogste belang. Probeer het nog eens!"; + /* WordPress.com Marketing Footer Text */ "Tips for getting the most out of WordPress.com." = "Tips om WordPress.com optimaal te benutten."; @@ -7998,24 +7751,20 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title for the traffic section in site settings screen */ "Traffic" = "Verkeer"; +/* Describes a domain that was transferred from elsewhere to wordpress.com */ +"Transferred Domain" = "Domein overzetten"; + /* translators: %s: block title e.g: \"Paragraph\". */ "Transform %s to" = "Transformeer %s naar"; /* No comment provided by engineer. */ "Transform block…" = "Transformeer blok..."; -/* Accessibility label for trash button to delete items from the user's media library - Accessibility label for trash buttons in nav bars +/* Accessibility label for trash buttons in nav bars Trashes a comment Trashes the comment */ "Trash" = "Prullenmand"; -/* Accessibility hint for trash button to delete items from the user's media library */ -"Trash selected media" = "Verplaats geselecteerde media naar prullenbak"; - -/* Title of the trash page confirmation alert. */ -"Trash this page?" = "Deze pagina naar prullenbak verplaatsen?"; - /* Title of the trash confirmation alert. */ "Trash this post?" = "Bericht naar prullenbak verplaatsen?"; @@ -8133,9 +7882,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Shown when a user logs in with Google but it subsequently fails to work as login to WordPress.com */ "Unable To Connect" = "Verbinding niet mogelijk"; -/* An error message. */ -"Unable to Connect" = "Kan geen verbinding maken"; - /* Title for stories unknown error. */ "Unable to Create Stories Editor" = "Kan geen verhalen-editor maken"; @@ -8151,9 +7897,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* An error message shown when there is an issue creating new invite links. */ "Unable to create new invite links." = "Het is niet mogelijk om nieuwe uitnodigingslinks te maken."; -/* Text displayed in HUD if there was an error attempting to delete a group of media items. */ -"Unable to delete all media items." = "Niet mogelijk om alle media te verwijderen."; - /* Text displayed in HUD if there was an error attempting to delete a media item. */ "Unable to delete media item." = "Niet mogelijk om media te verwijderen."; @@ -8217,12 +7960,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Message displayed when sharing a link to the downloadable backup fails. */ "Unable to share link" = "Kan de link niet delen"; -/* Message that appears when a user tries to trash a page while their device is offline. */ -"Unable to trash pages while offline. Please try again later." = "Onmogelijk om pagina's naar de prullenbak te verwijderen als je offline bent. Probeer het later nog eens."; - -/* Message that appears when a user tries to trash a post while their device is offline. */ -"Unable to trash posts while offline. Please try again later." = "Onmogelijk om berichten te verwijderen wanneer je offline bent. Probeer later opnieuw."; - /* Notice title when turning site notifications off fails. */ "Unable to turn off site notifications" = "Kan de site meldingen niet uitschakelen"; @@ -8295,8 +8032,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ Button title. Reverts the previous notification operation Revert an operation Revert enabling notification after successfully subcribing to the comments for the post. - The title of an 'undo' button. Tapping the button moves a trashed page out of the trash folder. - The title of an 'undo' button. Tapping the button moves a trashed post out of the trash folder. Undo action */ "Undo" = "Ongedaan maken"; @@ -8339,9 +8074,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title for Unknown HTML Editor */ "Unknown HTML" = "Onbekende HTML"; -/* Label to use when creation date from media asset is not know. */ -"Unknown creation date" = "Onbekende aanmaakdatum"; - /* No comment provided by engineer. */ "Unknown error" = "Onbekende fout"; @@ -8507,6 +8239,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of a row displayed on the debug screen used to configure the sandbox store use in the App. */ "Use Sandbox Store" = "Gebruik Sandbox Store"; +/* The button's title text to use a security key. */ +"Use a security key" = "Beveiligingssleutel"; + /* Option to enable the block editor for new posts */ "Use block editor" = "Gebruik blok-editor"; @@ -8582,15 +8317,10 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of an alert informing users that the video they are trying to select is not allowed. */ "Video not uploaded" = "Video niet geüpload"; -/* Accessibility label for video thumbnails in the media collection view. The parameter is the creation date of the video. */ -"Video, %@" = "Video, %@"; - /* Period Stats 'Videos' header */ "Videos" = "Video's"; /* Button title. Displays a summary / sharing screen for a specific post. - Label for a button that opens the page when tapped. - Label for the view post button. Tapping displays the post as it appears on the web. Opens the post epilogue screen to allow sharing / viewing of a post. Theme View action title Verb. The screen title shown when viewing a post inside the app. */ @@ -9076,6 +8806,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* An error message shown when a wpcom user provides the wrong password. */ "Whoops, something went wrong and we couldn't log you in. Please try again!" = "Er ging iets mis en we konden je niet inloggen. Probeer nogmaals!"; +/* Generic error on the 2FA screen */ +"Whoops, something went wrong. Please try again!" = "Oeps, er is iets fout gegaan. Probeer het nog eens!"; + +/* Error when the uses chooses an invalid security key on the 2FA screen. */ +"Whoops, that security key does not seem valid. Please try again with another one" = "Oeps, het lijkt erop dat die beveiligingssleutel niet geldig is. Probeer het opnieuw met een andere"; + /* Error message shown when an incorrect two factor code is provided. */ "Whoops, that's not a valid two-factor verification code. Double-check your code and try again!" = "Dat is geen geldige twee-factor verificatiecode. Controleer de code en probeer nogmaals!"; @@ -9103,9 +8839,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Siri Suggestion to open Support */ "WordPress Help" = "WordPress ondersteuning"; -/* Name for the WordPress Media Library */ -"WordPress Media" = "WordPress media"; - /* Accessibility label for selecting an image or video from the user's WordPress media library on formatting toolbar. */ "WordPress Media Library" = "WordPress mediabibliotheek"; @@ -9420,9 +9153,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Share extension error dialog text. */ "Your account does not have permission to upload media to this site. The Site Administrator can change these permissions." = "Jouw account heeft geen rechten om media naar deze site te uploaden. De sitebeheerder kan deze rechten aanpassen."; -/* Explaining to the user why the app needs access to the device media library. */ -"Your app is not authorized to access media library due to active restrictions such as parental controls. Please check your parental control settings in this device." = "Je app is niet geautoriseerd om de mediabibliotheek te benaderen vanwege actieve restricties zoals ouderlijk toezicht. Controleer de instellingen voor je ouderlijk toezicht op dit apparaat."; - /* Title for the Jetpack Backup Complete message. */ "Your backup is now available for download" = "Je back-up is nu beschikbaar om te downloaden"; @@ -9441,9 +9171,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of the site address section in the Domains Dashboard. */ "Your free WordPress.com address is" = "Je gratis WordPress.com adres is"; -/* Error message when picked media cannot be imported into stories. */ -"Your media could not be exported. If the problem persists you can contact us via the Me > Help & Support screen." = "Je media kunnen niet worden geëxporteerd. Als het probleem aanhoudt, kun je contact met ons opnemen via het scherm Ik> Help & Ondersteuning."; - /* Details about recently acquired domain on domain credit redemption success screen */ "Your new domain %@ is being set up. It may take up to 30 minutes for your domain to start working." = "Je nieuwe domein %@ wordt ingesteld. Het kan tot 30 minuten duren voor je domein werkend is."; @@ -9567,8 +9294,23 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* This is the string we display when prompting the user to review the WordPress app */ "appRatings.wordpress.prompt" = "Wat vind je van WordPress?"; -/* Label displayed on audio media items. */ -"audio" = "audio"; +/* Option to enable the optimization of images when uploading. */ +"appSettings.media.imageOptimizationRow" = "Afbeeldingen optimaliseren"; + +/* Indicates an image will use maximum quality when uploaded. */ +"appSettings.media.imageQuality.maximum" = "Maximaal"; + +/* Message of an alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.message" = "Afbeeldingoptimalisatie maakt afbeeldingen kleiner, zodat ze sneller geüpload kunnen worden.\n\nThis option is enabled by default, but you can change it in the app settings at any time."; + +/* Title of an alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.title" = "Keep optimizing images?"; + +/* Title of button for turning off image optimization, displayed in the alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.turnOff" = "No, turn off"; + +/* Title of button for leaving on image optimization, displayed in the alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.turnOn" = "Yes, leave on"; /* translators: displays audio file extension. e.g. MP3 audio file */ "audio file" = "audiobestand"; @@ -9594,6 +9336,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title displayed when there are no Blaze campaigns to display. */ "blaze.campaigns.empty.title" = "Je hebt geen campagnes"; +/* Text displayed when there is a failure loading Blaze campaigns. */ +"blaze.campaigns.errorMessage" = "Er is een fout opgetreden bij het laden van campagnes."; + /* Displayed while Blaze campaigns are being loaded. */ "blaze.campaigns.loading.title" = "Campagnes aan het inladen ..."; @@ -9676,7 +9421,38 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "blogHeader.actionCopyURL" = "URL kopiëren"; /* Context menu button title */ -"blogHeader.actionOpenInBrowser" = "Openen in browser"; +"blogHeader.actionVisitSite" = "Site bekijken"; + +/* Title for a button that, when tapped, shows more info about participating in Bloganuary. */ +"bloganuary.dashboard.card.button.learnMore" = "Meer informatie"; + +/* Title for the Bloganuary dashboard card while Bloganuary is running. */ +"bloganuary.dashboard.card.runningTitle" = "Bloganuary is aangebroken!"; + +/* Title of a button that calls the user to enable the Blogging Prompts feature. */ +"bloganuary.learnMore.modal.button.promptsDisabled" = "Blogmeldingen inschakelen"; + +/* Title of a button that will dismiss the Bloganuary modal when tapped. +Note that the word 'go' here should have a closer meaning to 'start' rather than 'move forward'. */ +"bloganuary.learnMore.modal.button.promptsEnabled" = "Aan de slag."; + +/* The second line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.description.second" = "Je antwoord publiceren."; + +/* The third line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.description.third" = "Lees de antwoorden van andere bloggers ter inspiratie en om nieuwe contacten te leggen."; + +/* The first line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.descriptions.first" = "Krijg elke dag een nieuwe melding als bron van inspiratie."; + +/* An additional piece of information shown in case the user has the Blogging Prompts feature disabled. */ +"bloganuary.learnMore.modal.footer.addition" = "Je moet Blogmeldingen inschakelen om je aan te kunnen melden voor Bloganuary."; + +/* An informative excerpt shown in a subtler tone. */ +"bloganuary.learnMore.modal.footer.text" = "Bloganuary gebruikt Dagelijkse blogmeldingen om je in de maand januari onderwerpen toe te sturen."; + +/* The headline text of the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.headline" = "Doe mee aan onze schrijfchallenge van een maand."; /* Verb. Dismisses the blogging prompt notification. */ "bloggingPrompt.pushNotification.customActionDescription.dismiss" = "Negeren"; @@ -9705,6 +9481,9 @@ Example: Comment on Example: Reply to Pamela Nguyen */ "comment.header.subText.reply" = "Reageer op %1$@"; +/* Error message informing a user about an invalid password. */ +"common.reEnterPasswordMessage" = "Gebruikersnaam en wachtwoord die in de app zijn opgeslagen zijn wellicht verouderd. Voer je wachtwoord opnieuw in bij de instellingen en probeer het nogmaals."; + /* Footnote for the privacy compliance popover. */ "compliance.analytics.popover.footnote" = "Met deze cookies kunnen wij prestaties optimaliseren door informatie te verzamelen over hoe gebruikers zich gedragen op onze website."; @@ -9837,6 +9616,69 @@ Example: Reply to Pamela Nguyen */ /* Title for a menu action in the context menu on the Jetpack install card. */ "domain.dashboard.card.menu.hide" = "Dit verbergen"; +/* Search domain - Title for the Suggested domains screen */ +"domain.management.addDomain.search.title" = "Een domein zoeken"; + +/* Domain management buy domain card button title */ +"domain.management.buy.card.button.title" = "Neem een domein"; + +/* Domain management buy domain card subtitle */ +"domain.management.buy.card.subtitle" = "Later een site toevoegen."; + +/* Domain management buy domain card title */ +"domain.management.buy.card.title" = "Koop gewoon een domein"; + +/* The empty state button title in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.button.title" = "Een domein vinden"; + +/* The empty state description in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.description" = "Tik hieronder om jouw ideale domein te vinden."; + +/* The empty state description in All Domains screen when an error occurs */ +"domain.management.error.empty.state.description" = "We encountered an error while loading your domains. Neem contact op met het ondersteunend personeel als het probleem nog niet is opgelost."; + +/* The empty state title in All Domains screen when an error occurs */ +"domain.management.error.empty.state.title" = "Er is iets fout gegaan"; + +/* The empty state button title in All Domains screen when an error occurs */ +"domain.management.error.state.button.title" = "Opnieuw proberen"; + +/* The empty state description in All Domains screen when the user is offline */ +"domain.management.offline.empty.state.description" = "Controleer je verbinding en probeer het nogmaals."; + +/* The empty state title in All Domains screen when the user is offline */ +"domain.management.offline.empty.state.title" = "Geen internetverbinding."; + +/* Domain management choose site card button title */ +"domain.management.purchase.footer" = "*Bij alle betaalde jaarabonnementen is een gratis domein voor een jaar inbegrepen."; + +/* Domain management purchase domain screen title. */ +"domain.management.purchase.title" = "Kies hoe je jouw domein gebruikt"; + +/* The search bar title in All Domains screen. */ +"domain.management.search-bar.title" = "Domeinen zoeken"; + +/* The empty state description in All Domains screen when the are no domains matching the search criteria */ +"domain.management.search.empty.state.description" = "We couldn't find any domains that match your search for '%@'"; + +/* The empty state title in All Domains screen when the are no domains matching the search criteria */ +"domain.management.search.empty.state.title" = "Geen overeenkomstige domeinen."; + +/* Domain management choose site card button title */ +"domain.management.site.card.button.title" = "Site kiezen"; + +/* Domain management choose site card subtitle */ +"domain.management.site.card.footer" = "Een gratis domein voor het eerste jaar"; + +/* Domain management choose site card subtitle */ +"domain.management.site.card.subtitle" = "Gebruik met een site die je al hebt aangemaakt."; + +/* Domain management choose site card title */ +"domain.management.site.card.title" = "Bestaande WordPress.com-site"; + +/* Domain Management Screen Title */ +"domain.management.title" = "Alle domeinen"; + /* Domain Purchase Completion footer */ "domain.purchase.preview.footer" = "Het kan tot 30 minuten duren voor je aangepaste domein werkt."; @@ -9849,21 +9691,6 @@ Example: Reply to Pamela Nguyen */ /* Reflects that site is live when domain purchase feature flag is ON. */ "domain.purchase.preview.title" = "Gefeliciteerd, je site is online!"; -/* Status of a domain in `Complete Setup` state */ -"domain.status.complete.setup" = "Configuratie voltooien"; - -/* Status of a domain in `Expiring Soon` state */ -"domain.status.expiring.soon" = "Verloopt binnenkort"; - -/* Status of a domain in `In Progress` state */ -"domain.status.in.progress" = "Nog steeds bezig"; - -/* Status of a domain in `Renew` state */ -"domain.status.renew" = "Verlengen"; - -/* Status of a domain in `Verify Email` state */ -"domain.status.verify.email" = "E-mail verifiëren"; - /* The 'Best Alternative' label under the domain name in 'Choose a domain' screen */ "domain.suggestions.row.best-alternative" = "Beste alternatief"; @@ -9908,6 +9735,9 @@ Example: Reply to Pamela Nguyen */ Site Address placeholder */ "example.com" = "voorbeeld.nl"; +/* Bottom toolbar title in the selection mode */ +"externalMediaPicker.toolbarViewSelected" = "Geselecteerde weergeven (%@)"; + /* Title of screen the displays the details of an advertisement campaign. */ "feature.blaze.campaignDetails.title" = "Campagnedetails"; @@ -10004,9 +9834,6 @@ Example: Reply to Pamela Nguyen */ /* (placeholder) Help the user enter a URL into the field */ "http:\/\/my-site-address (URL)" = "http:\/\/mijn-site-adres (URL)"; -/* Label displayed on image media items. */ -"image" = "afbeelding"; - /* Sentence to justify why the app is asking permission from the user to use their camera. */ "infoplist.NSCameraUsageDescription" = "Voor het maken van foto's of video's die gebruikt kunnen worden in je berichten."; @@ -10316,12 +10143,15 @@ Example: Reply to Pamela Nguyen */ /* An error message the app shows if media import fails */ "mediaExporter.error.unsupportedContentType" = "HTTP inhoudstype niet ondersteund"; -/* Verb. User action to dismiss error alert when failing to load media item. */ -"mediaItemTable.errorAlert.dismissButton" = "Negeren"; - /* Accessibility label for add button to add items to the user's media library */ "mediaLibrary.addButtonAccessibilityLabel" = "Voeg toe"; +/* Button name in the more menu */ +"mediaLibrary.aspectRatioGrid" = "Aspect ratio:"; + +/* Context menu button */ +"mediaLibrary.buttonDelete" = "Verwijderen"; + /* Media screen navigation bar button Select title */ "mediaLibrary.buttonSelect" = "Selecteer"; @@ -10346,9 +10176,21 @@ Example: Reply to Pamela Nguyen */ /* Message displayed when no results are returned from a media library search. Should match Calypso. */ "mediaLibrary.searchResultsEmptyTitle" = "Er komt geen media overeen met je zoekopdracht"; +/* Text displayed in HUD if there was an error attempting to share a group of media items. */ +"mediaLibrary.sharingFailureMessage" = "Unable to share the selected items."; + +/* Button name in the more menu */ +"mediaLibrary.squareGrid" = "Square Grid"; + /* The title of the button to dismiss the alert shown when the picked media cannot be imported into stories. */ "mediaPicker.failedMediaExportAlert.dismissButton" = "Negeren"; +/* Error message when picked media cannot be imported into stories. */ +"mediaPicker.failedMediaExportAlert.message" = "Je media kon niet worden geëxporteerd Help & Support-scherm. Als het probleem aanhoudt, kun je contact met ons opnemen via het scherm Ik > Help & ondersteuning."; + +/* Error title when picked media cannot be imported into stories. */ +"mediaPicker.failedMediaExportAlert.title" = "Media export mislukt"; + /* Message for alert when access to camera is not granted */ "mediaPicker.noCameraAccessMessage" = "Deze app heeft toestemming nodig om de camera te openen voor het registreren van nieuwe media; wijzig de privacyinstellingen als je dit wilt toestaan."; @@ -10361,9 +10203,15 @@ Example: Reply to Pamela Nguyen */ /* The name of the action in the context menu */ "mediaPicker.pickFromPhotosLibrary" = "\"Kiezen op apparaat\""; +/* The name of the action in the context menu */ +"mediaPicker.takePhoto" = "Maak een foto"; + /* The name of the action in the context menu */ "mediaPicker.takeVideo" = "Video maken"; +/* Max image size in pixels (e.g. 300x300px) */ +"mediaSizeSlider.valueFormat" = "px.%1$d%2$d"; + /* The description in the Delete WordPress screen */ "migration.deleteWordpress.description" = "Het lijkt erop dat je de WordPress app nog steeds geïnstalleerd hebt."; @@ -10376,9 +10224,6 @@ Example: Reply to Pamela Nguyen */ /* The title in the Delete WordPress screen */ "migration.deleteWordpress.title" = "Je hebt de WordPress app niet meer nodig op je apparaat"; -/* Primary button title in the migration done screen. */ -"migration.done.actions.primary.title" = "Afronden"; - /* Footer for the migration done screen. */ "migration.done.footer" = "We raden je aan om de WordPress app van je apparaat te verwijderen om gegevens conflicten te vermijden."; @@ -10388,6 +10233,9 @@ Example: Reply to Pamela Nguyen */ /* Primary description in the migration done screen. */ "migration.done.primaryDescription" = "We hebben al je gegevens en instellingen overgedragen. Alles staat precies waar je het gelaten hebt."; +/* Secondary description (second paragraph) in the migration done screen. */ +"migration.done.secondaryDescription" = "It's time to continue your WordPress journey on the Jetpack app!"; + /* Title of the migration done screen. */ "migration.done.title" = "Bedankt dat je bent overgestapt naar Jetpack!"; @@ -10436,6 +10284,9 @@ Example: Reply to Pamela Nguyen */ /* The title in the migration welcome screen */ "migration.welcome.title" = "Welkom bij Jetpack!"; +/* Primary button title in the migration done screen. */ +"migrationDone.actions.primaryTitle" = "Aan de slag"; + /* Description for the static screen displayed prompting users to switch the Jetpack app. */ "movedToJetpack.description" = "De Jetpack-app heeft alle functies van de WordPress-app, en nu ook exclusieve toegang tot Statistieken, Reader, Meldingen en meer."; @@ -10502,6 +10353,24 @@ Example: Reply to Pamela Nguyen */ /* Message title for when a user has no sites. */ "mySite.noSites.title" = "Je hebt geen sites"; +/* Button that reveals more site actions */ +"mySite.siteActions.button" = "Siteacties"; + +/* Accessibility hint for button used to show more site actions */ +"mySite.siteActions.hint" = "Tik om meer siteactie weer te geven"; + +/* Menu title for the personalize home option */ +"mySite.siteActions.personalizeHome" = "Startpagina personaliseren"; + +/* Menu title for the change site icon option */ +"mySite.siteActions.siteIcon" = "Favicon wijzigen"; + +/* Menu title for the change site title option */ +"mySite.siteActions.siteTitle" = "Sitetitel wijzigen"; + +/* Menu title for the visit site option */ +"mySite.siteActions.visitSite" = "Site bekijken"; + /* Dismiss button title. */ "noResultsViewController.dismissButton" = "Negeren"; @@ -10517,14 +10386,11 @@ Example: Reply to Pamela Nguyen */ /* This is one of the buttons we display when prompting the user for a review */ "notifications.appRatings.sendFeedback.yes.buttonTitle" = "Stuur feedback"; -/* Word separating the current index from the total amount. I.e.: 7 of 9 */ -"of" = "van"; +/* Badge for page cells */ +"pageList.badgeHomepage" = "Startpagina"; -/* Label displayed on media items that are not video, image, or audio. */ -"other" = "andere"; - -/* Promote the page with Blaze. */ -"pages.blaze.actionTitle" = "Promoten met Blaze"; +/* Badge for page cells */ +"pageList.badgePendingReview" = "Beoordeling in behandeling"; /* Subtitle of the theme template homepage cell */ "pages.template.subtitle" = "Jouw homepage gebruikt een themasjabloon en wordt in de webeditor geopend."; @@ -10532,12 +10398,30 @@ Example: Reply to Pamela Nguyen */ /* Title of the theme template homepage cell */ "pages.template.title" = "Startpagina"; +/* Message informing the user that their static homepage page was set successfully */ +"pages.updatePage.successTitle" = "Pagina is bijgewerkt."; + +/* Menu option for filtering posts by everyone */ +"pagesList.pagesByEveryone" = "Pages by everyone"; + +/* Menu option for filtering posts by me */ +"pagesList.pagesByMe" = "Pages by me"; + +/* Trash option in the trash page confirmation alert. */ +"pagesList.trash.actionTitle" = "Verplaatsen naar prullenbak"; + +/* Message of the trash page confirmation alert. */ +"pagesList.trash.alertMessage" = "Weet je zeker dat je deze pagina naar de prullenbak wilt verplaatsen?"; + /* No comment provided by engineer. */ "password" = "wachtwoord"; /* Section footer displayed below the list of toggles */ "personalizeHome.cardsSectionFooter" = "Kaarten kunnen verschillende inhoud weergeven, afhankelijk van wat er op je site gebeurt. We zijn bezig meer kaarten en beheermogelijkheden te creëren."; +/* Section header */ +"personalizeHome.cardsSectionHeader" = "Kaarten toevoegen of verbergen"; + /* Card title for the pesonalization menu */ "personalizeHome.dashboardCard.activityLog" = "Recente activiteit"; @@ -10565,6 +10449,30 @@ Example: Reply to Pamela Nguyen */ /* Register Domain - Domain contact information field Phone */ "phone number" = "Telefoonnummer"; +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.createdTimeAgo" = "%@ aangemaakt"; + +/* Status mesasge for post cells */ +"post.deletingPostPermanentlyStatusMessage" = "Bericht verwijderen"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.editedTimeAgo" = "Edited %@"; + +/* Status mesasge for post cells */ +"post.movingToTrashStatusMessage" = "Bezig met verplaatsen naar de prullenbak …"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.publishedTimeAgo" = "%@ gepubliceerd"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.scheduledForDate" = "%@ gepland"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.trashedTimeAgo" = "%@ verplaatst naar prullenbak"; + +/* Title for the 'View' post list row swipe action */ +"postList.swipeActionView" = "Weergave"; + /* User action to dismiss featured media options. */ "postSettings.featuredImageUploadActionSheet.dismiss" = "Negeren"; @@ -10583,9 +10491,48 @@ Example: Reply to Pamela Nguyen */ /* Button in Post Settings */ "postSettings.setFeaturedImageButton" = "Stel uitgelichte afbeelding in"; +/* Error message on post/page settings screen */ +"postSettings.updateFailedMessage" = "Kan de meldingsinstellingen niet bijwerken."; + /* Promote the post with Blaze. */ "posts.blaze.actionTitle" = "Promoten met Blaze"; +/* Label for the Post List option that cancels automatic uploading of a post. */ +"posts.cancelUpload.actionTitle" = "Uploaden annuleren"; + +/* Opens a submenu for page attributes. */ +"posts.pageAttributes.actionTitle" = "Pagina-eigenschappen"; + +/* Label for an option that moves a publishes a post immediately */ +"posts.publish.actionTitle" = "Nu publiceren"; + +/* Retry uploading the post. */ +"posts.retry.actionTitle" = "Opnieuw proberen"; + +/* Set the parent page for the selected page. */ +"posts.setParent.actionTitle" = "Instellen als bovenliggend item"; + +/* Set the selected page as a posts page. */ +"posts.setPostsPage.actionTitle" = "Stel je berichtenpagina in"; + +/* Label for a option that moves a post to the trash folder */ +"posts.trash.actionTitle" = "Verplaatsen naar prullenbak"; + +/* Label for the view post button. Tapping displays the post as it appears on the web. */ +"posts.view.actionTitle" = "Weergave"; + +/* A short message explaining that a page was deleted permanently. */ +"postsList.deletePage.message" = "Beoordeling permanent verwijderd."; + +/* A short message explaining that a post was deleted permanently. */ +"postsList.deletePost.message" = "Beoordeling permanent verwijderd."; + +/* Menu option for filtering posts by everyone */ +"postsList.postsByEveryone" = "Posts by everyone"; + +/* Menu option for filtering posts by me */ +"postsList.postsByMe" = "Posts by me"; + /* Title for the button to subscribe to Jetpack Social on the remaining shares view */ "postsettings.social.remainingshares.subscribe" = "Neem nu een abonnement om meer te delen"; @@ -10681,13 +10628,15 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Accessibility hint for the comment button on the reader post card cell */ "reader.post.button.comment.accessibility.hint" = "Opent de reacties op het bericht."; -/* Accessibility hint for the like button on the reader post card cell - Accessibility hint for the liked button on the reader post card cell */ +/* Accessibility hint for the like button on the reader post card cell */ "reader.post.button.like.accessibility.hint" = "Vindt het bericht leuk."; /* Text for the 'Liked' button on the reader post card cell. */ "reader.post.button.liked" = "Geliked"; +/* Accessibility hint for the liked button on the reader post card cell */ +"reader.post.button.liked.accessibility.hint" = "Vindt het bericht niet meer leuk."; + /* Text for the 'Reblog' button on the reader post card cell. */ "reader.post.button.reblog" = "Rebloggen"; @@ -10739,6 +10688,9 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Title for the tooltip anchor. */ "readerDetail.tooltipAnchorTitle.accessibilityLabel" = "Nieuw"; +/* The title for the transfer footer view in Register Domain screen */ +"register.domain.transfer.title" = "Looking to transfer a domain you already own?"; + /* Information of what related post are and how they are presented */ "relatedPostsSettings.optionsFooter" = "Gerelateerde berichten geeft relevante content van je site weer onder je berichten."; @@ -10805,11 +10757,23 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Site name that is placed in the tooltip view. */ "site.creation.domain.tooltip.site.name" = "JouwSitenaam.com"; +/* Accessibility hint for media item preview for user's viewing an item in their media library */ +"siteMediaItem.contentViewAccessibilityHint" = "Klik om media op volledig scherm te bekijken"; + +/* Accessibility label for media item preview for user's viewing an item in their media library */ +"siteMediaItem.contentViewAccessibilityLabel" = "Een voorbeeld van de media bekijken"; + +/* Button selection media in media picker */ +"siteMediaPicker.deselect" = "Deselecteren"; + +/* Button selection media in media picker */ +"siteMediaPicker.select" = "Selecteer"; + /* Title for screen to select the privacy options for a blog */ "siteSettings.privacy.title" = "Privacy"; /* Hint for users when hidden privacy setting is set */ -"siteVisibility.hidden.hint" = "Je site is zichtbaar voor iedereen, maar vraagt de zoekmachines om je site niet te indexeren."; +"siteVisibility.hidden.hint" = "Je site wordt verborgen voor bezoekers door middel van een melding 'Binnenkort beschikbaar' tot hij klaar is om bekeken te worden."; /* Text for privacy settings: Hidden */ "siteVisibility.hidden.title" = "Verborgen"; @@ -11090,6 +11054,9 @@ Example: given a notice format "Following %@" and empty site name, this will be /* View title for Help & Support page. */ "support.title" = "Help"; +/* Title for placeholder in Tenor picker */ +"tenor.welcomeMessage" = "Zoek gratis GIF's om toe te voegen aan je mediabibliotheek!"; + /* Header of delete screen section listing things that will be deleted. */ "these items will be deleted:" = "Deze items worden verwijderd:"; @@ -11105,9 +11072,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Displayed in the confirmation alert when marking unread notifications as read. */ "unread" = "ongelezen"; -/* Label displayed on video media items. */ -"video" = "video"; - /* Portion of a message for Jetpack users that have multisite WP installation, thus Restore is not available. This part is a link, colored with a different color. */ "visit our documentation page" = "bezoek onze documentatie pagina"; diff --git a/WordPress/Resources/pl.lproj/Localizable.strings b/WordPress/Resources/pl.lproj/Localizable.strings index 861045ac6d48..b19beeff7982 100644 --- a/WordPress/Resources/pl.lproj/Localizable.strings +++ b/WordPress/Resources/pl.lproj/Localizable.strings @@ -1,4 +1,4 @@ -/* Translation-Revision-Date: 2023-07-24 20:13:46+0000 */ +/* Translation-Revision-Date: 2023-10-25 07:12:08+0000 */ /* Plural-Forms: nplurals=3; plural=(n == 1) ? 0 : ((n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14)) ? 1 : 2); */ /* Generator: GlotPress/4.0.0-alpha.11 */ /* Language: pl */ @@ -62,9 +62,6 @@ /* Let's the user know that a third party sharing service was reconnected. The %@ is a placeholder for the service name. */ "%@ was reconnected." = "Połączony ponownie: %@."; -/* Accessibility label for a post in the post list. The parameters are the title, and date respectively. For example, \"Let it Go, 1 hour ago.\" */ -"%@, %@." = "%1$@, %2$@."; - /* The number of connected accounts on a third party sharing service connected to the user's blog. The '%d' is a placeholder for the number of accounts. */ "%d accounts" = "%d konta"; @@ -159,10 +156,6 @@ /* No comment provided by engineer. */ "Activity Logs" = "Dziennik aktywności"; -/* Accessibility label for add button to add items to the user's media library - Remove asset from media picker list */ -"Add" = "Dodaj"; - /* No comment provided by engineer. */ "Add To Beginning" = "Dodaj na początku"; @@ -206,9 +199,6 @@ /* Title for the advanced section in site settings screen */ "Advanced" = "Zaawansowane"; -/* Description of albums in the photo libraries */ -"Albums" = "Albumy"; - /* Autoapprove every comment Browse all themes selection title Displays all of the historical threats @@ -292,9 +282,6 @@ /* Message asking for confirmation on tag deletion */ "Are you sure you want to delete this tag?" = "Czy na pewno chcesz usunąć tag?"; -/* Message prompting the user to confirm that they want to permanently delete a group of media items. */ -"Are you sure you want to permanently delete these items?" = "Czy na pewno usunąć wybrane elementy?"; - /* Message of the trash confirmation alert. */ "Are you sure you want to trash this post?" = "Czy na pewno chcesz przenieść ten wpis do kosza?"; @@ -412,8 +399,7 @@ /* Label for size of media while it's being calculated. */ "Calculating..." = "Obliczanie…"; -/* Accessibility label for taking an image or video with the camera on formatting toolbar. - Accessibility label for the camera tile in the collection view */ +/* Accessibility label for taking an image or video with the camera on formatting toolbar. */ "Camera" = "Aparat"; /* Title of an alert letting the user know */ @@ -461,10 +447,7 @@ Cancels an Action Cancels an alert. Cancels the mark all as read action. - Dismiss the post action sheet Editing GIF alert cancel action button. - Label for a cancel button - Label for the auto-upload cancelation button in the post list. Tapping will prevent the app from auto-uploading the post. Menus cancel button for deleting a menu. Menus cancel button within text bar while editing items. Menus: Cancel button title for canceling an edited menu item. @@ -519,9 +502,6 @@ Main title */ "Change Password" = "Zmiana hasła"; -/* Title of button that takes user to the system Settings section for the app */ -"Change Settings" = "Zmień ustawienia"; - /* Message to show when Publicize globally shared setting failed */ "Change failed" = "Zmiana nie powiodła się"; @@ -549,6 +529,9 @@ /* Label for Publish date picker */ "Choose a date" = "Wybierz datę"; +/* No comment provided by engineer. */ +"Choose a file" = "Wybierz plik"; + /* OK Button title shown in alert informing users about the Reader Save for Later feature. */ "Choose a new app icon" = "Wybierz nową ikonkę aplikacji"; @@ -732,10 +715,6 @@ /* No comment provided by engineer. */ "Copied block" = "Blok został skopiowany"; -/* Copy the post url and paste anywhere in phone - Label for page copy link. Tapping copy the url of page */ -"Copy Link" = "Skopiuj odnośnik"; - /* translators: Copy URL from the clipboard, https://sample.url */ "Copy URL from the clipboard, %s" = "Skopiuj adres URL ze schowka, %s"; @@ -869,7 +848,6 @@ /* Delete Delete button title for the warning shown to the user when he refuses to re-login when the authToken is missing. Title for button that permanently deletes a media item (photo / video) - Title for button that permanently deletes one or more media items (photos / videos) Title of a delete button Title of the trash confirmation alert. */ "Delete" = "Usuń"; @@ -936,7 +914,6 @@ /* Accessibility label for button to dismiss a bottom sheet Accessibility label for the transparent space above the login dialog which acts as a button to dismiss the dialog. Accessibility label for the transparent space above the signup dialog which acts as a button to dismiss the dialog. - Action to show on alert when view asset fails. Dismiss a view. Verb */ "Dismiss" = "Odrzuć"; @@ -945,12 +922,6 @@ User's Display Name */ "Display Name" = "Wyświetlana nazwa"; -/* Accessibility label for other media items in the media collection view. The parameter is the creation date of the document. */ -"Document, %@" = "Dokument, %@"; - -/* Accessibility label for other media items in the media collection view. The parameter is the filename file. */ -"Document: %@" = "Dokument: %@"; - /* Noun. Title. Links to the Domains screen. */ "Domains" = "Domeny"; @@ -1005,20 +976,15 @@ /* Name for the status of a draft post. */ "Draft" = "Szkic"; -/* Title of the drafts filter. This filter shows a list of draft posts. - Title of the drafts header in search list. */ +/* Title of the drafts filter. This filter shows a list of draft posts. */ "Drafts" = "Szkice"; /* No comment provided by engineer. */ "Duplicate block" = "Powiel blok"; -/* Accessibility label for edit button to enable multi selection mode in the user's media library - Edit the post. - Editing GIF alert default action button. +/* Editing GIF alert default action button. Edits a Comment Edits the comment - Label for a button that opens the Edit Page view controller - Label for the edit post button. Tapping displays the editor. User action to edit media details. Verb, edit a comment */ "Edit" = "Edytuj"; @@ -1026,9 +992,6 @@ /* Title for the edit more button section */ "Edit \"More\" button" = "Edytuj przycisk \"Więcej\""; -/* Button that displays the media editor to the user */ -"Edit %@" = "Edytuj %@"; - /* Blocklist Keyword Edition Title */ "Edit Blocklist Word" = "Edytuj czarną listę słów"; @@ -1137,9 +1100,6 @@ /* Error message informing the user that their site's title could not be changed */ "Error updating site title" = "Błąd uaktualnienia tytułu strony"; -/* Label for the post author filter. This filter shows posts for all users on the blog. */ -"Everyone" = "Wszyscy"; - /* Label for the excerpt field. Should be the same as WP core. */ "Excerpt" = "Zajawka"; @@ -1405,8 +1365,7 @@ "History" = "Historia"; /* Title for setting which shows the current page assigned as a site's homepage - Title for the homepage section in site settings screen - Title of the Homepage Badge */ + Title for the homepage section in site settings screen */ "Homepage" = "Strona główna"; /* Label for Homepage Settings site settings section @@ -1733,7 +1692,6 @@ "Max Image Upload Size" = "Maksymalny rozmiar przesyłanego obrazka"; /* Accessibility label for the Me button in My Site. - Label for the post author filter. This filter shows posts only authored by the current user. Me page title Noun. Title. Links to the Me screen. The accessibility value of the me tab. @@ -1741,9 +1699,7 @@ "Me" = "Ja"; /* Noun. Title. Links to the blog's Media library. - Tab bar title for the Media tab in Media Picker The menu item to select during a guided tour. - Title for Media Library section of the app. Title for the media section in site settings screen Title label for the media settings section in the app settings */ "Media" = "Media"; @@ -1751,10 +1707,6 @@ /* Label for size of media cache in the app. */ "Media Cache Size" = "Rozmiar pamięci podręcznej mediów"; -/* Title for alert when a generic error happened when loading media - Title for alert when access to the media library is not granted by the user */ -"Media Library" = "Biblioteka mediów"; - /* Message to indicate progress of uploading media to server */ "Media Uploading" = "Wgrywanie multimediów"; @@ -1768,9 +1720,6 @@ /* Menus placeholder text for the name field of a menu with no name. */ "Menu Name" = "Nazwa menu"; -/* Title of section containing image / video metadata such as size and file type */ -"Metadata" = "Metadane"; - /* Option to select the Microsft Outlook app when logging in with magic links */ "Microsoft Outlook" = "Microsoft Outlook"; @@ -1796,11 +1745,8 @@ /* translators: accessibility text. %1: current block position (number). %2: next block position (number) */ "Move block right from position %1$s to position %2$s" = "Przesuń blok w prawo z pozycji %1$s na pozycję %2$s"; -/* Label for a button that moves a page to the trash folder - Label for a option that moves a post to the trash folder - Title for button on the comment details page that moves the comment to trash when tapped. +/* Title for button on the comment details page that moves the comment to trash when tapped. Trash option in the trash confirmation alert. - Trash option in the trash page confirmation alert. Trashes the comment */ "Move to Trash" = "Przenieś do kosza"; @@ -1817,7 +1763,8 @@ Title of My Site tab */ "My Site" = "Moja witryna"; -/* Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ +/* Title for site picker screen. + Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ "My Sites" = "Moje witryny"; /* Siri Suggestion to open My Sites */ @@ -1966,7 +1913,6 @@ Alert dismissal title Button title. Acknowledges a prompt. Button title. An acknowledgement of the message displayed in a prompt. - Confirmation of action Default action Dismisses the alert Marks all notifications as read. @@ -2102,8 +2048,7 @@ Title of pending Comments filter. */ "Pending" = "Oczekujące"; -/* Name for the status of a post pending review. - Title of the Pending Review Badge */ +/* Name for the status of a post pending review. */ "Pending review" = "Oczekuje na przegląd"; /* Noun. Title of the people management feature. @@ -2120,9 +2065,6 @@ /* Photography site intent topic */ "Photography" = "Fotografia"; -/* Tab bar title for the Photos tab in Media Picker */ -"Photos" = "Zdjęcia"; - /* Action title. Noun. Links to a blog's Plans screen. Title for the plan selector */ "Plans" = "Plany"; @@ -2151,9 +2093,6 @@ /* Register Domain - Domain contact information validation error message for an input field */ "Please enter a valid phone number" = "Proszę wprowadzić poprawny numer telefonu"; -/* Instruction text on the two-factor screen. */ -"Please enter the verification code from your authenticator app, or tap the link below to receive a code via SMS." = "Proszę wprowadzić kod weryfikacyjny ze swojej aplikacji autentykacyjnej lub nacisnąć poniższy odnośnik, aby otrzymać kod w SMSie."; - /* Popup message to ask for user credentials (fields shown below). */ "Please enter your credentials" = "Wprowadź swoje dane"; @@ -2216,9 +2155,6 @@ /* Label for selecting the number of posts per page */ "Posts per page" = "Wpisów na stronie"; -/* Subtitle for placeholder in Tenor picker. `The company name 'Tenor' should always be written as it is. */ -"Powered by Tenor" = "Napędzane przez Tenor"; - /* Browse premium themes selection title */ "Premium" = "Premium"; @@ -2255,8 +2191,7 @@ "Privacy notice for California users" = "Nota o ochronie prywatności dla użytkowników z Kalifornii"; /* Name for the status of a post that is marked private. - Privacy setting for posts set to 'Private'. Should be the same as in core WP. - Title of the Private Badge */ + Privacy setting for posts set to 'Private'. Should be the same as in core WP. */ "Private" = "Prywatny"; /* Error message title informing the user that reader content could not be loaded. */ @@ -2280,19 +2215,16 @@ Label for the publish date button. */ "Publish Date" = "Data publikacji"; -/* A short phrase indicating a post is due to be immedately published. - Label for a button that moves a page to the published folder, publishing with the current date/time. */ +/* A short phrase indicating a post is due to be immedately published. */ "Publish Immediately" = "Opublikuj natychmiast"; /* Label for a button that publishes the post - Label for an option that moves a publishes a post immediately Title of button allowing the user to immediately publish the post they are editing. */ "Publish Now" = "Opublikuj teraz"; /* Name for the status of a published post. Period Stats 'Published' header - Title of the published filter. This filter shows a list of posts that the user has published. - Title of the published header in search list. */ + Title of the published filter. This filter shows a list of posts that the user has published. */ "Published" = "Opublikowano"; /* Published on [date] */ @@ -2369,8 +2301,7 @@ /* Title of the completion screen of the Blogging Reminders Settings screen when the reminders are removed. */ "Reminders removed" = "Przypomnienia zostały usunięte"; -/* Add asset to media picker list - Alert button to confirm a plugin to be removed +/* Alert button to confirm a plugin to be removed Button label when removing a blog Label for the Remove Feature Image icon. Tapping will show a confirmation screen for removing the feature image from the post. Remove Action @@ -2423,9 +2354,6 @@ /* Settings: Comments Approval settings */ "Require users to log in" = "Wymagaj od użytkowników zalogowania się"; -/* Title of the reset button */ -"Reset" = "Resetuj"; - /* Accessibility label for the reset filter button in the reader. */ "Reset filter" = "Resetuj filtr"; @@ -2449,12 +2377,9 @@ Button title, displayed when media has failed to upload. Allows the user to try the upload again. Button title. Retries uploading a post. If a user taps the button with this label, the action that evinced this error view will be retried. - Label for a button that attempts to re-upload a page that previously failed to upload. - Label for the retry post upload button. Tapping attempts to upload the post again. Opens the media library . Retries the upload of a user's post. Retry updating User's Role - Retry uploading the post. Retry. Action Retry. Verb – retry a failed media upload. The Jetpack view button title used when an error occurred @@ -2494,14 +2419,10 @@ /* Title of alert informing users about the Reader Save for Later feature. */ "Save Posts for Later" = "Zapisz wpis na później"; -/* Text displayed in HUD when a media item's metadata (title, etc) is saved successfully. */ -"Saved!" = "Zapisano!"; - /* A short message that informs the user a draft page is being saved to the server from the share extension. */ "Saving page…" = "Zapisywanie strony..."; /* Menus save button title while it is saving a Menu. - Text displayed in HUD while a media item's metadata (title, etc) is being saved. Text displayed in HUD while a post is being saved as a draft. */ "Saving..." = "Zapisywanie..."; @@ -2550,9 +2471,6 @@ /* A step in a guided tour for quick start. %@ will be the name of the item to select. */ "Select %@ to set a new title." = "Wybierz %@, aby ustawić nowy tytuł."; -/* Title of button that allows the user to select more photos to access within the app */ -"Select More" = "Wybierz więcej"; - /* Blog Picker's Title */ "Select Site" = "Wybierz witrynę"; @@ -2584,9 +2502,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Button title. Sends a email verification link (Magin link) for signing in. */ "Send email verification link" = "Wyślij wiadomość e-mail z odnośnikiem weryfikacyjnym"; -/* Label for a button that sets the selected page as the site's Homepage */ -"Set as Homepage" = "Ustaw jako stronę domową"; - /* Title of the set goals button in the Blogging Reminders Settings flow. */ "Set reminders" = "Ustaw przypomnienia"; @@ -2600,7 +2515,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Accessibility label for share buttons in nav bars Button label to share a post Button label to share a web page - Share the post. Shares the comment URL Spoken accessibility label Title for a button that allows the user to share their answer to the prompt. @@ -2799,8 +2713,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Name of setting configured when a site uses a static page as its homepage */ "Static Homepage" = "Statyczna strona główna"; -/* Label for post stats option. Tapping displays statistics for a post. - Noun. Abbreviation of Statistics. Name of the Stats feature +/* Noun. Abbreviation of Statistics. Name of the Stats feature Noun. Abbv. of Statistics. Links to a blog's Stats screen. Stats 3D Touch Shortcut Stats window title @@ -2936,7 +2849,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Message shown when site export API failed */ "The site could not be exported." = "Strona nie może zostać wyeksportowana."; -/* Error message informing a user about an invalid password. */ +/* No comment provided by engineer. */ "The username or password stored in the app may be out of date. Please re-enter your password in the settings and try again." = "Nazwa użytkownika lub hasło używane przez aplikację mogą być nieaktualne. Proszę zaktualizować hasło w ustawieniach i spróbować ponownie."; /* Title of alert when theme activation succeeds */ @@ -3025,8 +2938,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title for the traffic section in site settings screen */ "Traffic" = "Ruch"; -/* Accessibility label for trash button to delete items from the user's media library - Accessibility label for trash buttons in nav bars +/* Accessibility label for trash buttons in nav bars Trashes a comment Trashes the comment */ "Trash" = "Kosz"; @@ -3219,8 +3131,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "Videos" = "Filmy"; /* Button title. Displays a summary / sharing screen for a specific post. - Label for a button that opens the page when tapped. - Label for the view post button. Tapping displays the post as it appears on the web. Opens the post epilogue screen to allow sharing / viewing of a post. Theme View action title Verb. The screen title shown when viewing a post inside the app. */ @@ -3291,6 +3201,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* View title during the Google auth process. */ "Waiting..." = "Oczekuję…"; +/* No comment provided by engineer. */ +"Warning message" = "Wiadomość ostrzeżenia"; + /* Error message displayed when a refresh is taking longer than usual. The refresh hasn't failed and it might still succeed */ "We are having trouble loading data" = "Mamy problem z ładowaniem danych"; @@ -3723,9 +3636,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Indicating that referrer was marked as spam */ "marked as spam" = "oznaczono jako spam"; -/* Verb. User action to dismiss error alert when failing to load media item. */ -"mediaItemTable.errorAlert.dismissButton" = "Odrzuć"; - /* Verb. Button title. Tapping dismisses a prompt. */ "mediaLibrary.retryOptionsAlert.dismissButton" = "Odrzuć"; @@ -3741,9 +3651,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* The title in the Delete WordPress screen */ "migration.deleteWordpress.title" = "Nie potrzebujesz już aplikacji WordPressa na swoim urządzeniu"; -/* Primary button title in the migration done screen. */ -"migration.done.actions.primary.title" = "Koniec"; - /* Highlighted text in the footer of the migration done screen. */ "migration.done.footer.highlighted" = "odinstalowanie aplikacji WordPressa"; @@ -3795,9 +3702,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Dismiss button title. */ "noResultsViewController.dismissButton" = "Odrzuć"; -/* Word separating the current index from the total amount. I.e.: 7 of 9 */ -"of" = "z"; - /* No comment provided by engineer. */ "password" = "hasło"; @@ -3918,9 +3822,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Used when the response doesn't have a valid url to display */ "unknown url" = "nieznany adres URL"; -/* Label displayed on video media items. */ -"video" = "film"; - /* Verb. Dismiss the web view screen. */ "webKit.button.dismiss" = "Odrzuć"; diff --git a/WordPress/Resources/pt-BR.lproj/Localizable.strings b/WordPress/Resources/pt-BR.lproj/Localizable.strings index 98f0b8358242..acdc95ebcd3a 100644 --- a/WordPress/Resources/pt-BR.lproj/Localizable.strings +++ b/WordPress/Resources/pt-BR.lproj/Localizable.strings @@ -1,4 +1,4 @@ -/* Translation-Revision-Date: 2023-04-21 14:14:30+0000 */ +/* Translation-Revision-Date: 2024-01-04 16:02:17+0000 */ /* Plural-Forms: nplurals=2; plural=n > 1; */ /* Generator: GlotPress/4.0.0-alpha.11 */ /* Language: pt_BR */ @@ -152,9 +152,6 @@ /* Difference label for Period Overview stat, indicating change from previous period. Ex: +99.9K (5%) */ "%@%@ (%@%%)" = "%1$@%2$@ (%3$@%%)"; -/* Accessibility label for a post in the post list. The parameters are the title, and date respectively. For example, \"Let it Go, 1 hour ago.\" */ -"%@, %@." = "%1$@, %2$@."; - /* Accessibility value for a Stats' Posting Activity Month if the user selected a day with posts. The first parameter is day (e.g. November 2019). The second parameter is the number of posts. */ "%@. %d posts." = "%1$@. %2$d posts."; @@ -210,9 +207,6 @@ /* Age between dates over one year. */ "%d years" = "%d anos"; -/* Max image size in pixels (e.g. 300x300px) */ -"%dx%dpx" = "%1$dx%2$dpx"; - /* One menu area available in the theme */ "%i menu area in this theme" = "%i área de menu neste tema"; @@ -472,13 +466,6 @@ translators: Block name. %s: The localized block name */ /* Label for the Activity Type filter button when there are more than 1 activity type selected */ "Activity Type (%1$d)" = "Tipo de atividade (%1$d)"; -/* Accessibility label for add button to add items to the user's media library - Remove asset from media picker list */ -"Add" = "Adicionar"; - -/* Action for Media Picker to indicate selection of media. The argument in the string represents the number of elements (as numeric digits) selected */ -"Add %@" = "Adicionar %@"; - /* No comment provided by engineer. */ "Add Block After" = "Adicionar bloco depois"; @@ -569,9 +556,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Add menu item to children" = "Adicionar item de menu"; -/* Accessibility hint for add button to add items to the user's media library */ -"Add new media" = "Adicionar nova mídia"; - /* Screen reader text for Menus button that adds a new menu to a site. */ "Add new menu" = "Adicionar novo menu"; @@ -634,9 +618,6 @@ translators: Block name. %s: The localized block name */ /* Option to select the Airmail app when logging in with magic links */ "Airmail" = "Airmail"; -/* Description of albums in the photo libraries */ -"Albums" = "Álbuns"; - /* Image alignment option title. Title of the screen for choosing an image's alignment. */ "Alignment" = "Alinhamento"; @@ -861,15 +842,9 @@ translators: Block name. %s: The localized block name */ /* Message prompting the user to confirm that they want to disconnect Jetpack from the site. */ "Are you sure you want to disconnect Jetpack from the site?" = "Tem certeza de que deseja desconectar o Jetpack do seu site?"; -/* Message prompting the user to confirm that they want to permanently delete a group of media items. */ -"Are you sure you want to permanently delete these items?" = "Tem certeza que deseja excluir permanentemente os itens selecionados?"; - /* Message prompting the user to confirm that they want to permanently delete a media item. Should match Calypso. */ "Are you sure you want to permanently delete this item?" = "Tem certeza de que deseja excluir permanentemente este item?"; -/* Message of the confirmation alert when deleting a page from the trash. */ -"Are you sure you want to permanently delete this page?" = "Tem certeza de que deseja excluir esta página permanentemente?"; - /* Message of the confirmation alert when deleting a post from the trash. */ "Are you sure you want to permanently delete this post?" = "Tem certeza de que deseja excluir permanentemente esse post?"; @@ -901,9 +876,6 @@ translators: Block name. %s: The localized block name */ /* Title of message shown when user taps submit for review. */ "Are you sure you want to submit for review?" = "Tem certeza de que deseja enviar para revisão?"; -/* Message of the trash page confirmation alert. */ -"Are you sure you want to trash this page?" = "Tem certeza de que deseja mover esta página para a lixeira?"; - /* Message of the trash confirmation alert. */ "Are you sure you want to trash this post?" = "Você tem certeza de que deseja enviar este post para a lixeira?"; @@ -944,9 +916,6 @@ translators: Block name. %s: The localized block name */ /* translators: accessibility text. Empty Audio caption. */ "Audio caption. Empty" = "Legenda do áudio. Vazia"; -/* Accessibility label for audio items in the media collection view. The parameter is the creation date of the audio. */ -"Audio, %@" = "Áudio, %@"; - /* No comment provided by engineer. */ "Authenticating" = "Autenticando"; @@ -1223,9 +1192,6 @@ translators: Block name. %s: The localized block name */ /* Label for the post author in the post detail. */ "By " = "Por"; -/* Accessibility label for the post author in the post list. The parameter is the author name. For example, \"By Elsa.\" */ -"By %@." = "Por %@."; - /* Legal disclaimer for logging in. The underscores _..._ denote underline. */ "By continuing, you agree to our _Terms of Service_." = "Ao continuar, você concorda com os nossos _Termos de Serviço_."; @@ -1245,8 +1211,7 @@ translators: Block name. %s: The localized block name */ /* Label for size of media while it's being calculated. */ "Calculating..." = "Calculando..."; -/* Accessibility label for taking an image or video with the camera on formatting toolbar. - Accessibility label for the camera tile in the collection view */ +/* Accessibility label for taking an image or video with the camera on formatting toolbar. */ "Camera" = "Câmera"; /* Title of an alert informing the user the camera permission for the app is disabled and its needed to proceed */ @@ -1297,10 +1262,7 @@ translators: Block name. %s: The localized block name */ Cancels an Action Cancels an alert. Cancels the mark all as read action. - Dismiss the post action sheet Editing GIF alert cancel action button. - Label for a cancel button - Label for the auto-upload cancelation button in the post list. Tapping will prevent the app from auto-uploading the post. Menus cancel button for deleting a menu. Menus cancel button within text bar while editing items. Menus: Cancel button title for canceling an edited menu item. @@ -1317,10 +1279,6 @@ translators: Block name. %s: The localized block name */ Verb. Title for Jetpack Restore cancel button. */ "Cancel" = "Cancelar"; -/* Label for the Post List option that cancels automatic uploading of a post. - Media Library option to cancel an in-progress or failed upload. */ -"Cancel Upload" = "Cancelar envio"; - /* Menus alert button title to cancel discarding changes and not createa a new menu. Menus alert button title to cancel discarding changes and not select a new menu Menus alert button title to cancel discarding changes and not select a new menu location */ @@ -1379,9 +1337,6 @@ translators: Block name. %s: The localized block name */ Main title */ "Change Password" = "Alterar senha"; -/* Title of button that takes user to the system Settings section for the app */ -"Change Settings" = "Alterar configurações"; - /* Change Username title. */ "Change Username" = "Alterar nome de usuário"; @@ -1518,9 +1473,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Choose file" = "Escolher arquivo"; -/* Menu option for selecting media from the device's photo library. */ -"Choose from My Device" = "Escolher em meu dispositivo"; - /* Explanatory text for Homepage Settings homepage type selection. */ "Choose from a homepage that displays your latest posts (classic blog) or a fixed \/ static page." = "Escolha entre uma página inicial que exiba seus posts mais recentes (blog clássico) ou uma página estática\/fixa."; @@ -1721,9 +1673,6 @@ translators: Block name. %s: The localized block name */ /* Community & Non-Profit site intent topic */ "Community & Non-Profit" = "Comunidade e trabalho beneficente"; -/* Accessibility indication that the current Post List style is currently Compact. */ -"Compact" = "Compacta"; - /* The action is completed */ "Completed" = "Operação concluída"; @@ -1909,10 +1858,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Copied block" = "Bloco copiado"; -/* Copy the post url and paste anywhere in phone - Label for page copy link. Tapping copy the url of page */ -"Copy Link" = "Copiar Link"; - /* Copy link to oomment button title */ "Copy Link to Comment" = "Copiar link para o comentário"; @@ -2021,9 +1966,6 @@ translators: Block name. %s: The localized block name */ /* Error title displayed when unable to close user account. */ "Couldn’t close account automatically" = "Não foi possível encerrar a conta automaticamente"; -/* Message to show while media data source is finding the number of items available. */ -"Counting media items..." = "Contando itens de mídia..."; - /* Period Stats 'Countries' header */ "Countries" = "Países"; @@ -2271,7 +2213,6 @@ translators: Block name. %s: The localized block name */ /* Delete Delete button title for the warning shown to the user when he refuses to re-login when the authToken is missing. Title for button that permanently deletes a media item (photo / video) - Title for button that permanently deletes one or more media items (photos / videos) Title of a delete button Title of the trash confirmation alert. */ "Delete" = "Excluir"; @@ -2279,15 +2220,11 @@ translators: Block name. %s: The localized block name */ /* Menus confirmation button for deleting a menu. */ "Delete Menu" = "Excluir menu"; -/* Delete option in the confirmation alert when deleting a page from the trash. - Delete option in the confirmation alert when deleting a post from the trash. - Label for a button permanently deletes a page. - Label for the delete post option. Tapping permanently deletes a post. +/* Delete option in the confirmation alert when deleting a post from the trash. Title for button on the comment details page that deletes the comment when tapped. */ "Delete Permanently" = "Excluir permanentemente"; -/* Title of the confirmation alert when deleting a page from the trash. - Title of the confirmation alert when deleting a post from the trash. */ +/* Title of the confirmation alert when deleting a post from the trash. */ "Delete Permanently?" = "Excluir permanentemente?"; /* Button label for deleting the current site @@ -2403,7 +2340,6 @@ translators: Block name. %s: The localized block name */ /* Accessibility label for button to dismiss a bottom sheet Accessibility label for the transparent space above the login dialog which acts as a button to dismiss the dialog. Accessibility label for the transparent space above the signup dialog which acts as a button to dismiss the dialog. - Action to show on alert when view asset fails. Dismiss a view. Verb */ "Dismiss" = "Dispensar"; @@ -2421,12 +2357,6 @@ translators: Block name. %s: The localized block name */ User's Display Name */ "Display Name" = "Nome de exibição"; -/* Accessibility label for other media items in the media collection view. The parameter is the creation date of the document. */ -"Document, %@" = "Documento, %@"; - -/* Accessibility label for other media items in the media collection view. The parameter is the filename file. */ -"Document: %@" = "Documento: %@"; - /* Message shown when all tours have been completed */ "Doesn't it feel good to cross things off a list?" = "Não é ótimo riscar os itens da lista?"; @@ -2587,8 +2517,7 @@ translators: Block name. %s: The localized block name */ /* Description of a Quick Start Tour */ "Draft and publish a post." = "Crie e publique um post."; -/* Title of the drafts filter. This filter shows a list of draft posts. - Title of the drafts header in search list. */ +/* Title of the drafts filter. This filter shows a list of draft posts. */ "Drafts" = "Rascunhos"; /* No comment provided by engineer. */ @@ -2600,10 +2529,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Drag to adjust focal point" = "Arraste para ajustar o ponto focal"; -/* Label for page duplicate option. Tapping creates a copy of the page. - Label for post duplicate option. Tapping creates a copy of the post. */ -"Duplicate" = "Duplicar"; - /* No comment provided by engineer. */ "Duplicate block" = "Duplicar bloco"; @@ -2616,13 +2541,9 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Each block has its own settings. To find them, tap on a block. Its settings will appear on the toolbar at the bottom of the screen." = "Cada bloco possui suas próprias configurações. Para encontrá-las, toque em um bloco. As configurações dele aparecerão na barra de ferramentas na parte inferior da tela."; -/* Accessibility label for edit button to enable multi selection mode in the user's media library - Edit the post. - Editing GIF alert default action button. +/* Editing GIF alert default action button. Edits a Comment Edits the comment - Label for a button that opens the Edit Page view controller - Label for the edit post button. Tapping displays the editor. User action to edit media details. Verb, edit a comment */ "Edit" = "Editar"; @@ -2630,9 +2551,6 @@ translators: Block name. %s: The localized block name */ /* Title for the edit more button section */ "Edit \"More\" button" = "Editar botão \"Mais\""; -/* Button that displays the media editor to the user */ -"Edit %@" = "Editar %@"; - /* Blocklist Keyword Edition Title */ "Edit Blocklist Word" = "Editar palavra da lista de bloqueios"; @@ -2819,9 +2737,6 @@ translators: Block name. %s: The localized block name */ /* Secondary message shown when there are no domains that match the user entered text. */ "Enter different words above and we'll look for an address that matches it." = "Digite palavras diferentes das acima e tentaremos encontrar um endereço relacionado a elas."; -/* Accessibility hint for edit button to enable multi selection mode in the user's media library */ -"Enter edit mode to enable multi select to delete" = "Entre no modo de edição para ativar a seleção múltipla para exclusão"; - /* (placeholder) Help enter WordPress password Placeholder of a field to type a password to protect the post. */ "Enter password" = "Digite a senha"; @@ -2977,9 +2892,6 @@ translators: Block name. %s: The localized block name */ /* Short title telling the user they will receive a blogging reminder every day of the week. */ "Every day at %@" = "Todo dia às %@"; -/* Label for the post author filter. This filter shows posts for all users on the blog. */ -"Everyone" = "Todos"; - /* Example story title description */ "Example story title" = "Título de exemplo do story"; @@ -2989,9 +2901,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Excerpt length (words)" = "Tamanho do resumo (em palavras)"; -/* Accessibility label for a post's excerpt in the post list. The parameter is the post excerpt. For example, \"Excerpt. This is the first paragraph.\" */ -"Excerpt. %@." = "Resumo. %@."; - /* Should be the same as the text displayed if the user clicks the (i) in Calypso. */ "Excerpts are optional hand-crafted summaries of your content." = "Os fragmentos são resumos opcionais e feitos manualmente do seu conteúdo."; @@ -3001,8 +2910,7 @@ translators: Block name. %s: The localized block name */ /* Accessibility Label for the exit full screen button on the full screen comment reply mode */ "Exit Full Screen" = "Sair do modo de tela cheia"; -/* Accessibility indication that the current Post List style is currently Expanded. - Screen reader text to represent the expanded state of a UI control */ +/* Screen reader text to represent the expanded state of a UI control */ "Expanded" = "Expandida"; /* Accessibility hint */ @@ -3052,9 +2960,6 @@ translators: Block name. %s: The localized block name */ The action failed */ "Failed" = "Falhou"; -/* Error title when picked media cannot be imported into stories. */ -"Failed Media Export" = "Falha ao exportar mídia"; - /* Message for mark all as read success notice */ "Failed marking Notifications as read" = "Erro ao marcar todas as notificações como lidas"; @@ -3573,8 +3478,7 @@ translators: Block name. %s: The localized block name */ "Home" = "Página inicial"; /* Title for setting which shows the current page assigned as a site's homepage - Title for the homepage section in site settings screen - Title of the Homepage Badge */ + Title for the homepage section in site settings screen */ "Homepage" = "Página inicial"; /* Label for Homepage Settings site settings section @@ -3671,9 +3575,6 @@ translators: Block name. %s: The localized block name */ /* Hint for image title on image settings. */ "Image title" = "Título da imagem"; -/* Accessibility label for image thumbnails in the media collection view. The parameter is the creation date of the image. */ -"Image, %@" = "Imagem, %@"; - /* Undated post time label */ "Immediately" = "Imediatamente"; @@ -4147,9 +4048,6 @@ translators: Block name. %s: The localized block name */ Settings: Comments Approval settings */ "Links in comments" = "Links nos comentários"; -/* The accessibility label for the list style button in the Post List. */ -"List style" = "Estilo da lista"; - /* Title of the screen that load selected the revisions. */ "Load" = "Carregar"; @@ -4162,18 +4060,12 @@ translators: Block name. %s: The localized block name */ /* Text displayed while loading the activity feed for a site */ "Loading Backups..." = "Carregando backups…"; -/* Phrase to show when the user has searched for GIFs and they are being loaded. */ -"Loading GIFs..." = "Carregando gifs..."; - /* Menus label text displayed while menus are loading */ "Loading Menus..." = "Carregando menus..."; /* Text displayed while loading site People. */ "Loading People..." = "Carregando pessoas..."; -/* Phrase to show when the user has searched for images and they are being loaded. */ -"Loading Photos..." = "Carregando fotos..."; - /* Text displayed while loading plans details */ "Loading Plan..." = "Carregando o plano..."; @@ -4234,8 +4126,7 @@ translators: Block name. %s: The localized block name */ /* Local Services site intent topic */ "Local Services" = "Serviços locais"; -/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. - Title of the Local Changes Badge */ +/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. */ "Local changes" = "Alterações locais"; /* Title for alert when a generic error happened when trying to find the location of the device */ @@ -4399,7 +4290,6 @@ translators: Block name. %s: The localized block name */ "Max Video Upload Size" = "Tamanho máximo do vídeo"; /* Accessibility label for the Me button in My Site. - Label for the post author filter. This filter shows posts only authored by the current user. Me page title Noun. Title. Links to the Me screen. The accessibility value of the me tab. @@ -4407,9 +4297,7 @@ translators: Block name. %s: The localized block name */ "Me" = "Eu"; /* Noun. Title. Links to the blog's Media library. - Tab bar title for the Media tab in Media Picker The menu item to select during a guided tour. - Title for Media Library section of the app. Title for the media section in site settings screen Title label for the media settings section in the app settings */ "Media" = "Mídia"; @@ -4421,13 +4309,6 @@ translators: Block name. %s: The localized block name */ /* Label for size of media cache in the app. */ "Media Cache Size" = "Tamanho do cache de mídia"; -/* Title for alert when access to media capture is not granted */ -"Media Capture" = "Captura de mídia"; - -/* Title for alert when a generic error happened when loading media - Title for alert when access to the media library is not granted by the user */ -"Media Library" = "Biblioteca de mídia"; - /* Title for action sheet with media options. */ "Media Options" = "Opções de mídia"; @@ -4450,9 +4331,6 @@ translators: Block name. %s: The localized block name */ /* translators: %s: block title e.g: \"Paragraph\". */ "Media options" = "Opções de mídia"; -/* Alert title when there is issues loading an asset to preview. */ -"Media preview failed." = "Falha ao visualizar mídia."; - /* Alert displayed to the user when multiple media items have uploaded successfully. */ "Media uploaded (%ld files)" = "Mídias enviadas (%ld arquivos)"; @@ -4490,9 +4368,6 @@ translators: Block name. %s: The localized block name */ Label for the share message field on the post settings. */ "Message" = "Mensagem"; -/* Title of section containing image / video metadata such as size and file type */ -"Metadata" = "Metadados"; - /* Option to select the Microsft Outlook app when logging in with magic links */ "Microsoft Outlook" = "Microsoft Outlook"; @@ -4512,13 +4387,11 @@ translators: Block name. %s: The localized block name */ "Months and Years" = "Meses e anos"; /* Accessibility label for more button in dashboard quick start card. - Accessibility label for the More button in Page List. Accessibility label for the More button in Post List (compact view). Accessibility label for the More button on formatting toolbar. Accessibility label for the More button on Reader Cell Accessibility label for the More button on Reader's post details - Action button to display more available options - Label for the more post button. Tapping displays an action sheet with post options. */ + Action button to display more available options */ "More" = "Mais"; /* Action button to display more available options @@ -4576,15 +4449,8 @@ translators: Block name. %s: The localized block name */ /* Screen reader text for button that will move the menu item */ "Move menu item" = "Mover o item de menu"; -/* Label for a button that moves a page to the draft folder - Label for an option that moves a post to the draft folder */ -"Move to Draft" = "Mover para rascunhos"; - -/* Label for a button that moves a page to the trash folder - Label for a option that moves a post to the trash folder - Title for button on the comment details page that moves the comment to trash when tapped. +/* Title for button on the comment details page that moves the comment to trash when tapped. Trash option in the trash confirmation alert. - Trash option in the trash page confirmation alert. Trashes the comment */ "Move to Trash" = "Mover para a lixeira"; @@ -4616,7 +4482,8 @@ translators: Block name. %s: The localized block name */ Title of My Site tab */ "My Site" = "Meu site"; -/* Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ +/* Title for site picker screen. + Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ "My Sites" = "Meus sites"; /* Siri Suggestion to open My Sites */ @@ -4863,9 +4730,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title for the view when there aren't any Activities to display in the Activity Log for a given filter. */ "No matching events found." = "Nenhum evento correspondente encontrado."; -/* Message displayed when no results are returned from a media library search. Should match Calypso. - Phrase to show when the user search for images but there are no result to show. - Phrase to show when the user searches for GIFs but there are no result to show. */ +/* Message displayed when no results are returned from a media library search. Should match Calypso. */ "No media matching your search" = "Nenhum arquivo corresponde à sua pesquisa"; /* Menus text shown when no menus were available for loading the Menus editor. */ @@ -4883,8 +4748,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Displayed in the Notifications Tab as a title, when there are no notifications */ "No notifications yet" = "Nenhuma notificação ainda"; -/* Displayed when the user is searching the pages list and there are no matching pages - Text displayed when there's no matching with the text search */ +/* Text displayed when there's no matching with the text search */ "No pages matching your search" = "Nenhuma página atende sua pesquisa"; /* Text displayed when search for plugins returns no results */ @@ -4905,9 +4769,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Message shown whent the reader finds no posts for the chosen tag */ "No posts have been made recently with this tag." = "Não há nenhuma publicação recente contendo essa tag."; -/* Displayed when the user is searching the posts list and there are no matching posts */ -"No posts matching your search" = "Nenhum post atende sua pesquisa"; - /* Accessibility value for a Stats' Posting Activity Month if there are no posts. */ "No posts." = "Nenhum post."; @@ -5005,9 +4866,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* A message title */ "Nothing liked yet" = "Nada foi curtido ainda"; -/* Default message for empty media picker */ -"Nothing to show" = "Nada para mostrar"; - /* Notifications Details Accessibility Identifier */ "Notification Details Table" = "Tabela de detalhes de notificação"; @@ -5067,7 +4925,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Alert dismissal title Button title. Acknowledges a prompt. Button title. An acknowledgement of the message displayed in a prompt. - Confirmation of action Default action Dismisses the alert Marks all notifications as read. @@ -5129,9 +4986,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* No comment provided by engineer. */ "Only show excerpt" = "Mostrar apenas o resumo"; -/* Message telling the user that they've only enabled limited photo library permissions for the app. */ -"Only the selected photos you've given access to are available." = "Somente as fotos selecionadas para as quais você concedeu acesso estão disponíveis."; - /* An informal exclaimation that means `something went wrong`. Title for the view when there's an error loading a comment. Title for the view when there's an error loading Activity Log @@ -5166,9 +5020,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title of a button that opens the apps settings in the system Settings.app */ "Open Settings" = "Abrir configurações"; -/* Editor button to swich the media picker from quick mode to full picker */ -"Open full media picker" = "Abrir seletor de mídia completo"; - /* No comment provided by engineer. */ "Open in Safari" = "Abrir no Safari"; @@ -5266,15 +5117,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title shown when selecting a post type of Page from the Share Extension. */ "Page" = "Página"; -/* Prompts the user that a restored page was moved to the drafts list. */ -"Page Restored to Drafts" = "Página recuperada dos rascunhos"; - -/* Prompts the user that a restored page was moved to the published list. */ -"Page Restored to Published" = "Página restaurada para Publicado"; - -/* Prompts the user that a restored page was moved to the scheduled list. */ -"Page Restored to Scheduled" = "Página restaurada para Agendado"; - /* Name of the button to open the page settings The title of the Page Settings screen. */ "Page Settings" = "Configurações da página"; @@ -5291,9 +5133,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of notification displayed when a page has failed to upload. */ "Page failed to upload" = "Falha ao enviar página"; -/* A short message explaining that a page was moved to the trash bin. */ -"Page moved to trash." = "Página movida para a lixeira."; - /* Title of notification displayed when a page has been successfully saved as a draft. */ "Page pending review" = "Página pendente de revisão"; @@ -5365,8 +5204,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title of pending Comments filter. */ "Pending" = "Pendente"; -/* Name for the status of a post pending review. - Title of the Pending Review Badge */ +/* Name for the status of a post pending review. */ "Pending review" = "Revisão pendente"; /* Noun. Title of the people management feature. @@ -5395,12 +5233,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Photography site intent topic */ "Photography" = "Fotografia"; -/* Tab bar title for the Photos tab in Media Picker */ -"Photos" = "Fotos"; - -/* Subtitle for placeholder in Free Photos. The company name 'Pexels' should always be written as it is. */ -"Photos provided by Pexels" = "Fotos fornecidas por Pexels"; - /* Title for selecting a new username in the site creation flow. */ "Pick username" = "Escolha seu nome de usuário"; @@ -5492,9 +5324,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Instructional text shown when requesting the user's password for a login initiated via Sign In with Apple */ "Please enter the password for your WordPress.com account to log in with your Apple ID." = "Digite a senha de sua conta do WordPress.com para acessar com a ID da Apple."; -/* Instruction text on the two-factor screen. */ -"Please enter the verification code from your authenticator app, or tap the link below to receive a code via SMS." = "Digite o código de verificação de seu aplicativo de autenticação ou toque no link abaixo para receber um código por SMS."; - /* Popup message to ask for user credentials (fields shown below). */ "Please enter your credentials" = "Entre com suas credenciais"; @@ -5588,15 +5417,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ The post formats available for the post. Should be the same as in core WP. */ "Post Format" = "Formato de post"; -/* Prompts the user that a restored post was moved to the drafts list. */ -"Post Restored to Drafts" = "Post restaurado como rascunho"; - -/* Prompts the user that a restored post was moved to the published list. */ -"Post Restored to Published" = "Post restaurado como publicado"; - -/* Prompts the user that a restored post was moved to the scheduled list. */ -"Post Restored to Scheduled" = "Post restaurado como agendado"; - /* Name of the button to open the post settings The title of the Post Settings screen. */ "Post Settings" = "Configurações do post"; @@ -5616,9 +5436,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of notification displayed when a post has failed to upload. */ "Post failed to upload" = "Falha ao enviar post"; -/* A short message explaining that a post was moved to the trash bin. */ -"Post moved to trash." = "Post movido para a lixeira."; - /* Title of notification displayed when a post has been successfully saved as a draft. */ "Post pending review" = "Post pendente de revisão"; @@ -5677,9 +5494,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Period Stats 'Posts and Pages' header */ "Posts and Pages" = "Posts e páginas"; -/* Title of the Posts Page Badge */ -"Posts page" = "Página de posts"; - /* Message informing the user that their static homepage for posts was set successfully */ "Posts page successfully updated" = "Página de posts atualizada"; @@ -5692,9 +5506,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* A message explaining the Posts I Like feature in the reader */ "Posts that you like will appear here." = "Os posts que você curtir vão aparecer aqui."; -/* Subtitle for placeholder in Tenor picker. `The company name 'Tenor' should always be written as it is. */ -"Powered by Tenor" = "Desenvolvido por Tenor"; - /* Browse premium themes selection title */ "Premium" = "Premium"; @@ -5713,18 +5524,12 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title for screen to preview a static content. */ "Preview" = "Pré-visualizar"; -/* Action for Media Picker to preview the selected media items. The argument in the string represents the number of elements (as numeric digits) selected */ -"Preview %@" = "Visualizar %@"; - /* Title for web preview device switching button */ "Preview Device" = "Dispositivo para visualizar"; /* Title on display preview error */ "Preview Unavailable" = "Visualização indisponível"; -/* Accessibility label for media item preview for user's viewing an item in their media library */ -"Preview media" = "Visualizar mídia"; - /* No comment provided by engineer. */ "Preview page" = "Visualizar página"; @@ -5771,8 +5576,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Privacy notice for California users" = "Aviso de privacidade para usuários na Califórnia"; /* Name for the status of a post that is marked private. - Privacy setting for posts set to 'Private'. Should be the same as in core WP. - Title of the Private Badge */ + Privacy setting for posts set to 'Private'. Should be the same as in core WP. */ "Private" = "Privado"; /* No comment provided by engineer. */ @@ -5822,12 +5626,10 @@ translators: %s: Select control button label e.g. \"Button width\" */ Label for the publish date button. */ "Publish Date" = "Data de publicação"; -/* A short phrase indicating a post is due to be immedately published. - Label for a button that moves a page to the published folder, publishing with the current date/time. */ +/* A short phrase indicating a post is due to be immedately published. */ "Publish Immediately" = "Publicar imediatamente"; /* Label for a button that publishes the post - Label for an option that moves a publishes a post immediately Title of button allowing the user to immediately publish the post they are editing. */ "Publish Now" = "Publicar agora"; @@ -5845,8 +5647,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Name for the status of a published post. Period Stats 'Published' header - Title of the published filter. This filter shows a list of posts that the user has published. - Title of the published header in search list. */ + Title of the published filter. This filter shows a list of posts that the user has published. */ "Published" = "Publicado"; /* Precedes the name of the blog just posted on */ @@ -5985,8 +5786,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of the completion screen of the Blogging Reminders Settings screen when the reminders are removed. */ "Reminders removed" = "Lembretes removidos"; -/* Add asset to media picker list - Alert button to confirm a plugin to be removed +/* Alert button to confirm a plugin to be removed Button label when removing a blog Label for the Remove Feature Image icon. Tapping will show a confirmation screen for removing the feature image from the post. Remove Action @@ -6139,9 +5939,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of secondary button on alert prompting verify their accounts while attempting to publish */ "Resend" = "Reenviar"; -/* Title of the reset button */ -"Reset" = "Redefinir"; - /* Accessibility label for the reset activity type button */ "Reset Activity Type filter" = "Redefine o filtro de tipo de atividade"; @@ -6196,12 +5993,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ Button title, displayed when media has failed to upload. Allows the user to try the upload again. Button title. Retries uploading a post. If a user taps the button with this label, the action that evinced this error view will be retried. - Label for a button that attempts to re-upload a page that previously failed to upload. - Label for the retry post upload button. Tapping attempts to upload the post again. Opens the media library . Retries the upload of a user's post. Retry updating User's Role - Retry uploading the post. Retry. Action Retry. Verb – retry a failed media upload. The Jetpack view button title used when an error occurred @@ -6213,9 +6007,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Button title that triggers a scan */ "Retry Scan" = "Tentar varredura novamente"; -/* User action to retry media upload. */ -"Retry Upload" = "Tentar upload novamente"; - /* User action to retry all failed media uploads. */ "Retry all" = "Tentar todas novamente"; @@ -6313,9 +6104,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Accessibility label for the 'Save Post' button when a post has been saved. */ "Saved Post" = "Post salvo"; -/* Text displayed in HUD when a media item's metadata (title, etc) is saved successfully. */ -"Saved!" = "Salvo!"; - /* Accessibility hint for the 'Save Post' button. */ "Saves this post for later." = "Salvar esse post para depois."; @@ -6326,7 +6114,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Saving post…" = "Salvando o post..."; /* Menus save button title while it is saving a Menu. - Text displayed in HUD while a media item's metadata (title, etc) is being saved. Text displayed in HUD while a post is being saved as a draft. */ "Saving..." = "Salvando..."; @@ -6417,21 +6204,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* No comment provided by engineer. */ "Search or type URL" = "Pesquisar ou digitar o URL"; -/* Text displayed when the search controller will be presented */ -"Search pages" = "Pesquisar páginas"; - -/* Text displayed when the search controller will be presented */ -"Search posts" = "Pesquisar posts"; - /* No comment provided by engineer. */ "Search settings" = "Configurações de pesquisa"; -/* Title for placeholder in Tenor picker */ -"Search to find GIFs to add to your Media Library!" = "Pesquise por gifs para adicionar em sua biblioteca de arquivos."; - -/* Title for placeholder in Free Photos */ -"Search to find free photos to add to your Media Library!" = "Pesquise para encontrar imagens gratuitas para adicionar à sua biblioteca de arquivos!"; - /* Menus search bar placeholder text. */ "Search..." = "Pesquisar..."; @@ -6502,9 +6277,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Register Domain - Domain contact information field placeholder for Country */ "Select Country" = "Selecione seu país"; -/* Title of button that allows the user to select more photos to access within the app */ -"Select More" = "Selecionar mais"; - /* Blog Picker's Title */ "Select Site" = "Selecionar Site"; @@ -6526,9 +6298,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Register domain - Title for the Choose domain button of Suggested domains screen */ "Select domain" = "Selecionar domínio"; -/* Accessibility hint for actions when displaying media items. */ -"Select media." = "Selecione a mídia."; - /* Accessibility label for selecting paragraph style button on formatting toolbar. */ "Select paragraph style" = "Selecione o estilo do parágrafo"; @@ -6632,19 +6401,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label for connected service in Publicize stat. */ "Service" = "Serviço"; -/* Label for a button that opens the Set Parent options view controller - Navigation title displayed on the navigation bar */ +/* Navigation title displayed on the navigation bar */ "Set Parent" = "Configurar ascendente"; /* No comment provided by engineer. */ "Set as Featured Image" = "Definir como imagem destacada"; -/* Label for a button that sets the selected page as the site's Homepage */ -"Set as Homepage" = "Definir como página inicial"; - -/* Label for a button that sets the selected page as the site's Posts page */ -"Set as Posts Page" = "Definir como página de posts"; - /* Notice confirming that an image has been set as the post's featured image. */ "Set as featured image" = "Definir como imagem em destaque"; @@ -6688,7 +6450,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Accessibility label for share buttons in nav bars Button label to share a post Button label to share a web page - Share the post. Shares the comment URL Spoken accessibility label Title for a button that allows the user to share their answer to the prompt. @@ -7074,8 +6835,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Name of setting configured when a site uses a static page as its homepage */ "Static Homepage" = "Página inicial estática"; -/* Label for post stats option. Tapping displays statistics for a post. - Noun. Abbreviation of Statistics. Name of the Stats feature +/* Noun. Abbreviation of Statistics. Name of the Stats feature Noun. Abbv. of Statistics. Links to a blog's Stats screen. Stats 3D Touch Shortcut Stats window title @@ -7106,9 +6866,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label text that defines a post marked as sticky */ "Sticky" = "Fixo"; -/* Accessibility label for a sticky post in the post list. */ -"Sticky." = "Fixo."; - /* User action to stop upload. */ "Stop upload" = "Interromper upload"; @@ -7165,7 +6922,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ Theme Support action title */ "Support" = "Suporte"; -/* Button used to switch site */ +/* Title for back button that leads to the site picker screen. */ "Switch Site" = "Trocar site"; /* Switches the Editor to HTML Mode */ @@ -7250,9 +7007,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label explaining why users might want to add tags. */ "Tags help tell readers what a post is about. Separate different tags with commas." = "As tags indicam aos visitantes o assunto do post. Separe tags com vírgulas."; -/* Menu option for taking an image or video with the device's camera. */ -"Take Photo or Video" = "Fotografar ou filmar"; - /* No comment provided by engineer. */ "Take a Photo" = "Tirar uma foto"; @@ -7320,12 +7074,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Accessibility hint */ "Tap to select the previous period" = "Toque para selecionar o período anterior"; -/* Accessibility hint for button used to switch site */ -"Tap to switch to another site, or add a new site" = "Toque para mudar para outro site ou adicionar um novo site."; - -/* Accessibility hint for media item preview for user's viewing an item in their media library */ -"Tap to view media in full screen" = "Toque para ver a mídia em tela cheia"; - /* Accessibility hint for a button that opens a new view with more details. */ "Tap to view more details." = "Toque para visualizar mais detalhes."; @@ -7371,8 +7119,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* No comment provided by engineer. */ "Text formatting controls are located within the toolbar positioned above the keyboard while editing a text block" = "Os controles de formatação de texto estão posicionados na barra de ferramentas acima do teclado ao editar um bloco de texto"; -/* Button title - The button's title text to send a 2FA code via SMS text message. */ +/* Button title */ "Text me a code instead" = "Envie-me o código por SMS"; /* Message of alert when theme activation succeeds */ @@ -7402,9 +7149,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Error message shown to a user who is trying to share to Facebook but does not have any available Facebook Pages. */ "The Facebook connection cannot find any Pages. Publicize cannot connect to Facebook Profiles, only published Pages." = "A conexão com o Facebook não conseguiu encontrar nenhuma página. A publicidade não consegue se conectar aos perfis do Facebook, apenas às páginas publicadas."; -/* Message shown when a GIF failed to load while trying to add it to the Media library. */ -"The GIF could not be added to the Media Library." = "Não foi possível adicionar o GIF à biblioteca de mídia."; - /* Description shown when a user logs in with Google but no matching WordPress.com account is found */ "The Google account \"%@\" doesn't match any account on WordPress.com" = "A conta do Google \"%@\" não corresponde à nenhuma conta do WordPress.com"; @@ -7532,7 +7276,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Error message shown when user attempts to remove the site owner. */ "The user you are trying to remove is the owner of this site. Please contact support for assistance." = "O usuário que você esta tentando excluir é o dono deste site. Entre em contato com o suporte para obter assistência."; -/* Error message informing a user about an invalid password. */ +/* No comment provided by engineer. */ "The username or password stored in the app may be out of date. Please re-enter your password in the settings and try again." = "O nome de usuário ou a senha armazenada no aplicativo deve estar desatualizada. Digite sua senha novamente nas configurações e tente de novo."; /* Message shown when a video failed to load while trying to add it to the Media library. */ @@ -7600,9 +7344,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* A short error message letting the user know about a problem displaying a post. */ "There was a problem displaying this post." = "Houve um problema exibindo este post."; -/* Error message displayed when the Media Library is unable to load a full sized preview of an item. */ -"There was a problem loading the media item." = "Ocorreu um problema no carregamento do item de mídia."; - /* The loading view subtitle displayed when an error occurred */ "There was a problem loading your data, refresh your page to try again." = "Ocorreu um problema ao carregar seus dados. Atualize a página para tentar novamente."; @@ -7615,9 +7356,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Explaining to the user there was an error trying to obtain the current location of the user. */ "There was a problem when trying to access your location. Please try again later." = "Ocorreu um erro ao tentar acessar sua localização. Por favor, tente novamente mais tarde."; -/* Explaining to the user there was an generic error accesing media. */ -"There was a problem when trying to access your media. Please try again later." = "Ocorreu um problema ao tentar acessar sua mídia. Tente novamente mais tarde."; - /* Message for stories unknown error. */ "There was a problem with the Stories editor. If the problem persists you can contact us via the Me > Help & Support screen." = "Ocorreu um problema no editor de Stories. Caso o problema persista, entre em contato conosco por meio da tela Eu > Ajuda e suporte."; @@ -7688,9 +7426,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* A description informing the user in order to proceed with this feature we will need camera permissions, and how to enable it. */ "This app needs permission to access the Camera to scan login codes, tap on the Open Settings button to enable it." = "Este aplicativo precisa de permissão para acessar a câmera para verificar os códigos de login, toque no botão abrir configurações para ativá-lo."; -/* Explaining to the user why the app needs access to the device media library. */ -"This app needs permission to access your device media library in order to add photos and\/or video to your posts. Please change the privacy settings if you wish to allow this." = "Este aplicativo precisa de permissão para acessar sua biblioteca de mídia e adicionar fotos e\/ou vídeos ao seus posts. Altere as configurações de privacidade para permitir isto."; - /* No comment provided by engineer. */ "This color combination may be hard for people to read. Try using a brighter background color and\/or a darker text color." = "Essa combinação de cores pode ser difícil para as pessoas lerem. Tente usar uma cor de fundo mais clara e\/ou uma cor de texto mais escura."; @@ -7929,18 +7664,11 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* No comment provided by engineer. */ "Transform block…" = "Transformar bloco..."; -/* Accessibility label for trash button to delete items from the user's media library - Accessibility label for trash buttons in nav bars +/* Accessibility label for trash buttons in nav bars Trashes a comment Trashes the comment */ "Trash" = "Lixeira"; -/* Accessibility hint for trash button to delete items from the user's media library */ -"Trash selected media" = "Enviar mídias selecionadas para a lixeira"; - -/* Title of the trash page confirmation alert. */ -"Trash this page?" = "Colocar este post na lixeira?"; - /* Title of the trash confirmation alert. */ "Trash this post?" = "Colocar este post na lixeira?"; @@ -8058,9 +7786,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Shown when a user logs in with Google but it subsequently fails to work as login to WordPress.com */ "Unable To Connect" = "Não foi possível conectar"; -/* An error message. */ -"Unable to Connect" = "Não é possível conectar"; - /* Title for stories unknown error. */ "Unable to Create Stories Editor" = "Não foi possível abrir o editor de criação de histórias"; @@ -8076,9 +7801,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* An error message shown when there is an issue creating new invite links. */ "Unable to create new invite links." = "Não foi possível criar novos links de convite."; -/* Text displayed in HUD if there was an error attempting to delete a group of media items. */ -"Unable to delete all media items." = "Incapaz de excluir todos os itens de mídia."; - /* Text displayed in HUD if there was an error attempting to delete a media item. */ "Unable to delete media item." = "Incapaz de excluir item de mídia."; @@ -8142,12 +7864,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Message displayed when sharing a link to the downloadable backup fails. */ "Unable to share link" = "Não foi possível compartilhar o link"; -/* Message that appears when a user tries to trash a page while their device is offline. */ -"Unable to trash pages while offline. Please try again later." = "Não é possível enviar páginas para a lixeira no modo off-line. Tente novamente mais tarde."; - -/* Message that appears when a user tries to trash a post while their device is offline. */ -"Unable to trash posts while offline. Please try again later." = "Não é possível enviar posts para a lixeira no modo off-line. Tente novamente mais tarde."; - /* Notice title when turning site notifications off fails. */ "Unable to turn off site notifications" = "Não foi possível desativar as notificações do site"; @@ -8220,8 +7936,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ Button title. Reverts the previous notification operation Revert an operation Revert enabling notification after successfully subcribing to the comments for the post. - The title of an 'undo' button. Tapping the button moves a trashed page out of the trash folder. - The title of an 'undo' button. Tapping the button moves a trashed post out of the trash folder. Undo action */ "Undo" = "Desfazer"; @@ -8261,9 +7975,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title for Unknown HTML Editor */ "Unknown HTML" = "HTML desconhecido"; -/* Label to use when creation date from media asset is not know. */ -"Unknown creation date" = "Data de criação desconhecida"; - /* No comment provided by engineer. */ "Unknown error" = "Erro desconhecido"; @@ -8501,15 +8212,10 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of an alert informing users that the video they are trying to select is not allowed. */ "Video not uploaded" = "Vídeo não enviado"; -/* Accessibility label for video thumbnails in the media collection view. The parameter is the creation date of the video. */ -"Video, %@" = "Vídeo, %@"; - /* Period Stats 'Videos' header */ "Videos" = "Vídeos"; /* Button title. Displays a summary / sharing screen for a specific post. - Label for a button that opens the page when tapped. - Label for the view post button. Tapping displays the post as it appears on the web. Opens the post epilogue screen to allow sharing / viewing of a post. Theme View action title Verb. The screen title shown when viewing a post inside the app. */ @@ -9022,9 +8728,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Siri Suggestion to open Support */ "WordPress Help" = "Ajuda do WordPress"; -/* Name for the WordPress Media Library */ -"WordPress Media" = "Mídia do WordPress"; - /* Accessibility label for selecting an image or video from the user's WordPress media library on formatting toolbar. */ "WordPress Media Library" = "Biblioteca de mídia do WordPress"; @@ -9339,9 +9042,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Share extension error dialog text. */ "Your account does not have permission to upload media to this site. The Site Administrator can change these permissions." = "Sua conta não possui permissão para enviar arquivos para esse site. O administrador do site pode alterar essas permissões."; -/* Explaining to the user why the app needs access to the device media library. */ -"Your app is not authorized to access media library due to active restrictions such as parental controls. Please check your parental control settings in this device." = "Seu aplicativo não está autorizado a acessar a biblioteca de mídia devido restrições como controle dos pais. Verifique as configurações de controle dos pais neste dispositivo."; - /* Title for the Jetpack Backup Complete message. */ "Your backup is now available for download" = "Seu backup já está disponível para download"; @@ -9360,9 +9060,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of the site address section in the Domains Dashboard. */ "Your free WordPress.com address is" = "Seu endereço gratuito do WordPress.com é"; -/* Error message when picked media cannot be imported into stories. */ -"Your media could not be exported. If the problem persists you can contact us via the Me > Help & Support screen." = "Não foi possível exportar seu arquivo de mídia. Caso o problema persista, entre em contato conosco por meio da tela Eu > Ajuda e suporte."; - /* Details about recently acquired domain on domain credit redemption success screen */ "Your new domain %@ is being set up. It may take up to 30 minutes for your domain to start working." = "Seu novo domínio %@ está sendo configurado. O domínio pode levar até 30 minutos para começar a funcionar."; @@ -9486,9 +9183,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* This is the string we display when prompting the user to review the WordPress app */ "appRatings.wordpress.prompt" = "O que você acha do WordPress?"; -/* Label displayed on audio media items. */ -"audio" = "áudio"; - /* translators: displays audio file extension. e.g. MP3 audio file */ "audio file" = "arquivo de áudio"; @@ -9534,6 +9228,40 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Action shown in a bottom notice to dismiss it. */ "blogDashboard.dismiss" = "Dispensar"; +/* Title for a button that, when tapped, shows more info about participating in Bloganuary. */ +"bloganuary.dashboard.card.button.learnMore" = "Saiba mais"; + +/* Title for the Bloganuary dashboard card while Bloganuary is running. */ +"bloganuary.dashboard.card.runningTitle" = "O Bloganuary está aqui!"; + +/* Title for the Bloganuary dashboard card. */ +"bloganuary.dashboard.card.title" = "O Bloganuary está chegando!"; + +/* Title of a button that calls the user to enable the Blogging Prompts feature. */ +"bloganuary.learnMore.modal.button.promptsDisabled" = "Ativar sugestões de publicação"; + +/* Title of a button that will dismiss the Bloganuary modal when tapped. +Note that the word 'go' here should have a closer meaning to 'start' rather than 'move forward'. */ +"bloganuary.learnMore.modal.button.promptsEnabled" = "Vamos lá!"; + +/* The second line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.description.second" = "Publique sua resposta."; + +/* The third line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.description.third" = "Leia as respostas de outros blogueiros para obter inspiração e criar novos laços."; + +/* The first line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.descriptions.first" = "Receba uma nova sugestão por dia para se inspirar."; + +/* An additional piece of information shown in case the user has the Blogging Prompts feature disabled. */ +"bloganuary.learnMore.modal.footer.addition" = "Para participar do Bloganuary, você precisa ativar as Sugestões de publicação."; + +/* An informative excerpt shown in a subtler tone. */ +"bloganuary.learnMore.modal.footer.text" = "O Bloganuary usará sugestões de publicação diárias para enviar tópicos do mês de janeiro para você."; + +/* The headline text of the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.headline" = "Participe do nosso desafio de escrita do mês"; + /* Verb. Dismisses the blogging prompt notification. */ "bloggingPrompt.pushNotification.customActionDescription.dismiss" = "Dispensar"; @@ -9692,9 +9420,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* (placeholder) Help the user enter a URL into the field */ "http:\/\/my-site-address (URL)" = "http:\/\/endereco-do-meu-site (URL)"; -/* Label displayed on image media items. */ -"image" = "imagem"; - /* Sentence to justify why the app is asking permission from the user to use their camera. */ "infoplist.NSCameraUsageDescription" = "Para tirar fotos ou gravar vídeos para usar nos seus posts."; @@ -9995,9 +9720,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Indicating that referrer was marked as spam */ "marked as spam" = "marcado como spam"; -/* Verb. User action to dismiss error alert when failing to load media item. */ -"mediaItemTable.errorAlert.dismissButton" = "Dispensar"; - /* Verb. Button title. Tapping dismisses a prompt. */ "mediaLibrary.retryOptionsAlert.dismissButton" = "Dispensar"; @@ -10016,9 +9738,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* The title in the Delete WordPress screen */ "migration.deleteWordpress.title" = "Você não precisa mais do aplicativo WordPress em seu dispositivo"; -/* Primary button title in the migration done screen. */ -"migration.done.actions.primary.title" = "Concluir"; - /* Footer for the migration done screen. */ "migration.done.footer" = "Recomendamos que você desinstale o aplicativo WordPress de seu dispositivo para evitar conflitos de dados."; @@ -10118,15 +9837,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* This is one of the buttons we display when prompting the user for a review */ "notifications.appRatings.sendFeedback.yes.buttonTitle" = "Enviar feedback"; -/* Word separating the current index from the total amount. I.e.: 7 of 9 */ -"of" = "de"; - -/* Label displayed on media items that are not video, image, or audio. */ -"other" = "outro"; - -/* Promote the page with Blaze. */ -"pages.blaze.actionTitle" = "Promova com Blaze"; - /* No comment provided by engineer. */ "password" = "senha"; @@ -10463,9 +10173,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Displayed in the confirmation alert when marking unread notifications as read. */ "unread" = "não lida"; -/* Label displayed on video media items. */ -"video" = "vídeo"; - /* Portion of a message for Jetpack users that have multisite WP installation, thus Restore is not available. This part is a link, colored with a different color. */ "visit our documentation page" = "visite a nossa página de documentação"; diff --git a/WordPress/Resources/pt.lproj/Localizable.strings b/WordPress/Resources/pt.lproj/Localizable.strings index 77e5d7e53eac..e7b06514311f 100644 --- a/WordPress/Resources/pt.lproj/Localizable.strings +++ b/WordPress/Resources/pt.lproj/Localizable.strings @@ -54,9 +54,6 @@ /* Age between dates over one year. */ "%d years" = "%d anos"; -/* Max image size in pixels (e.g. 300x300px) */ -"%dx%dpx" = "%1$dx%2$dpx"; - /* One menu area available in the theme */ "%i menu area in this theme" = "%i área de menu neste tema"; @@ -147,13 +144,6 @@ /* No comment provided by engineer. */ "Activity Logs" = "Relatórios de actividade"; -/* Accessibility label for add button to add items to the user's media library - Remove asset from media picker list */ -"Add" = "Adicionar"; - -/* Action for Media Picker to indicate selection of media. The argument in the string represents the number of elements (as numeric digits) selected */ -"Add %@" = "Adicionar %@"; - /* The title on the add category screen */ "Add a Category" = "Adicionar categoria"; @@ -194,9 +184,6 @@ /* Title for the advanced section in site settings screen */ "Advanced" = "Avançadas"; -/* Description of albums in the photo libraries */ -"Albums" = "Álbuns"; - /* Image alignment option title. Title of the screen for choosing an image's alignment. */ "Alignment" = "Alinhamento"; @@ -305,15 +292,9 @@ /* Message prompting the user to confirm that they want to disconnect Jetpack from the site. */ "Are you sure you want to disconnect Jetpack from the site?" = "Tem a certeza que quer desligar o Jetpack do site?"; -/* Message prompting the user to confirm that they want to permanently delete a group of media items. */ -"Are you sure you want to permanently delete these items?" = "Tem a certeza que pretende eliminar permanentemente estes itens?"; - /* Message of the trash confirmation alert. */ "Are you sure you want to trash this post?" = "Tem a certeza que pretende mover este artigo para o lixo?"; -/* Accessibility label for audio items in the media collection view. The parameter is the creation date of the audio. */ -"Audio, %@" = "Áudio, %@"; - /* No comment provided by engineer. */ "Authenticating" = "A autenticar"; @@ -396,8 +377,7 @@ /* Label for size of media while it's being calculated. */ "Calculating..." = "A calcular..."; -/* Accessibility label for taking an image or video with the camera on formatting toolbar. - Accessibility label for the camera tile in the collection view */ +/* Accessibility label for taking an image or video with the camera on formatting toolbar. */ "Camera" = "Câmara"; /* Alert dismissal title @@ -436,10 +416,7 @@ Cancels an Action Cancels an alert. Cancels the mark all as read action. - Dismiss the post action sheet Editing GIF alert cancel action button. - Label for a cancel button - Label for the auto-upload cancelation button in the post list. Tapping will prevent the app from auto-uploading the post. Menus cancel button for deleting a menu. Menus cancel button within text bar while editing items. Menus: Cancel button title for canceling an edited menu item. @@ -456,10 +433,6 @@ Verb. Title for Jetpack Restore cancel button. */ "Cancel" = "Cancelar"; -/* Label for the Post List option that cancels automatic uploading of a post. - Media Library option to cancel an in-progress or failed upload. */ -"Cancel Upload" = "Cancelar carregamento"; - /* Menus alert button title to cancel discarding changes and not createa a new menu. Menus alert button title to cancel discarding changes and not select a new menu Menus alert button title to cancel discarding changes and not select a new menu location */ @@ -684,10 +657,6 @@ /* Part of a prompt suggesting that there is more content for the user to read. */ "Continue reading" = "Continuar a ler"; -/* Copy the post url and paste anywhere in phone - Label for page copy link. Tapping copy the url of page */ -"Copy Link" = "Copiar link"; - /* Title of an prompt letting the user know there was a problem saving. */ "Could Not Save Changes" = "Não foi possível guardar alterações"; @@ -718,9 +687,6 @@ /* The title for an alert that says to the user that the featured image he selected couldn't be uploaded. */ "Couldn't upload the featured image" = "Não foi possível carregar a imagem de destaque"; -/* Message to show while media data source is finding the number of items available. */ -"Counting media items..." = "A contar itens multimédia"; - /* Period Stats 'Countries' header */ "Countries" = "Países"; @@ -801,7 +767,6 @@ /* Delete Delete button title for the warning shown to the user when he refuses to re-login when the authToken is missing. Title for button that permanently deletes a media item (photo / video) - Title for button that permanently deletes one or more media items (photos / videos) Title of a delete button Title of the trash confirmation alert. */ "Delete" = "Eliminar"; @@ -809,10 +774,7 @@ /* Menus confirmation button for deleting a menu. */ "Delete Menu" = "Eliminar menu"; -/* Delete option in the confirmation alert when deleting a page from the trash. - Delete option in the confirmation alert when deleting a post from the trash. - Label for a button permanently deletes a page. - Label for the delete post option. Tapping permanently deletes a post. +/* Delete option in the confirmation alert when deleting a post from the trash. Title for button on the comment details page that deletes the comment when tapped. */ "Delete Permanently" = "Eliminar permanentemente"; @@ -883,7 +845,6 @@ /* Accessibility label for button to dismiss a bottom sheet Accessibility label for the transparent space above the login dialog which acts as a button to dismiss the dialog. Accessibility label for the transparent space above the signup dialog which acts as a button to dismiss the dialog. - Action to show on alert when view asset fails. Dismiss a view. Verb */ "Dismiss" = "Ignorar"; @@ -892,9 +853,6 @@ User's Display Name */ "Display Name" = "Nome a mostrar"; -/* Accessibility label for other media items in the media collection view. The parameter is the filename file. */ -"Document: %@" = "Documento: %@"; - /* Noun. Title. Links to the Domains screen. */ "Domains" = "Domínios"; @@ -922,17 +880,12 @@ /* Name for the status of a draft post. */ "Draft" = "Rascunho"; -/* Title of the drafts filter. This filter shows a list of draft posts. - Title of the drafts header in search list. */ +/* Title of the drafts filter. This filter shows a list of draft posts. */ "Drafts" = "Rascunhos"; -/* Accessibility label for edit button to enable multi selection mode in the user's media library - Edit the post. - Editing GIF alert default action button. +/* Editing GIF alert default action button. Edits a Comment Edits the comment - Label for a button that opens the Edit Page view controller - Label for the edit post button. Tapping displays the editor. User action to edit media details. Verb, edit a comment */ "Edit" = "Editar"; @@ -1035,9 +988,6 @@ /* Title of error dialog when updating jetpack settins fail. */ "Error updating Jetpack settings" = "Erro ao actualizar as definições do Jetpack"; -/* Label for the post author filter. This filter shows posts for all users on the blog. */ -"Everyone" = "Todos"; - /* Label for the excerpt field. Should be the same as WP core. */ "Excerpt" = "Excerto"; @@ -1268,9 +1218,6 @@ /* Hint for image title on image settings. */ "Image title" = "Título da imagem"; -/* Accessibility label for image thumbnails in the media collection view. The parameter is the creation date of the image. */ -"Image, %@" = "Imagem, %@"; - /* Undated post time label */ "Immediately" = "Imediatamente"; @@ -1453,15 +1400,9 @@ /* Text displayed while loading the activity feed for a site */ "Loading Activities..." = "A carregar actividades..."; -/* Phrase to show when the user has searched for GIFs and they are being loaded. */ -"Loading GIFs..." = "A carregar GIFs..."; - /* Menus label text displayed while menus are loading */ "Loading Menus..." = "A carregar menus..."; -/* Phrase to show when the user has searched for images and they are being loaded. */ -"Loading Photos..." = "A carregar as fotografias..."; - /* Text displayed while loading plans details */ "Loading Plan..." = "A carregar plano..."; @@ -1565,7 +1506,6 @@ "Max Video Upload Size" = "Resolução máxima dos vídeos"; /* Accessibility label for the Me button in My Site. - Label for the post author filter. This filter shows posts only authored by the current user. Me page title Noun. Title. Links to the Me screen. The accessibility value of the me tab. @@ -1573,9 +1513,7 @@ "Me" = "Eu"; /* Noun. Title. Links to the blog's Media library. - Tab bar title for the Media tab in Media Picker The menu item to select during a guided tour. - Title for Media Library section of the app. Title for the media section in site settings screen Title label for the media settings section in the app settings */ "Media" = "Multimédia"; @@ -1583,13 +1521,6 @@ /* Label for size of media cache in the app. */ "Media Cache Size" = "Tamanho da cache de multimédia"; -/* Title for alert when access to media capture is not granted */ -"Media Capture" = "Captura de multimédia"; - -/* Title for alert when a generic error happened when loading media - Title for alert when access to the media library is not granted by the user */ -"Media Library" = "Biblioteca multimédia"; - /* Title for action sheet with media options. */ "Media Options" = "Opções de multimédia"; @@ -1600,9 +1531,6 @@ /* Message to indicate progress of uploading media to server */ "Media Uploading" = "A carregar multimédia"; -/* Alert title when there is issues loading an asset to preview. */ -"Media preview failed." = "Falhou ao pré-visualizar multimédia."; - /* Medium image size. Should be the same as in core WP. */ "Medium" = "Médio"; @@ -1622,9 +1550,6 @@ Label for the share message field on the post settings. */ "Message" = "Mensagem"; -/* Title of section containing image / video metadata such as size and file type */ -"Metadata" = "Metadados"; - /* Title of Months stats filter. */ "Months" = "Meses"; @@ -1632,28 +1557,19 @@ "Months and Years" = "Meses e anos"; /* Accessibility label for more button in dashboard quick start card. - Accessibility label for the More button in Page List. Accessibility label for the More button in Post List (compact view). Accessibility label for the More button on formatting toolbar. Accessibility label for the More button on Reader Cell Accessibility label for the More button on Reader's post details - Action button to display more available options - Label for the more post button. Tapping displays an action sheet with post options. */ + Action button to display more available options */ "More" = "Mais"; /* Action button to display more available options Label for the More Options area in post settings. Should use the same translation as core WP. */ "More Options" = "Mais opções"; -/* Label for a button that moves a page to the draft folder - Label for an option that moves a post to the draft folder */ -"Move to Draft" = "Mover para rascunhos"; - -/* Label for a button that moves a page to the trash folder - Label for a option that moves a post to the trash folder - Title for button on the comment details page that moves the comment to trash when tapped. +/* Title for button on the comment details page that moves the comment to trash when tapped. Trash option in the trash confirmation alert. - Trash option in the trash page confirmation alert. Trashes the comment */ "Move to Trash" = "Mover para o lixo"; @@ -1669,7 +1585,8 @@ Title of My Site tab */ "My Site" = "O meu site"; -/* Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ +/* Title for site picker screen. + Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ "My Sites" = "Os meus sites"; /* 'Need help?' button label, links off to the WP for iOS FAQ. */ @@ -1773,9 +1690,6 @@ /* Shown when user is loading Menu item options and no items are available, such as posts, pages, etc. */ "Nothing found." = "Nada encontrado."; -/* Default message for empty media picker */ -"Nothing to show" = "Nada para mostrar"; - /* Link to Notification Settings section Title displayed in the Notification settings Title of the 'Notification Settings' screen within the 'Me' tab - used for spotlight indexing on iOS. */ @@ -1808,7 +1722,6 @@ Alert dismissal title Button title. Acknowledges a prompt. Button title. An acknowledgement of the message displayed in a prompt. - Confirmation of action Default action Dismisses the alert Marks all notifications as read. @@ -1926,18 +1839,6 @@ Title shown when selecting a post type of Page from the Share Extension. */ "Page" = "Página"; -/* Prompts the user that a restored page was moved to the drafts list. */ -"Page Restored to Drafts" = "Página restaurada para os rascunhos."; - -/* Prompts the user that a restored page was moved to the published list. */ -"Page Restored to Published" = "Página restaurada e publicada."; - -/* Prompts the user that a restored page was moved to the scheduled list. */ -"Page Restored to Scheduled" = "Página restaurada e agendada."; - -/* A short message explaining that a page was moved to the trash bin. */ -"Page moved to trash." = "Página movida para o lixo."; - /* Noun. Title. Links to the blog's Pages screen. The item to select during a guided tour. This is the section title @@ -1970,8 +1871,7 @@ Title of pending Comments filter. */ "Pending" = "Pendente"; -/* Name for the status of a post pending review. - Title of the Pending Review Badge */ +/* Name for the status of a post pending review. */ "Pending review" = "Pendente de revisão"; /* Noun. Title of the people management feature. @@ -2060,15 +1960,6 @@ The post formats available for the post. Should be the same as in core WP. */ "Post Format" = "Formato do artigo"; -/* Prompts the user that a restored post was moved to the drafts list. */ -"Post Restored to Drafts" = "Conteúdo restaurado para os rascunhos."; - -/* Prompts the user that a restored post was moved to the published list. */ -"Post Restored to Published" = "Conteúdo restaurado e publicado."; - -/* Prompts the user that a restored post was moved to the scheduled list. */ -"Post Restored to Scheduled" = "Conteúdo restaurado e agendado."; - /* Name of the button to open the post settings The title of the Post Settings screen. */ "Post Settings" = "Definições do Artigo"; @@ -2076,9 +1967,6 @@ /* The footer text appears within the footer displaying when the post has been created. */ "Post created on %@" = "Artigo criado em %@"; -/* A short message explaining that a post was moved to the trash bin. */ -"Post moved to trash." = "Conteúdo movido para o lixo."; - /* Register Domain - Address information field Postal Code */ "Postal Code" = "Código postal"; @@ -2138,8 +2026,7 @@ "Privacy Settings" = "Definições de privacidade"; /* Name for the status of a post that is marked private. - Privacy setting for posts set to 'Private'. Should be the same as in core WP. - Title of the Private Badge */ + Privacy setting for posts set to 'Private'. Should be the same as in core WP. */ "Private" = "Privada"; /* Menu item label for linking a project page. */ @@ -2156,19 +2043,16 @@ Title for the publish settings view */ "Publish" = "Publicar"; -/* A short phrase indicating a post is due to be immedately published. - Label for a button that moves a page to the published folder, publishing with the current date/time. */ +/* A short phrase indicating a post is due to be immedately published. */ "Publish Immediately" = "Publicar imediatamente"; /* Label for a button that publishes the post - Label for an option that moves a publishes a post immediately Title of button allowing the user to immediately publish the post they are editing. */ "Publish Now" = "Publicar agora"; /* Name for the status of a published post. Period Stats 'Published' header - Title of the published filter. This filter shows a list of posts that the user has published. - Title of the published header in search list. */ + Title of the published filter. This filter shows a list of posts that the user has published. */ "Published" = "Publicado"; /* Precedes the name of the blog just posted on */ @@ -2228,8 +2112,7 @@ /* Label for selecting the related posts options */ "Related Posts" = "Artigos relacionados"; -/* Add asset to media picker list - Alert button to confirm a plugin to be removed +/* Alert button to confirm a plugin to be removed Button label when removing a blog Label for the Remove Feature Image icon. Tapping will show a confirmation screen for removing the feature image from the post. Remove Action @@ -2303,9 +2186,6 @@ /* Title of secondary button on alert prompting verify their accounts while attempting to publish */ "Resend" = "Reenviar"; -/* Title of the reset button */ -"Reset" = "Repor"; - /* Screen title. Resize and crop an image. */ "Resize & Crop" = "Redimensionar e cortar"; @@ -2326,12 +2206,9 @@ Button title, displayed when media has failed to upload. Allows the user to try the upload again. Button title. Retries uploading a post. If a user taps the button with this label, the action that evinced this error view will be retried. - Label for a button that attempts to re-upload a page that previously failed to upload. - Label for the retry post upload button. Tapping attempts to upload the post again. Opens the media library . Retries the upload of a user's post. Retry updating User's Role - Retry uploading the post. Retry. Action Retry. Verb – retry a failed media upload. The Jetpack view button title used when an error occurred @@ -2340,9 +2217,6 @@ User action to retry media upload. */ "Retry" = "Tentar de novo"; -/* User action to retry media upload. */ -"Retry Upload" = "Tentar carregar de novo"; - /* No comment provided by engineer. */ "Retry?" = "Tentar de novo?"; @@ -2379,11 +2253,7 @@ /* Title of button allowing users to change the status of the post they are currently editing to Draft. */ "Save as Draft" = "Guardar como rascunho"; -/* Text displayed in HUD when a media item's metadata (title, etc) is saved successfully. */ -"Saved!" = "Guardado!"; - /* Menus save button title while it is saving a Menu. - Text displayed in HUD while a media item's metadata (title, etc) is being saved. Text displayed in HUD while a post is being saved as a draft. */ "Saving..." = "A guardar..."; @@ -2423,9 +2293,6 @@ /* Blog Picker's Title */ "Select Site" = "Seleccionar site"; -/* Accessibility hint for actions when displaying media items. */ -"Select media." = "Seleccionar multimédia."; - /* Accessibility label for selecting paragraph style button on formatting toolbar. */ "Select paragraph style" = "Seleccione o estilo do parágrafo"; @@ -2454,7 +2321,6 @@ /* Accessibility label for share buttons in nav bars Button label to share a post Button label to share a web page - Share the post. Shares the comment URL Spoken accessibility label Title for a button that allows the user to share their answer to the prompt. @@ -2621,8 +2487,7 @@ Title of Start Over settings page */ "Start Over" = "Começar de novo"; -/* Label for post stats option. Tapping displays statistics for a post. - Noun. Abbreviation of Statistics. Name of the Stats feature +/* Noun. Abbreviation of Statistics. Name of the Stats feature Noun. Abbv. of Statistics. Links to a blog's Stats screen. Stats 3D Touch Shortcut Stats window title @@ -2658,7 +2523,7 @@ Theme Support action title */ "Support" = "Suporte"; -/* Button used to switch site */ +/* Title for back button that leads to the site picker screen. */ "Switch Site" = "Mudar de site"; /* Switches the Editor to HTML Mode */ @@ -2768,7 +2633,7 @@ /* Should be the same as the text displayed if the user clicks the (i) in Slug in Calypso. */ "The slug is the URL-friendly version of the post title." = "O URL personalizado para o título do artigo."; -/* Error message informing a user about an invalid password. */ +/* No comment provided by engineer. */ "The username or password stored in the app may be out of date. Please re-enter your password in the settings and try again." = "O nome de utilizador ou senha armazenada na aplicação podem estar desactualizados. Por favor re-digite sua senha nas configurações e tente novamente."; /* Message shown when a video failed to load while trying to add it to the Media library. */ @@ -2795,18 +2660,12 @@ /* A short error message letting the user know about a problem displaying a post. */ "There was a problem displaying this post." = "Ocorreu um problema ao mostrar este artigo."; -/* Error message displayed when the Media Library is unable to load a full sized preview of an item. */ -"There was a problem loading the media item." = "Ocorreu um erro ao carregar o item multimédia."; - /* Error message informing the user that there was a problem clearing the block on site preventing its posts from displaying in the reader. */ "There was a problem removing the block for specified site." = "Ocorreu um problema ao remover o bloco do site especificado."; /* A short error message shown in a prompt. */ "There was a problem saving changes to sharing management." = "Ocorreu um erro ao guardar alterações da gestão de partilha."; -/* Explaining to the user there was an generic error accesing media. */ -"There was a problem when trying to access your media. Please try again later." = "Ocorreu um problema ao tentar aceder à sua biblioteca multimédia. Por favor tente de novo mais tarde."; - /* Text displayed when there is a failure loading the activity feed */ "There was an error loading activities" = "Ocorreu um erro ao carregar actividades"; @@ -2828,9 +2687,6 @@ /* An error message display if the users device does not have a camera input available */ "This app needs permission to access the Camera to capture new media, please change the privacy settings if you wish to allow this." = "WordPress precisa de autorização para aceder à câmara do seu dispositivo para poder adicionar fotos e vídeos aos seus artigos. Por favor, actualize as suas definições de privacidade."; -/* Explaining to the user why the app needs access to the device media library. */ -"This app needs permission to access your device media library in order to add photos and\/or video to your posts. Please change the privacy settings if you wish to allow this." = "WordPress precisa de autorização para aceder à biblioteca multimédia do seu dispositivo para poder adicionar fotos e vídeos aos seus artigos. Por favor, actualize as suas definições de privacidade."; - /* An error message informing the user the email address they entered did not match a WordPress.com account. */ "This email address is not registered on WordPress.com." = "Este endereço de email não está registado em WordPress.com."; @@ -2908,8 +2764,7 @@ /* Title for the traffic section in site settings screen */ "Traffic" = "Tráfego"; -/* Accessibility label for trash button to delete items from the user's media library - Accessibility label for trash buttons in nav bars +/* Accessibility label for trash buttons in nav bars Trashes a comment Trashes the comment */ "Trash" = "Lixo"; @@ -2965,18 +2820,12 @@ /* Shown when a user logs in with Google but it subsequently fails to work as login to WordPress.com */ "Unable To Connect" = "Não é possível ligar"; -/* An error message. */ -"Unable to Connect" = "Não é possível ligar"; - /* Title of a prompt saying the app needs an internet connection before it can load posts */ "Unable to Load Posts" = "Não foi possível carregar os conteúdos"; /* Title of error prompt shown when a sync the user initiated fails. */ "Unable to Sync" = "Não é possível sincronizar"; -/* Text displayed in HUD if there was an error attempting to delete a group of media items. */ -"Unable to delete all media items." = "Não é possível eliminar todos os itens multimédia."; - /* Text displayed in HUD if there was an error attempting to delete a media item. */ "Unable to delete media item." = "Não é possível eliminar o item multimédia."; @@ -3013,8 +2862,6 @@ Button title. Reverts the previous notification operation Revert an operation Revert enabling notification after successfully subcribing to the comments for the post. - The title of an 'undo' button. Tapping the button moves a trashed page out of the trash folder. - The title of an 'undo' button. Tapping the button moves a trashed post out of the trash folder. Undo action */ "Undo" = "Anular"; @@ -3035,9 +2882,6 @@ /* Title for Unknown HTML Editor */ "Unknown HTML" = "HTML desconhecido"; -/* Label to use when creation date from media asset is not know. */ -"Unknown creation date" = "Data de criação desconhecida"; - /* No comment provided by engineer. */ "Unknown error" = "Erro desconhecido"; @@ -3143,15 +2987,10 @@ /* Message shown if a video preview image is unavailable while the video is being uploaded. */ "Video Preview Unavailable" = "Pré-visualização de vídeo indisponível"; -/* Accessibility label for video thumbnails in the media collection view. The parameter is the creation date of the video. */ -"Video, %@" = "Vídeo, %@"; - /* Period Stats 'Videos' header */ "Videos" = "Vídeos"; /* Button title. Displays a summary / sharing screen for a specific post. - Label for a button that opens the page when tapped. - Label for the view post button. Tapping displays the post as it appears on the web. Opens the post epilogue screen to allow sharing / viewing of a post. Theme View action title Verb. The screen title shown when viewing a post inside the app. */ @@ -3384,18 +3223,12 @@ /* Age between dates equaling one hour. */ "an hour" = "uma hora"; -/* Label displayed on audio media items. */ -"audio" = "áudio"; - /* Used when displaying author of a plugin. */ "by %@" = "por %@"; /* (placeholder) Help the user enter a URL into the field */ "http:\/\/my-site-address (URL)" = "http:\/\/endereço-do-meu-site (URL)"; -/* Label displayed on image media items. */ -"image" = "imagem"; - /* This text is used when the user is configuring the iOS widget to suggest them to select the site to configure the widget for */ "ios-widget.gpCwrM" = "Select Site"; @@ -3405,15 +3238,9 @@ /* Later today */ "later today" = "mais logo"; -/* Label displayed on media items that are not video, image, or audio. */ -"other" = "outros"; - /* Used when the response doesn't have a valid url to display */ "unknown url" = "URL desconhecido"; -/* Label displayed on video media items. */ -"video" = "vídeo"; - /* Title of posts label in all time widget */ "widget.alltime.posts.label" = "Conteúdos"; diff --git a/WordPress/Resources/release_notes.txt b/WordPress/Resources/release_notes.txt index 5933c47f05ed..52afb2999ddb 100644 --- a/WordPress/Resources/release_notes.txt +++ b/WordPress/Resources/release_notes.txt @@ -1,7 +1,14 @@ -In the block editor, you can now split or exit a formatted block by pressing the “enter” key three times. The left-hand border is always visible for quote blocks, too. And you can quote us on that. +* [**] [internal] A minor refactor in authentication flow, including but not limited to social sign-in and two factor authentication. [#22086] +* [**] [internal] Refactor domain selection flows to use the same domain selection UI. [22254] +* [**] Re-enable the support for using Security Keys as a second factor during login [#22258] +* [*] Fix crash in editor that sometimes happens after modifying tags or categories [#22265] +* [*] Add defensive code to make sure the retain cycles in the editor don't lead to crashes [#22252] +* [**] [internal] Add support for the Phase One Fast Media Uploads banner [#22330] +* [*] [internal] Remove personalizeHomeTab feature flag [#22280] +* [*] Fix a rare crash in post search related to tags [#22275] +* [*] Fix a rare crash when deleting posts [#22277] +* [*] Fix a rare crash in Site Media prefetching cancellation [#22278] +* [*] Fix an issue with BlogDashboardPersonalizationService being used on the background thread [#22335] +* [***] Block Editor: Avoid keyboard dismiss when interacting with text blocks [https://github.com/WordPress/gutenberg/pull/57070] +* [**] Block Editor: Auto-scroll upon block insertion [https://github.com/WordPress/gutenberg/pull/57273] -We also squashed a handful of bugs. - -- We fixed a code issue in blogging prompt settings that caused the app to crash. -- During the signup process, you won’t end up in the reader by accident. -- Creating a .com site? You should no longer see two overlays after completing the signup process. One and done. diff --git a/WordPress/Resources/ro.lproj/Localizable.strings b/WordPress/Resources/ro.lproj/Localizable.strings index 32399ba613f8..0880f032a6ed 100644 --- a/WordPress/Resources/ro.lproj/Localizable.strings +++ b/WordPress/Resources/ro.lproj/Localizable.strings @@ -1,4 +1,4 @@ -/* Translation-Revision-Date: 2023-10-17 05:45:52+0000 */ +/* Translation-Revision-Date: 2024-01-04 07:30:34+0000 */ /* Plural-Forms: nplurals=3; plural=(n == 1) ? 0 : ((n == 0 || n % 100 >= 2 && n % 100 <= 19) ? 1 : 2); */ /* Generator: GlotPress/4.0.0-alpha.11 */ /* Language: ro */ @@ -152,9 +152,6 @@ /* Difference label for Period Overview stat, indicating change from previous period. Ex: +99.9K (5%) */ "%@%@ (%@%%)" = "%1$@%2$@ (%3$@%%)"; -/* Accessibility label for a post in the post list. The parameters are the title, and date respectively. For example, \"Let it Go, 1 hour ago.\" */ -"%@, %@." = "%1$@, %2$@."; - /* Accessibility value for a Stats' Posting Activity Month if the user selected a day with posts. The first parameter is day (e.g. November 2019). The second parameter is the number of posts. */ "%@. %d posts." = "%1$@. %2$d articole."; @@ -210,9 +207,6 @@ /* Age between dates over one year. */ "%d years" = "%d ani"; -/* Max image size in pixels (e.g. 300x300px) */ -"%dx%dpx" = "%1$dx%2$dpx"; - /* One menu area available in the theme */ "%i menu area in this theme" = "%i zonă meniu în această temă"; @@ -271,6 +265,9 @@ translators: Block name. %s: The localized block name */ /* translators: %s: social link name e.g: \"Instagram\". */ "%s social icon" = "Icon social %s"; +/* translators: displayed right after the classic block is converted to blocks. %s: The localized classic block name */ +"'%s' block converted to blocks" = "Am convertit blocul „%s” în blocuri"; + /* translators: Missing block alert title. %s: The localized block name */ "'%s' is not fully-supported" = "„%s” nu este acceptat în totalitate"; @@ -478,13 +475,6 @@ translators: Block name. %s: The localized block name */ /* Label for the Activity Type filter button when there are more than 1 activity type selected */ "Activity Type (%1$d)" = "Tip de activitate (%1$d)"; -/* Accessibility label for add button to add items to the user's media library - Remove asset from media picker list */ -"Add" = "Adaugă"; - -/* Action for Media Picker to indicate selection of media. The argument in the string represents the number of elements (as numeric digits) selected */ -"Add %@" = "Adaugă %@"; - /* No comment provided by engineer. */ "Add Block After" = "Adaugă bloc după"; @@ -581,9 +571,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Add menu item to children" = "Adaugă element meniu la copil"; -/* Accessibility hint for add button to add items to the user's media library */ -"Add new media" = "Adaugă element media nou"; - /* Screen reader text for Menus button that adds a new menu to a site. */ "Add new menu" = "Adaugă meniu nou"; @@ -649,9 +636,6 @@ translators: Block name. %s: The localized block name */ /* Option to select the Airmail app when logging in with magic links */ "Airmail" = "Airmail"; -/* Description of albums in the photo libraries */ -"Albums" = "Albume"; - /* Image alignment option title. Title of the screen for choosing an image's alignment. */ "Alignment" = "Aliniere"; @@ -665,6 +649,9 @@ translators: Block name. %s: The localized block name */ Title of the drafts filter. This filter shows a list of draft posts. */ "All" = "Toate"; +/* Information about redeeming domain credit on site dashboard. */ +"All WordPress.com annual plans include a custom domain name. Register your free domain now." = "Toate planurile WordPress.com cu plată anuală includ un nume de domeniu personalizat. Înregistrează-ți domeniul gratuit acum."; + /* Footer of the free domain registration section for a paid plan. Information about redeeming domain credit on site dashboard. */ "All WordPress.com plans include a custom domain name. Register your free premium domain now." = "Toate planurile WordPress.com includ un nume de domeniu personalizat. Înregistrează-ți domeniul premium gratuit acum."; @@ -723,17 +710,20 @@ translators: Block name. %s: The localized block name */ "Allowlisted IP addresses" = "Adrese IP permise în lista"; /* Instructions for users with two-factor authentication enabled. */ -"Almost there! Please enter the verification code from your authenticator app." = "Aproape gata! Te rog introdu codul de verificare din aplicația ta Authenticator."; +"Almost there! Please enter the verification code from your authenticator app." = "Aproape gata! Te rog să introduci codul de verificare din aplicația Authenticator."; /* Image alt attribute option title. Label for the alt for a media asset (image) */ "Alt Text" = "Text alternativ"; /* No comment provided by engineer. */ -"Alternatively, you can detach and edit these blocks separately by tapping “Detach patterns”." = "Ca alternativă, poți să detașezi și să editezi separat aceste blocuri atingând „Detașează modulele”."; +"Alternatively, you can convert the content to blocks." = "Ca alternativă, poți să convertești conținutul în blocuri."; /* No comment provided by engineer. */ -"Alternatively, you can detach and edit this block separately by tapping “Detach pattern”." = "Ca alternativă, poți să detașezi și să editezi separat acest bloc atingând „Detașează modulele”."; +"Alternatively, you can detach and edit this block separately by tapping “Detach”." = "Ca alternativă, poți să detașezi și să editezi separat acest bloc atingând „Detașează”."; + +/* translators: Alternative option included in a warning related to having blocks deeply nested. */ +"Alternatively, you can flatten the content by ungrouping the block." = "Ca alternativă, poți să aplatizezi conținutul prin anularea grupării blocurilor."; /* Instruction text to explain to help users type their password instead of using magic link login option. */ "Alternatively, you may enter the password for this account." = "Ca alternativă, poți să introduci parola pentru acest cont."; @@ -882,15 +872,9 @@ translators: Block name. %s: The localized block name */ /* Message prompting the user to confirm that they want to disconnect Jetpack from the site. */ "Are you sure you want to disconnect Jetpack from the site?" = "Sigur vrei să deconectezi Jetpack de pe site?"; -/* Message prompting the user to confirm that they want to permanently delete a group of media items. */ -"Are you sure you want to permanently delete these items?" = "Sigur vrei să ștergi definitiv aceste elemente?"; - /* Message prompting the user to confirm that they want to permanently delete a media item. Should match Calypso. */ "Are you sure you want to permanently delete this item?" = "Sigur vrei să ștergi definitiv acest element?"; -/* Message of the confirmation alert when deleting a page from the trash. */ -"Are you sure you want to permanently delete this page?" = "Sigur vrei să ștergi definitiv această pagină?"; - /* Message of the confirmation alert when deleting a post from the trash. */ "Are you sure you want to permanently delete this post?" = "Sigur vrei să ștergi definitiv acest articol?"; @@ -922,9 +906,6 @@ translators: Block name. %s: The localized block name */ /* Title of message shown when user taps submit for review. */ "Are you sure you want to submit for review?" = "Sigur vrei să trimiți pentru revizuire?"; -/* Message of the trash page confirmation alert. */ -"Are you sure you want to trash this page?" = "Sigur vrei să arunci la gunoi această pagină?"; - /* Message of the trash confirmation alert. */ "Are you sure you want to trash this post?" = "Sigur vrei să arunci la gunoi acest articol?"; @@ -965,9 +946,6 @@ translators: Block name. %s: The localized block name */ /* translators: accessibility text. Empty Audio caption. */ "Audio caption. Empty" = "Text asociat audio. Gol"; -/* Accessibility label for audio items in the media collection view. The parameter is the creation date of the audio. */ -"Audio, %@" = "Audio, %@"; - /* No comment provided by engineer. */ "Authenticating" = "Autentificare"; @@ -1178,10 +1156,7 @@ translators: Block name. %s: The localized block name */ "Blocks menu" = "Meniu blocuri"; /* translators: Warning related to having blocks deeply nested. %d: The deepest nesting level. */ -"Blocks nested deeper than %d levels may not render properly in the mobile editor. For this reason, we recommend flattening the content by ungrouping the block or editing the block using the web editor." = "Blocurile imbricate în adâncime pe mai mult de %d niveluri pot să nu fie randate corect în editorul pentru dispozitive mobile. Din acest motiv, recomandăm aplatizarea conținutului prin anularea grupării blocului sau prin editarea blocului folosind editorul web."; - -/* translators: Warning related to having blocks deeply nested. %d: The deepest nesting level. */ -"Blocks nested deeper than %d levels may not render properly in the mobile editor. For this reason, we recommend flattening the content by ungrouping the block or editing the block using your web browser." = "Blocurile imbricate în adâncime pe mai mult de %d niveluri pot să nu fie randate corect în editorul pentru dispozitive mobile. Din acest motiv, recomandăm aplatizarea conținutului prin anularea grupării blocului sau prin editarea blocului folosind navigatorul tău web."; +"Blocks nested deeper than %d levels may not render properly in the mobile editor." = "Este posibil ca blocurile imbricate mai adânc de %d niveluri să nu fie randate corect în editorul aplicației pentru mobil."; /* Title of a button that displays the WordPress.com blog */ "Blog" = "Blog"; @@ -1259,9 +1234,6 @@ translators: Block name. %s: The localized block name */ /* Label for the post author in the post detail. */ "By " = "De"; -/* Accessibility label for the post author in the post list. The parameter is the author name. For example, \"By Elsa.\" */ -"By %@." = "De %@."; - /* Legal disclaimer for logging in. The underscores _..._ denote underline. */ "By continuing, you agree to our _Terms of Service_." = "Dacă continui, ești de acord cu _Termenii noștri de utilizare ai serviciului_."; @@ -1281,8 +1253,7 @@ translators: Block name. %s: The localized block name */ /* Label for size of media while it's being calculated. */ "Calculating..." = "Calculez..."; -/* Accessibility label for taking an image or video with the camera on formatting toolbar. - Accessibility label for the camera tile in the collection view */ +/* Accessibility label for taking an image or video with the camera on formatting toolbar. */ "Camera" = "Camera"; /* Title of an alert informing the user the camera permission for the app is disabled and its needed to proceed */ @@ -1333,10 +1304,7 @@ translators: Block name. %s: The localized block name */ Cancels an Action Cancels an alert. Cancels the mark all as read action. - Dismiss the post action sheet Editing GIF alert cancel action button. - Label for a cancel button - Label for the auto-upload cancelation button in the post list. Tapping will prevent the app from auto-uploading the post. Menus cancel button for deleting a menu. Menus cancel button within text bar while editing items. Menus: Cancel button title for canceling an edited menu item. @@ -1353,10 +1321,6 @@ translators: Block name. %s: The localized block name */ Verb. Title for Jetpack Restore cancel button. */ "Cancel" = "Anulare"; -/* Label for the Post List option that cancels automatic uploading of a post. - Media Library option to cancel an in-progress or failed upload. */ -"Cancel Upload" = "Anulează încărcarea"; - /* Menus alert button title to cancel discarding changes and not createa a new menu. Menus alert button title to cancel discarding changes and not select a new menu Menus alert button title to cancel discarding changes and not select a new menu location */ @@ -1415,9 +1379,6 @@ translators: Block name. %s: The localized block name */ Main title */ "Change Password" = "Schimbă parola"; -/* Title of button that takes user to the system Settings section for the app */ -"Change Settings" = "Modifică setările"; - /* Change Username title. */ "Change Username" = "Schimbă numele de utilizator"; @@ -1557,9 +1518,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Choose file" = "Alege fișierul"; -/* Menu option for selecting media from the device's photo library. */ -"Choose from My Device" = "Alege de pe dispozitivul meu"; - /* Explanatory text for Homepage Settings homepage type selection. */ "Choose from a homepage that displays your latest posts (classic blog) or a fixed \/ static page." = "Alege o primă pagină care să afișeze ultimele articole (blog clasic) sau o pagină fixă\/statică."; @@ -1760,9 +1718,6 @@ translators: Block name. %s: The localized block name */ /* Community & Non-Profit site intent topic */ "Community & Non-Profit" = "Comunități și organizații non-profit"; -/* Accessibility indication that the current Post List style is currently Compact. */ -"Compact" = "Compact"; - /* The action is completed */ "Completed" = "Terminat"; @@ -1948,10 +1903,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Copied block" = "Bloc copiat"; -/* Copy the post url and paste anywhere in phone - Label for page copy link. Tapping copy the url of page */ -"Copy Link" = "Copiază legătura"; - /* Copy link to oomment button title */ "Copy Link to Comment" = "Copiază legătura în comentariu"; @@ -2060,9 +2011,6 @@ translators: Block name. %s: The localized block name */ /* Error title displayed when unable to close user account. */ "Couldn’t close account automatically" = "Nu am putut închide automat contul"; -/* Message to show while media data source is finding the number of items available. */ -"Counting media items..." = "Număr elementele media..."; - /* Period Stats 'Countries' header */ "Countries" = "Țări"; @@ -2313,7 +2261,6 @@ translators: Block name. %s: The localized block name */ /* Delete Delete button title for the warning shown to the user when he refuses to re-login when the authToken is missing. Title for button that permanently deletes a media item (photo / video) - Title for button that permanently deletes one or more media items (photos / videos) Title of a delete button Title of the trash confirmation alert. */ "Delete" = "Ștergre"; @@ -2321,15 +2268,11 @@ translators: Block name. %s: The localized block name */ /* Menus confirmation button for deleting a menu. */ "Delete Menu" = "Șterge meniul"; -/* Delete option in the confirmation alert when deleting a page from the trash. - Delete option in the confirmation alert when deleting a post from the trash. - Label for a button permanently deletes a page. - Label for the delete post option. Tapping permanently deletes a post. +/* Delete option in the confirmation alert when deleting a post from the trash. Title for button on the comment details page that deletes the comment when tapped. */ "Delete Permanently" = "Șterge definitiv"; -/* Title of the confirmation alert when deleting a page from the trash. - Title of the confirmation alert when deleting a post from the trash. */ +/* Title of the confirmation alert when deleting a post from the trash. */ "Delete Permanently?" = "Ștergi definitiv?"; /* Button label for deleting the current site @@ -2448,7 +2391,6 @@ translators: Block name. %s: The localized block name */ /* Accessibility label for button to dismiss a bottom sheet Accessibility label for the transparent space above the login dialog which acts as a button to dismiss the dialog. Accessibility label for the transparent space above the signup dialog which acts as a button to dismiss the dialog. - Action to show on alert when view asset fails. Dismiss a view. Verb */ "Dismiss" = "Refuză"; @@ -2466,12 +2408,6 @@ translators: Block name. %s: The localized block name */ User's Display Name */ "Display Name" = "Nume de afișat"; -/* Accessibility label for other media items in the media collection view. The parameter is the creation date of the document. */ -"Document, %@" = "Document, %@"; - -/* Accessibility label for other media items in the media collection view. The parameter is the filename file. */ -"Document: %@" = "Document: %@"; - /* Message shown when all tours have been completed */ "Doesn't it feel good to cross things off a list?" = "Nu te simți bine când bifezi lucrurile dintr-o listă?"; @@ -2632,8 +2568,7 @@ translators: Block name. %s: The localized block name */ /* Description of a Quick Start Tour */ "Draft and publish a post." = "Redactează și publică un articol."; -/* Title of the drafts filter. This filter shows a list of draft posts. - Title of the drafts header in search list. */ +/* Title of the drafts filter. This filter shows a list of draft posts. */ "Drafts" = "Ciorne"; /* No comment provided by engineer. */ @@ -2645,10 +2580,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Drag to adjust focal point" = "Trage pentru a regla punctul de focalizare"; -/* Label for page duplicate option. Tapping creates a copy of the page. - Label for post duplicate option. Tapping creates a copy of the post. */ -"Duplicate" = "Fă duplicat"; - /* No comment provided by engineer. */ "Duplicate block" = "Fă un duplicat al blocului"; @@ -2661,13 +2592,9 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Each block has its own settings. To find them, tap on a block. Its settings will appear on the toolbar at the bottom of the screen." = "Fiecare bloc are setări proprii. Pentru a le găsi, atinge un bloc. Setările lui vor apărea în bara de unelte din partea de jos a ecranului."; -/* Accessibility label for edit button to enable multi selection mode in the user's media library - Edit the post. - Editing GIF alert default action button. +/* Editing GIF alert default action button. Edits a Comment Edits the comment - Label for a button that opens the Edit Page view controller - Label for the edit post button. Tapping displays the editor. User action to edit media details. Verb, edit a comment */ "Edit" = "Editare"; @@ -2675,9 +2602,6 @@ translators: Block name. %s: The localized block name */ /* Title for the edit more button section */ "Edit \"More\" button" = "Editare buton \"Mai mult\""; -/* Button that displays the media editor to the user */ -"Edit %@" = "Editează %@"; - /* Blocklist Keyword Edition Title */ "Edit Blocklist Word" = "Editează un cuvânt din Lista blocări"; @@ -2870,9 +2794,6 @@ translators: Block name. %s: The localized block name */ /* Secondary message shown when there are no domains that match the user entered text. */ "Enter different words above and we'll look for an address that matches it." = "Introdu cuvinte diferite mai sus și vom căuta o adresă care se potrivește cu ele."; -/* Accessibility hint for edit button to enable multi selection mode in the user's media library */ -"Enter edit mode to enable multi select to delete" = "Intră în modul editare pentru a activa selectarea mai multor elemente pentru ștergere"; - /* (placeholder) Help enter WordPress password Placeholder of a field to type a password to protect the post. */ "Enter password" = "Introdu parola"; @@ -3028,9 +2949,6 @@ translators: Block name. %s: The localized block name */ /* Short title telling the user they will receive a blogging reminder every day of the week. */ "Every day at %@" = "În fiecare zi la %@"; -/* Label for the post author filter. This filter shows posts for all users on the blog. */ -"Everyone" = "Oricine"; - /* Example story title description */ "Example story title" = "Exemplu de titlu narațiune"; @@ -3040,9 +2958,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Excerpt length (words)" = "Lungime rezumat (cuvinte)"; -/* Accessibility label for a post's excerpt in the post list. The parameter is the post excerpt. For example, \"Excerpt. This is the first paragraph.\" */ -"Excerpt. %@." = "Rezumat. %@."; - /* Should be the same as the text displayed if the user clicks the (i) in Calypso. */ "Excerpts are optional hand-crafted summaries of your content." = "Rezumatele sunt scurte expuneri manuale, opționale, ale conținutului tău."; @@ -3052,8 +2967,7 @@ translators: Block name. %s: The localized block name */ /* Accessibility Label for the exit full screen button on the full screen comment reply mode */ "Exit Full Screen" = "Ieși din ecranul complet"; -/* Accessibility indication that the current Post List style is currently Expanded. - Screen reader text to represent the expanded state of a UI control */ +/* Screen reader text to represent the expanded state of a UI control */ "Expanded" = "Extinse"; /* Accessibility hint */ @@ -3103,9 +3017,6 @@ translators: Block name. %s: The localized block name */ The action failed */ "Failed" = "Eșuat"; -/* Error title when picked media cannot be imported into stories. */ -"Failed Media Export" = "Exportul media a eșuat"; - /* Message for mark all as read success notice */ "Failed marking Notifications as read" = "Marcarea notificărilor ca citite a eșuat"; @@ -3307,6 +3218,12 @@ translators: Block name. %s: The localized block name */ /* An example tag used in the login prologue screens. */ "Football" = "Fotbal"; +/* translators: Recommendation included in a warning related to having blocks deeply nested. */ +"For this reason, we recommend editing the block using the web editor." = "Din acest motiv, recomandăm să editezi blocul folosind editorul web."; + +/* translators: Recommendation included in a warning related to having blocks deeply nested. */ +"For this reason, we recommend editing the block using your web browser." = "Din acest motiv, recomandăm să editezi blocul folosind navigatorul web."; + /* Register Domain - Domain contact information section header description */ "For your convenience, we have pre-filled your WordPress.com contact information. Please review to be sure it’s the correct information you want to use for this domain." = "Pentru comoditate, ți-am completat informațiile de contact din WordPress.com. Te rog să le verifici pentru a te asigura că sunt informațiile corecte pe care vrei să le folosești pentru acest domeniu."; @@ -3624,8 +3541,7 @@ translators: Block name. %s: The localized block name */ "Home" = "Prima pagină"; /* Title for setting which shows the current page assigned as a site's homepage - Title for the homepage section in site settings screen - Title of the Homepage Badge */ + Title for the homepage section in site settings screen */ "Homepage" = "Prima pagină"; /* Label for Homepage Settings site settings section @@ -3722,9 +3638,6 @@ translators: Block name. %s: The localized block name */ /* Hint for image title on image settings. */ "Image title" = "Titlu imagine"; -/* Accessibility label for image thumbnails in the media collection view. The parameter is the creation date of the image. */ -"Image, %@" = "Imagine, %@"; - /* Undated post time label */ "Immediately" = "Imediat"; @@ -3968,7 +3881,7 @@ translators: Block name. %s: The localized block name */ "Jetpack Scan will replace the affected file or directory." = "Scanarea Jetpack va înlocui fișierul sau directorul afectat."; /* Description that explains how we will fix the threat */ -"Jetpack Scan will resolve the threat." = "Scanarea Jetpack va înlătura amenințarea."; +"Jetpack Scan will resolve the threat." = "Scanare Jetpack va înlătura amenințarea."; /* Description that explains how we will fix the threat */ "Jetpack Scan will rollback the affected file to an older (clean) version." = "Scanarea Jetpack va restaura fișierul afectat la o versiune mai veche (curată)."; @@ -4210,9 +4123,6 @@ translators: Block name. %s: The localized block name */ Settings: Comments Approval settings */ "Links in comments" = "Legături în comentarii"; -/* The accessibility label for the list style button in the Post List. */ -"List style" = "Stil listă"; - /* Title of the screen that load selected the revisions. */ "Load" = "Încărcare"; @@ -4228,18 +4138,12 @@ translators: Block name. %s: The localized block name */ /* Text displayed while loading the activity feed for a site */ "Loading Backups..." = "Încarc copiile de siguranță..."; -/* Phrase to show when the user has searched for GIFs and they are being loaded. */ -"Loading GIFs..." = "Încarc GIF-uri..."; - /* Menus label text displayed while menus are loading */ "Loading Menus..." = "Încărcare meniuri..."; /* Text displayed while loading site People. */ "Loading People..." = "Încarc People..."; -/* Phrase to show when the user has searched for images and they are being loaded. */ -"Loading Photos..." = "Încarc fotografii..."; - /* Text displayed while loading plans details */ "Loading Plan..." = "Se încarcă planul..."; @@ -4300,8 +4204,7 @@ translators: Block name. %s: The localized block name */ /* Local Services site intent topic */ "Local Services" = "Servicii locale"; -/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. - Title of the Local Changes Badge */ +/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. */ "Local changes" = "Schimbări locale"; /* Title for alert when a generic error happened when trying to find the location of the device */ @@ -4465,7 +4368,6 @@ translators: Block name. %s: The localized block name */ "Max Video Upload Size" = "Dimensiune maximă de încărcare video"; /* Accessibility label for the Me button in My Site. - Label for the post author filter. This filter shows posts only authored by the current user. Me page title Noun. Title. Links to the Me screen. The accessibility value of the me tab. @@ -4473,9 +4375,7 @@ translators: Block name. %s: The localized block name */ "Me" = "Eu"; /* Noun. Title. Links to the blog's Media library. - Tab bar title for the Media tab in Media Picker The menu item to select during a guided tour. - Title for Media Library section of the app. Title for the media section in site settings screen Title label for the media settings section in the app settings */ "Media" = "Media"; @@ -4487,13 +4387,6 @@ translators: Block name. %s: The localized block name */ /* Label for size of media cache in the app. */ "Media Cache Size" = "Dimensiune cache media"; -/* Title for alert when access to media capture is not granted */ -"Media Capture" = "Captură media"; - -/* Title for alert when a generic error happened when loading media - Title for alert when access to the media library is not granted by the user */ -"Media Library" = "Bibliotecă Media"; - /* Title for action sheet with media options. */ "Media Options" = "Opțiuni media"; @@ -4516,9 +4409,6 @@ translators: Block name. %s: The localized block name */ /* translators: %s: block title e.g: \"Paragraph\". */ "Media options" = "Opțiuni media"; -/* Alert title when there is issues loading an asset to preview. */ -"Media preview failed." = "Previzualizarea media a eșuat."; - /* Alert displayed to the user when multiple media items have uploaded successfully. */ "Media uploaded (%ld files)" = "Media încărcată (%ld fișier)"; @@ -4556,9 +4446,6 @@ translators: Block name. %s: The localized block name */ Label for the share message field on the post settings. */ "Message" = "Mesaj"; -/* Title of section containing image / video metadata such as size and file type */ -"Metadata" = "Metadate"; - /* Option to select the Microsft Outlook app when logging in with magic links */ "Microsoft Outlook" = "Microsoft Outlook"; @@ -4578,13 +4465,11 @@ translators: Block name. %s: The localized block name */ "Months and Years" = "Luni și ani"; /* Accessibility label for more button in dashboard quick start card. - Accessibility label for the More button in Page List. Accessibility label for the More button in Post List (compact view). Accessibility label for the More button on formatting toolbar. Accessibility label for the More button on Reader Cell Accessibility label for the More button on Reader's post details - Action button to display more available options - Label for the more post button. Tapping displays an action sheet with post options. */ + Action button to display more available options */ "More" = "Mai mult"; /* Action button to display more available options @@ -4642,15 +4527,8 @@ translators: Block name. %s: The localized block name */ /* Screen reader text for button that will move the menu item */ "Move menu item" = "Mută elementul de meniu"; -/* Label for a button that moves a page to the draft folder - Label for an option that moves a post to the draft folder */ -"Move to Draft" = "Mută în ciorne"; - -/* Label for a button that moves a page to the trash folder - Label for a option that moves a post to the trash folder - Title for button on the comment details page that moves the comment to trash when tapped. +/* Title for button on the comment details page that moves the comment to trash when tapped. Trash option in the trash confirmation alert. - Trash option in the trash page confirmation alert. Trashes the comment */ "Move to Trash" = "Aruncă la gunoi"; @@ -4682,7 +4560,8 @@ translators: Block name. %s: The localized block name */ Title of My Site tab */ "My Site" = "Site-ul meu"; -/* Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ +/* Title for site picker screen. + Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ "My Sites" = "Site-urile mele"; /* Siri Suggestion to open My Sites */ @@ -4932,9 +4811,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title for the view when there aren't any Activities to display in the Activity Log for a given filter. */ "No matching events found." = "Nu am găsit niciun eveniment care să se potrivească."; -/* Message displayed when no results are returned from a media library search. Should match Calypso. - Phrase to show when the user search for images but there are no result to show. - Phrase to show when the user searches for GIFs but there are no result to show. */ +/* Message displayed when no results are returned from a media library search. Should match Calypso. */ "No media matching your search" = "Nu s-a potrivit niciun element media cu căutarea ta"; /* Menus text shown when no menus were available for loading the Menus editor. */ @@ -4952,8 +4829,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Displayed in the Notifications Tab as a title, when there are no notifications */ "No notifications yet" = "Nicio notificare până acum"; -/* Displayed when the user is searching the pages list and there are no matching pages - Text displayed when there's no matching with the text search */ +/* Text displayed when there's no matching with the text search */ "No pages matching your search" = "Nu se potrivește nicio pagină cu căutarea ta"; /* Text displayed when search for plugins returns no results */ @@ -4974,9 +4850,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Message shown whent the reader finds no posts for the chosen tag */ "No posts have been made recently with this tag." = "Niciun articol publicat recent cu această etichetă."; -/* Displayed when the user is searching the posts list and there are no matching posts */ -"No posts matching your search" = "Nu se potrivește niciun articol cu căutarea ta"; - /* Accessibility value for a Stats' Posting Activity Month if there are no posts. */ "No posts." = "Niciun articol."; @@ -5077,9 +4950,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* A message title */ "Nothing liked yet" = "Nu ai apreciat nimic până acum"; -/* Default message for empty media picker */ -"Nothing to show" = "Nimic de arătat"; - /* Notifications Details Accessibility Identifier */ "Notification Details Table" = "Tabelă detalii notificări"; @@ -5139,7 +5009,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Alert dismissal title Button title. Acknowledges a prompt. Button title. An acknowledgement of the message displayed in a prompt. - Confirmation of action Default action Dismisses the alert Marks all notifications as read. @@ -5201,9 +5070,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* No comment provided by engineer. */ "Only show excerpt" = "Arată numai rezumatul"; -/* Message telling the user that they've only enabled limited photo library permissions for the app. */ -"Only the selected photos you've given access to are available." = "Sunt disponibile numai fotografiile selectate pentru care ai dat acces."; - /* An informal exclaimation that means `something went wrong`. Title for the view when there's an error loading a comment. Title for the view when there's an error loading Activity Log @@ -5238,9 +5104,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title of a button that opens the apps settings in the system Settings.app */ "Open Settings" = "Deschide setările"; -/* Editor button to swich the media picker from quick mode to full picker */ -"Open full media picker" = "Deschide selectorul media complet"; - /* No comment provided by engineer. */ "Open in Safari" = "Deschide în Safari"; @@ -5280,6 +5143,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Divider on initial auth view separating auth options. */ "Or" = "Sau"; +/* Instruction text for other forms of two-factor auth methods. */ +"Or choose another form of authentication." = "Sau alege o alt tip de autentificare."; + /* Label for button to log in using site address. Underscores _..._ denote underline. */ "Or log in by _entering your site address_." = "Sau autentifică-te prin _introducerea adresei site-ului tău_."; @@ -5338,15 +5204,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title shown when selecting a post type of Page from the Share Extension. */ "Page" = "Pagină"; -/* Prompts the user that a restored page was moved to the drafts list. */ -"Page Restored to Drafts" = "Pagină restaurată la ciorne"; - -/* Prompts the user that a restored page was moved to the published list. */ -"Page Restored to Published" = "Pagină restaurată la publicate"; - -/* Prompts the user that a restored page was moved to the scheduled list. */ -"Page Restored to Scheduled" = "Pagină restaurată la programate"; - /* Name of the button to open the page settings The title of the Page Settings screen. */ "Page Settings" = "Setări pagină"; @@ -5363,9 +5220,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of notification displayed when a page has failed to upload. */ "Page failed to upload" = "Pagina a eșuat la încărcare"; -/* A short message explaining that a page was moved to the trash bin. */ -"Page moved to trash." = "Pagină mutată la gunoi."; - /* Title of notification displayed when a page has been successfully saved as a draft. */ "Page pending review" = "Pagină în așteptarea revizuirii"; @@ -5437,8 +5291,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title of pending Comments filter. */ "Pending" = "În așteptare"; -/* Name for the status of a post pending review. - Title of the Pending Review Badge */ +/* Name for the status of a post pending review. */ "Pending review" = "În curs de revizuire"; /* Noun. Title of the people management feature. @@ -5467,12 +5320,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Photography site intent topic */ "Photography" = "Fotografie"; -/* Tab bar title for the Photos tab in Media Picker */ -"Photos" = "Fotografii"; - -/* Subtitle for placeholder in Free Photos. The company name 'Pexels' should always be written as it is. */ -"Photos provided by Pexels" = "Fotografii oferite de Pexels"; - /* Title for selecting a new username in the site creation flow. */ "Pick username" = "Alege numele de utilizator"; @@ -5565,7 +5412,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Please enter the password for your WordPress.com account to log in with your Apple ID." = "Te rog introdu parola pentru contul tău WordPress.com pentru a te autentifica cu ID-ul Apple."; /* Instruction text on the two-factor screen. */ -"Please enter the verification code from your authenticator app, or tap the link below to receive a code via SMS." = "Te rog introdu codul de verificare din aplicația Authenticator sau atinge legătura de mai jos pentru a primi un cod prin SMS."; +"Please enter the verification code from your authenticator app." = "Te rog să introduci codul de verificare din aplicația Authenticator."; /* Popup message to ask for user credentials (fields shown below). */ "Please enter your credentials" = "Te rog să-ți introduci datele de conectare"; @@ -5660,15 +5507,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ The post formats available for the post. Should be the same as in core WP. */ "Post Format" = "Format de articol"; -/* Prompts the user that a restored post was moved to the drafts list. */ -"Post Restored to Drafts" = "Articol restaurat la schiță"; - -/* Prompts the user that a restored post was moved to the published list. */ -"Post Restored to Published" = "Articol restaurat la publicat"; - -/* Prompts the user that a restored post was moved to the scheduled list. */ -"Post Restored to Scheduled" = "Articol restaurat la programat"; - /* Name of the button to open the post settings The title of the Post Settings screen. */ "Post Settings" = "Setări articole"; @@ -5688,9 +5526,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of notification displayed when a post has failed to upload. */ "Post failed to upload" = "Articolul a eșuat la încărcare"; -/* A short message explaining that a post was moved to the trash bin. */ -"Post moved to trash." = "Articol mutat la gunoi."; - /* Title of notification displayed when a post has been successfully saved as a draft. */ "Post pending review" = "Articol în așteptarea revizuirii"; @@ -5749,9 +5584,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Period Stats 'Posts and Pages' header */ "Posts and Pages" = "Articole și pagini"; -/* Title of the Posts Page Badge */ -"Posts page" = "Pagină articole"; - /* Message informing the user that their static homepage for posts was set successfully */ "Posts page successfully updated" = "Pagina articole a fost actualizată cu succes"; @@ -5764,9 +5596,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* A message explaining the Posts I Like feature in the reader */ "Posts that you like will appear here." = "Articolele pe care le-ai apreciat vor apărea aici."; -/* Subtitle for placeholder in Tenor picker. `The company name 'Tenor' should always be written as it is. */ -"Powered by Tenor" = "Propulsat de Tenor"; - /* Browse premium themes selection title */ "Premium" = "Premium"; @@ -5785,18 +5614,12 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title for screen to preview a static content. */ "Preview" = "Previzualizează"; -/* Action for Media Picker to preview the selected media items. The argument in the string represents the number of elements (as numeric digits) selected */ -"Preview %@" = "Previzualizează %@"; - /* Title for web preview device switching button */ "Preview Device" = "Dispozitiv de previzualizare"; /* Title on display preview error */ "Preview Unavailable" = "Previzualizarea nu este disponibilă"; -/* Accessibility label for media item preview for user's viewing an item in their media library */ -"Preview media" = "Previzualizează elementul media"; - /* No comment provided by engineer. */ "Preview page" = "Previzualizează pagina"; @@ -5843,8 +5666,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Privacy notice for California users" = "Notificări de confidențialitate pentru utilizatorii din California"; /* Name for the status of a post that is marked private. - Privacy setting for posts set to 'Private'. Should be the same as in core WP. - Title of the Private Badge */ + Privacy setting for posts set to 'Private'. Should be the same as in core WP. */ "Private" = "Privat"; /* No comment provided by engineer. */ @@ -5894,12 +5716,10 @@ translators: %s: Select control button label e.g. \"Button width\" */ Label for the publish date button. */ "Publish Date" = "Data publicării"; -/* A short phrase indicating a post is due to be immedately published. - Label for a button that moves a page to the published folder, publishing with the current date/time. */ +/* A short phrase indicating a post is due to be immedately published. */ "Publish Immediately" = "Publică imediat"; /* Label for a button that publishes the post - Label for an option that moves a publishes a post immediately Title of button allowing the user to immediately publish the post they are editing. */ "Publish Now" = "Publică acum"; @@ -5917,8 +5737,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Name for the status of a published post. Period Stats 'Published' header - Title of the published filter. This filter shows a list of posts that the user has published. - Title of the published header in search list. */ + Title of the published filter. This filter shows a list of posts that the user has published. */ "Published" = "Publicat"; /* Precedes the name of the blog just posted on */ @@ -6060,8 +5879,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of the completion screen of the Blogging Reminders Settings screen when the reminders are removed. */ "Reminders removed" = "Reamintirile au fost înlăturate"; -/* Add asset to media picker list - Alert button to confirm a plugin to be removed +/* Alert button to confirm a plugin to be removed Button label when removing a blog Label for the Remove Feature Image icon. Tapping will show a confirmation screen for removing the feature image from the post. Remove Action @@ -6214,9 +6032,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of secondary button on alert prompting verify their accounts while attempting to publish */ "Resend" = "Retrimite"; -/* Title of the reset button */ -"Reset" = "Resetează"; - /* Accessibility label for the reset activity type button */ "Reset Activity Type filter" = "Resetează filtrul pentru tip de activitate"; @@ -6271,12 +6086,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ Button title, displayed when media has failed to upload. Allows the user to try the upload again. Button title. Retries uploading a post. If a user taps the button with this label, the action that evinced this error view will be retried. - Label for a button that attempts to re-upload a page that previously failed to upload. - Label for the retry post upload button. Tapping attempts to upload the post again. Opens the media library . Retries the upload of a user's post. Retry updating User's Role - Retry uploading the post. Retry. Action Retry. Verb – retry a failed media upload. The Jetpack view button title used when an error occurred @@ -6288,9 +6100,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Button title that triggers a scan */ "Retry Scan" = "Încearcă scanarea din nou"; -/* User action to retry media upload. */ -"Retry Upload" = "Reîncearcă încărcarea"; - /* User action to retry all failed media uploads. */ "Retry all" = "Reîncearcă-le pe toate"; @@ -6388,9 +6197,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Accessibility label for the 'Save Post' button when a post has been saved. */ "Saved Post" = "Articol salvat"; -/* Text displayed in HUD when a media item's metadata (title, etc) is saved successfully. */ -"Saved!" = "Salvat!"; - /* Accessibility hint for the 'Save Post' button. */ "Saves this post for later." = "Salvează acest articol pentru mai târziu."; @@ -6401,7 +6207,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Saving post…" = "Salvez articolul..."; /* Menus save button title while it is saving a Menu. - Text displayed in HUD while a media item's metadata (title, etc) is being saved. Text displayed in HUD while a post is being saved as a draft. */ "Saving..." = "Salvez..."; @@ -6492,21 +6297,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* No comment provided by engineer. */ "Search or type URL" = "Caută sau tastează URL-ul"; -/* Text displayed when the search controller will be presented */ -"Search pages" = "Caută pagini"; - -/* Text displayed when the search controller will be presented */ -"Search posts" = "Caută articole"; - /* No comment provided by engineer. */ "Search settings" = "Setări căutare"; -/* Title for placeholder in Tenor picker */ -"Search to find GIFs to add to your Media Library!" = "Caută pentru a găsi imagini GIF pe care să le adaugi în biblioteca ta Media!"; - -/* Title for placeholder in Free Photos */ -"Search to find free photos to add to your Media Library!" = "Caută pentru a găsi fotografii gratuite pe care să le adaugi în Biblioteca ta media!"; - /* Menus search bar placeholder text. */ "Search..." = "Caută..."; @@ -6577,9 +6370,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Register Domain - Domain contact information field placeholder for Country */ "Select Country" = "Selectează țara"; -/* Title of button that allows the user to select more photos to access within the app */ -"Select More" = "Selectează mai multe"; - /* Blog Picker's Title */ "Select Site" = "Selectează site-ul"; @@ -6601,9 +6391,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Register domain - Title for the Choose domain button of Suggested domains screen */ "Select domain" = "Selectează domeniul"; -/* Accessibility hint for actions when displaying media items. */ -"Select media." = "Selectează media."; - /* Accessibility label for selecting paragraph style button on formatting toolbar. */ "Select paragraph style" = "Selectează stil paragraf"; @@ -6707,19 +6494,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label for connected service in Publicize stat. */ "Service" = "Serviciu"; -/* Label for a button that opens the Set Parent options view controller - Navigation title displayed on the navigation bar */ +/* Navigation title displayed on the navigation bar */ "Set Parent" = "Setează părinte"; /* No comment provided by engineer. */ "Set as Featured Image" = "Setează ca imagine reprezentativă"; -/* Label for a button that sets the selected page as the site's Homepage */ -"Set as Homepage" = "Setează ca prima pagină"; - -/* Label for a button that sets the selected page as the site's Posts page */ -"Set as Posts Page" = "Setează ca pagină articole"; - /* Notice confirming that an image has been set as the post's featured image. */ "Set as featured image" = "Setează ca imagine reprezentativă"; @@ -6763,7 +6543,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Accessibility label for share buttons in nav bars Button label to share a post Button label to share a web page - Share the post. Shares the comment URL Spoken accessibility label Title for a button that allows the user to share their answer to the prompt. @@ -7149,8 +6928,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Name of setting configured when a site uses a static page as its homepage */ "Static Homepage" = "Prima pagină statică"; -/* Label for post stats option. Tapping displays statistics for a post. - Noun. Abbreviation of Statistics. Name of the Stats feature +/* Noun. Abbreviation of Statistics. Name of the Stats feature Noun. Abbv. of Statistics. Links to a blog's Stats screen. Stats 3D Touch Shortcut Stats window title @@ -7181,9 +6959,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label text that defines a post marked as sticky */ "Sticky" = "Reprezentativ"; -/* Accessibility label for a sticky post in the post list. */ -"Sticky." = "Reprezentativ."; - /* User action to stop upload. */ "Stop upload" = "Oprește încărcarea"; @@ -7240,7 +7015,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ Theme Support action title */ "Support" = "Suport"; -/* Button used to switch site */ +/* Title for back button that leads to the site picker screen. */ "Switch Site" = "Comută site-ul"; /* Switches the Editor to HTML Mode */ @@ -7328,9 +7103,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label explaining why users might want to add tags. */ "Tags help tell readers what a post is about. Separate different tags with commas." = "Etichetele le spun cititorilor despre ce este vorba în articol. Separă etichetele cu virgule."; -/* Menu option for taking an image or video with the device's camera. */ -"Take Photo or Video" = "Fă o fotografie sau un video"; - /* No comment provided by engineer. */ "Take a Photo" = "Fă o poză"; @@ -7401,12 +7173,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Accessibility hint */ "Tap to select the previous period" = "Atinge pentru a selecta perioada anterioară"; -/* Accessibility hint for button used to switch site */ -"Tap to switch to another site, or add a new site" = "Atinge pentru a comuta la un alt site sau pentru a adăuga unul nou"; - -/* Accessibility hint for media item preview for user's viewing an item in their media library */ -"Tap to view media in full screen" = "Atinge pentru a vizualiza elementul media pe ecran complet"; - /* Accessibility hint for a button that opens a new view with more details. */ "Tap to view more details." = "Atinge pentru a vedea mai multe detalii."; @@ -7452,10 +7218,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* No comment provided by engineer. */ "Text formatting controls are located within the toolbar positioned above the keyboard while editing a text block" = "Când editezi un bloc Text, comenzile pentru formatarea textului se află în bara de unelte poziționată deasupra tastaturii."; -/* Button title - The button's title text to send a 2FA code via SMS text message. */ +/* Button title */ "Text me a code instead" = "În schimb, trimite-mi un mesaj text cu un cod"; +/* The button's title text to send a 2FA code via SMS text message. */ +"Text me a code via SMS" = "Trimite-mi un cod prin SMS"; + /* Message of alert when theme activation succeeds */ "Thanks for choosing %@ by %@" = "Mulțumim pentru că ai ales %1$@ de %2$@"; @@ -7483,9 +7251,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Error message shown to a user who is trying to share to Facebook but does not have any available Facebook Pages. */ "The Facebook connection cannot find any Pages. Publicize cannot connect to Facebook Profiles, only published Pages." = "Conexiunea Facebook nu găsește nicio pagină. Publicitatea nu se poate conecta la profilurile Facebook, ci numai la pagini Facebook publicate."; -/* Message shown when a GIF failed to load while trying to add it to the Media library. */ -"The GIF could not be added to the Media Library." = "GIF-ul nu a putut fi adăugat în Biblioteca media."; - /* Description shown when a user logs in with Google but no matching WordPress.com account is found */ "The Google account \"%@\" doesn't match any account on WordPress.com" = "Contul Google „%@” nu se potrivește cu niciun cont de pe WordPress.com"; @@ -7613,7 +7378,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Error message shown when user attempts to remove the site owner. */ "The user you are trying to remove is the owner of this site. Please contact support for assistance." = "Utilizatorul pe care încerci să-l înlături este proprietarul acestui site. Te rog contactează suportul pentru asistență."; -/* Error message informing a user about an invalid password. */ +/* No comment provided by engineer. */ "The username or password stored in the app may be out of date. Please re-enter your password in the settings and try again." = "Numele de utilizator sau parola stocate în aplicație pot fi învechite. Te rog reintrodu parola ta în setări și încearcă din nou."; /* Message shown when a video failed to load while trying to add it to the Media library. */ @@ -7681,9 +7446,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* A short error message letting the user know about a problem displaying a post. */ "There was a problem displaying this post." = "A fost o problemă la afișarea acestui articol."; -/* Error message displayed when the Media Library is unable to load a full sized preview of an item. */ -"There was a problem loading the media item." = "A fost o problemă la încărcarea elementului media."; - /* The loading view subtitle displayed when an error occurred */ "There was a problem loading your data, refresh your page to try again." = "A fost o problemă la încărcarea datelor tale, reîmprospătează pagina pentru a încerca din nou."; @@ -7696,9 +7458,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Explaining to the user there was an error trying to obtain the current location of the user. */ "There was a problem when trying to access your location. Please try again later." = "A apărut o problemă când am încercat să accesăm locația ta. Te rog încearcă din nou."; -/* Explaining to the user there was an generic error accesing media. */ -"There was a problem when trying to access your media. Please try again later." = "A apărut o problemă când am încercat să accesăm media ta. Te rog încearcă din nou mai târziu."; - /* Message for stories unknown error. */ "There was a problem with the Stories editor. If the problem persists you can contact us via the Me > Help & Support screen." = "A fost o problemă la editorul de narațiuni. Dacă problema persistă poți să ne contactezi folosind ecranul Eu > Ajutor și suport."; @@ -7769,9 +7528,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* A description informing the user in order to proceed with this feature we will need camera permissions, and how to enable it. */ "This app needs permission to access the Camera to scan login codes, tap on the Open Settings button to enable it." = "Pentru a scana codurile de autentificare, această aplicație are nevoie de permisiunea de a accesa aparatul foto, atinge butonul Deschide setările pentru a o activa."; -/* Explaining to the user why the app needs access to the device media library. */ -"This app needs permission to access your device media library in order to add photos and\/or video to your posts. Please change the privacy settings if you wish to allow this." = "Această aplicație are nevoie de permisiunea de a accesa biblioteca media de pe dispozitiv pentru a adăuga fotografii și videouri în articolele tale. Te rog schimbă setările de confidențialitate dacă dorești să permiți asta."; - /* No comment provided by engineer. */ "This color combination may be hard for people to read. Try using a brighter background color and\/or a darker text color." = "Această combinație de culori poate să nu fie lizibilă pentru toți cititorii. Încearcă să folosești o culoare de fundal mai luminoasă și\/sau o culoare pentru text mai închisă."; @@ -7881,6 +7637,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Description of a Quick Start Tour */ "Time to finish setting up your site! Our checklist walks you through the next steps." = "Este timpul să termini inițializarea site-ului! Lista noastră de verificări te ghidează prin pașii următori."; +/* Error when the uses takes more than 1 minute to submit a security key. */ +"Time's up, but don't worry, your security is our priority. Please try again!" = "Timpul a expirat, dar nu îți face griji, securitatea ta este prioritatea noastră. Te rog să încerci din nou."; + /* WordPress.com Marketing Footer Text */ "Tips for getting the most out of WordPress.com." = "Sfaturi pentru a folosi la maxim WordPress.com."; @@ -8004,24 +7763,20 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title for the traffic section in site settings screen */ "Traffic" = "Trafic"; +/* Describes a domain that was transferred from elsewhere to wordpress.com */ +"Transferred Domain" = "Domeniu transferat"; + /* translators: %s: block title e.g: \"Paragraph\". */ "Transform %s to" = "Transformă %s în"; /* No comment provided by engineer. */ "Transform block…" = "Transformă blocul..."; -/* Accessibility label for trash button to delete items from the user's media library - Accessibility label for trash buttons in nav bars +/* Accessibility label for trash buttons in nav bars Trashes a comment Trashes the comment */ "Trash" = "Coș de gunoi"; -/* Accessibility hint for trash button to delete items from the user's media library */ -"Trash selected media" = "Aruncă la gunoi elementele media selectate"; - -/* Title of the trash page confirmation alert. */ -"Trash this page?" = "Arunci la gunoi această pagină?"; - /* Title of the trash confirmation alert. */ "Trash this post?" = "Arunci la gunoi acest articol?"; @@ -8139,9 +7894,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Shown when a user logs in with Google but it subsequently fails to work as login to WordPress.com */ "Unable To Connect" = "Nu mă pot conecta"; -/* An error message. */ -"Unable to Connect" = "Nu mă pot conecta"; - /* Title for stories unknown error. */ "Unable to Create Stories Editor" = "Nu pot să creez cu editorul de narațiuni"; @@ -8157,9 +7909,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* An error message shown when there is an issue creating new invite links. */ "Unable to create new invite links." = "Nu pot să creez legături noi la invitații."; -/* Text displayed in HUD if there was an error attempting to delete a group of media items. */ -"Unable to delete all media items." = "Nu pot să șterg toate elementele media."; - /* Text displayed in HUD if there was an error attempting to delete a media item. */ "Unable to delete media item." = "Nu pot șterge elementul media."; @@ -8223,12 +7972,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Message displayed when sharing a link to the downloadable backup fails. */ "Unable to share link" = "Nu pot să partajez legătura"; -/* Message that appears when a user tries to trash a page while their device is offline. */ -"Unable to trash pages while offline. Please try again later." = "Paginile nu pot fi aruncate la gunoi în timp ce ești offline. Te rog reîncearcă mai târziu."; - -/* Message that appears when a user tries to trash a post while their device is offline. */ -"Unable to trash posts while offline. Please try again later." = "Nu pot arunca articole la gunoi în timp ce ești offline. Te rog reîncearcă mai târziu."; - /* Notice title when turning site notifications off fails. */ "Unable to turn off site notifications" = "Nu pot să dezactivez notificările pe site"; @@ -8301,8 +8044,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ Button title. Reverts the previous notification operation Revert an operation Revert enabling notification after successfully subcribing to the comments for the post. - The title of an 'undo' button. Tapping the button moves a trashed page out of the trash folder. - The title of an 'undo' button. Tapping the button moves a trashed post out of the trash folder. Undo action */ "Undo" = "Anulează"; @@ -8345,9 +8086,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title for Unknown HTML Editor */ "Unknown HTML" = "HTML necunoscut"; -/* Label to use when creation date from media asset is not know. */ -"Unknown creation date" = "Dată creare necunoscută"; - /* No comment provided by engineer. */ "Unknown error" = "Eroare necunoscută"; @@ -8513,6 +8251,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of a row displayed on the debug screen used to configure the sandbox store use in the App. */ "Use Sandbox Store" = "Folosește Sandbox Store"; +/* The button's title text to use a security key. */ +"Use a security key" = "Folosește o cheie de securitate"; + /* Option to enable the block editor for new posts */ "Use block editor" = "Folosește editorul de blocuri"; @@ -8588,15 +8329,10 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of an alert informing users that the video they are trying to select is not allowed. */ "Video not uploaded" = "Videoul nu a fost încărcat"; -/* Accessibility label for video thumbnails in the media collection view. The parameter is the creation date of the video. */ -"Video, %@" = "Video, %@"; - /* Period Stats 'Videos' header */ "Videos" = "Videouri"; /* Button title. Displays a summary / sharing screen for a specific post. - Label for a button that opens the page when tapped. - Label for the view post button. Tapping displays the post as it appears on the web. Opens the post epilogue screen to allow sharing / viewing of a post. Theme View action title Verb. The screen title shown when viewing a post inside the app. */ @@ -8711,6 +8447,10 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Message shown on screen while waiting for Google to finish its signup process. */ "Waiting for Google to complete…" = "Aștept să finalizeze Google..."; +/* Text while the webauthn signature is being verified + Text while waiting for a security key challenge */ +"Waiting for security key" = "Aștept cheia de securitate"; + /* View title during the Google auth process. */ "Waiting..." = "Așteaptă..."; @@ -9085,6 +8825,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* An error message shown when a wpcom user provides the wrong password. */ "Whoops, something went wrong and we couldn't log you in. Please try again!" = "Hopa, ceva a mers prost și nu te-am putut autentifica. Te rog reîncearcă!"; +/* Generic error on the 2FA screen */ +"Whoops, something went wrong. Please try again!" = "Ceva nu a mers bine. Te rog să încerci din nou."; + +/* Error when the uses chooses an invalid security key on the 2FA screen. */ +"Whoops, that security key does not seem valid. Please try again with another one" = "Această cheie de securitate nu pare a fi validă. Te rog să încerci din nou cu alta"; + /* Error message shown when an incorrect two factor code is provided. */ "Whoops, that's not a valid two-factor verification code. Double-check your code and try again!" = "Hopa, acesta nu este un cod valid de verificare cu doi-factori. Verifică-ți din nou codul și reîncearcă!"; @@ -9112,9 +8858,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Siri Suggestion to open Support */ "WordPress Help" = "Ajutor WordPress"; -/* Name for the WordPress Media Library */ -"WordPress Media" = "Media WordPress"; - /* Accessibility label for selecting an image or video from the user's WordPress media library on formatting toolbar. */ "WordPress Media Library" = "Bibliotecă media WordPress"; @@ -9429,9 +9172,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Share extension error dialog text. */ "Your account does not have permission to upload media to this site. The Site Administrator can change these permissions." = "Contul tău nu are permisiunea de a încărca fișiere media pe acest site. Administratorul site-ului poate modifica aceste permisiuni."; -/* Explaining to the user why the app needs access to the device media library. */ -"Your app is not authorized to access media library due to active restrictions such as parental controls. Please check your parental control settings in this device." = "Aplicația ta nu este autorizată să acceseze biblioteca media din cauza restricțiilor active, cum ar fi controlul parental. Te rog verifică setările controlului parental pentru acest dispozitiv."; - /* Title for the Jetpack Backup Complete message. */ "Your backup is now available for download" = "Copia ta de siguranță este disponibilă acum pentru descărcare"; @@ -9450,9 +9190,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of the site address section in the Domains Dashboard. */ "Your free WordPress.com address is" = "Adresa ta gratuită pentru WordPress.com este"; -/* Error message when picked media cannot be imported into stories. */ -"Your media could not be exported. If the problem persists you can contact us via the Me > Help & Support screen." = "Elementul media nu a putut fi exportat. Dacă problema persistă poți să ne contactezi folosind ecranul Eu > Ajutor și suport."; - /* Details about recently acquired domain on domain credit redemption success screen */ "Your new domain %@ is being set up. It may take up to 30 minutes for your domain to start working." = "Noul tău domeniu %@ este în curs de inițializare. Poate dura până la 30 de minute înainte ca domeniul tău să fie funcțional."; @@ -9576,8 +9313,38 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* This is the string we display when prompting the user to review the WordPress app */ "appRatings.wordpress.prompt" = "Ce părere ai despre WordPress?"; -/* Label displayed on audio media items. */ -"audio" = "audio"; +/* Option to enable the optimization of images when uploading. */ +"appSettings.media.imageOptimizationRow" = "Optimizează imaginile"; + +/* Indicates an image will use high quality when uploaded. */ +"appSettings.media.imageQuality.high" = "Mare"; + +/* Indicates an image will use low quality when uploaded. */ +"appSettings.media.imageQuality.low" = "Mică"; + +/* Indicates an image will use maximum quality when uploaded. */ +"appSettings.media.imageQuality.maximum" = "Maximă"; + +/* Indicates an image will use medium quality when uploaded. */ +"appSettings.media.imageQuality.medium" = "Medie"; + +/* The quality of image used when uploading */ +"appSettings.media.imageQuality.title" = "Calitate"; + +/* Title for the image quality settings option. */ +"appSettings.media.imageQualityRow" = "Calitate imagini"; + +/* Message of an alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.message" = "Optimizarea imaginilor micșorează imaginile pentru o încărcare mai rapidă.\n\nAceastă opțiune este activată implicit, dar poți să o modifici oricând în setările aplicației."; + +/* Title of an alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.title" = "Continui optimizarea imaginilor?"; + +/* Title of button for turning off image optimization, displayed in the alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.turnOff" = "Nu, oprește"; + +/* Title of button for leaving on image optimization, displayed in the alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.turnOn" = "Da, lasă în continuare"; /* translators: displays audio file extension. e.g. MP3 audio file */ "audio file" = "fișier audio"; @@ -9691,7 +9458,44 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "blogHeader.actionCopyURL" = "Copiază URL-ul"; /* Context menu button title */ -"blogHeader.actionOpenInBrowser" = "Deschide în navigator"; +"blogHeader.actionVisitSite" = "Vizitează site-ul"; + +/* Title for a button that, when tapped, shows more info about participating in Bloganuary. */ +"bloganuary.dashboard.card.button.learnMore" = "Află mai multe"; + +/* Short description for the Bloganuary event, shown right below the title. */ +"bloganuary.dashboard.card.description" = "În luna ianuarie, îndemnurile de a scrie vor veni de la Bloganuary - provocarea comunității noastre de a crea un obicei de a scrie pe bloguri în noul an."; + +/* Title for the Bloganuary dashboard card while Bloganuary is running. */ +"bloganuary.dashboard.card.runningTitle" = "Bloganuary este aici!"; + +/* Title for the Bloganuary dashboard card. */ +"bloganuary.dashboard.card.title" = "Bloganuary vine în curând!"; + +/* Title of a button that calls the user to enable the Blogging Prompts feature. */ +"bloganuary.learnMore.modal.button.promptsDisabled" = "Activează îndemnurile de a scrie"; + +/* Title of a button that will dismiss the Bloganuary modal when tapped. +Note that the word 'go' here should have a closer meaning to 'start' rather than 'move forward'. */ +"bloganuary.learnMore.modal.button.promptsEnabled" = "Să începem!"; + +/* The second line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.description.second" = "Publică răspunsul tău."; + +/* The third line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.description.third" = "Citești răspunsurile altor bloggeri ca să găsești inspirație și să faci conexiuni noi."; + +/* The first line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.descriptions.first" = "Primești în fiecare zi un îndemn nou, ca să te inspire."; + +/* An additional piece of information shown in case the user has the Blogging Prompts feature disabled. */ +"bloganuary.learnMore.modal.footer.addition" = "Ca să te alături la Bloganuary, trebuie să activezi Îndemnuri de a scrie."; + +/* An informative excerpt shown in a subtler tone. */ +"bloganuary.learnMore.modal.footer.text" = "Bloganuary va folosi îndemnuri zilnice de a scrie ca să-ți trimită subiecte în luna ianuarie."; + +/* The headline text of the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.headline" = "Alătură-te provocării noastre de a scrie timp de o lună"; /* Verb. Dismisses the blogging prompt notification. */ "bloggingPrompt.pushNotification.customActionDescription.dismiss" = "Refuză"; @@ -9720,6 +9524,12 @@ Example: Comment on Example: Reply to Pamela Nguyen */ "comment.header.subText.reply" = "Răspunde-i lui %1$@"; +/* Error message informing a user about an invalid password. */ +"common.reEnterPasswordMessage" = "Numele de utilizator sau parola stocate în aplicație pot fi învechite. Te rog reintrodu parola ta în setări și încearcă din nou."; + +/* An error message. */ +"common.unableToConnect" = "Nu mă pot conecta"; + /* Footnote for the privacy compliance popover. */ "compliance.analytics.popover.footnote" = "Aceste cookie-uri ne permit să optimizăm performanța prin colectarea de informații despre modul în care utilizatorii interacționează cu site-urile noastre web."; @@ -9870,50 +9680,92 @@ Example: Reply to Pamela Nguyen */ /* Title for a menu action in the context menu on the Jetpack install card. */ "domain.dashboard.card.menu.hide" = "Ascunde asta"; -/* Domain Purchase Completion footer */ -"domain.purchase.preview.footer" = "Poate să dureze până la 30 de minute până când domeniul tău personalizat va începe să funcționeze."; +/* Search domain - Title for the Suggested domains screen */ +"domain.management.addDomain.search.title" = "Caută un domeniu"; -/* Domain Purchase Completion description (only for FREE domains). */ -"domain.purchase.preview.free.description" = "În continuare, te vom ajuta să îl pregătești pentru a fi răsfoit."; +/* Domain management buy domain card button title */ +"domain.management.buy.card.button.title" = "Ia domeniul"; -/* Domain Purchase Completion description (only for PAID domains). */ -"domain.purchase.preview.paid.description" = "Ți-am trimis chitanța prin email. În continuare, te vom ajuta să îl pregătești pentru toată lumea."; +/* Domain management buy domain card subtitle */ +"domain.management.buy.card.subtitle" = "Adaugă un site mai târziu."; -/* Reflects that site is live when domain purchase feature flag is ON. */ -"domain.purchase.preview.title" = "Felicitări, site-ul tău este live!"; +/* Domain management buy domain card title */ +"domain.management.buy.card.title" = "Doar cumpără un domeniu"; + +/* The expired label of the domain card in All Domains screen. */ +"domain.management.card.expired.label" = "Expirate"; + +/* The renews label of the domain card in All Domains screen. */ +"domain.management.card.renews.label" = "Reînnoiri"; + +/* The empty state button title in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.button.title" = "Găsește un domeniu"; + +/* The empty state description in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.description" = "Atinge mai jos ca să găsești domeniul perfect pentru tine."; + +/* The empty state title in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.title" = "Nu ai niciun domeniu"; + +/* The empty state description in All Domains screen when an error occurs */ +"domain.management.error.empty.state.description" = "Am întâmpinat o eroare la încărcarea domeniilor. Dacă problema persistă, te rog să contactezi suportul."; + +/* The empty state title in All Domains screen when an error occurs */ +"domain.management.error.empty.state.title" = "Ceva nu a mers bine"; + +/* The empty state button title in All Domains screen when an error occurs */ +"domain.management.error.state.button.title" = "Încearcă din nou"; + +/* The empty state description in All Domains screen when the user is offline */ +"domain.management.offline.empty.state.description" = "Te rog să verifici conexiunea la rețea și să încerci din nou."; + +/* The empty state title in All Domains screen when the user is offline */ +"domain.management.offline.empty.state.title" = "Nu există conexiune la internet"; -/* Status of a domain in `Action Required` state */ -"domain.status.action.required" = "Este necesară o acțiune"; +/* Domain management choose site card button title */ +"domain.management.purchase.footer" = "*Este inclus un domeniu gratuit pentru un an în toate planurile anuale plătite"; -/* Status of a domain in `Active` state */ -"domain.status.active" = "Activ"; +/* Domain management purchase domain screen title */ +"domain.management.purchase.subtitle" = "Nu-ți face griji, poți să adaugi cu ușurință un site mai târziu."; -/* Status of a domain in `Complete Setup` state */ -"domain.status.complete.setup" = "Finalizează inițializarea"; +/* Domain management purchase domain screen title. */ +"domain.management.purchase.title" = "Alege cum vrei să-ți folosești domeniul"; -/* Status of a domain in `Error` state */ -"domain.status.error" = "Eroare"; +/* The search bar title in All Domains screen. */ +"domain.management.search-bar.title" = "Caută domenii"; -/* Status of a domain in `Expired` state */ -"domain.status.expired" = "Expirat"; +/* The empty state description in All Domains screen when the are no domains matching the search criteria */ +"domain.management.search.empty.state.description" = "Nu am găsit niciun domeniu care să se potrivească cu căutarea ta „%@”"; -/* Status of a domain in `Expiring Soon` state */ -"domain.status.expiring.soon" = "Expiră în curând"; +/* The empty state title in All Domains screen when the are no domains matching the search criteria */ +"domain.management.search.empty.state.title" = "Nu am găsit niciun domeniu care să se potrivească"; -/* Status of a domain in `Failed` state */ -"domain.status.failed" = "Eșuat"; +/* Domain management choose site card button title */ +"domain.management.site.card.button.title" = "Alege site-ul"; -/* Status of a domain in `In Progress` state */ -"domain.status.in.progress" = "În curs de desfășurare"; +/* Domain management choose site card subtitle */ +"domain.management.site.card.footer" = "Domeniu gratuit în primul an*"; -/* Status of a domain in `Renew` state */ -"domain.status.renew" = "Reînnoiește"; +/* Domain management choose site card subtitle */ +"domain.management.site.card.subtitle" = "Folosește-l cu un site pe care îl ai deja."; -/* Status of a domain in `Verify Email` state */ -"domain.status.verify.email" = "Confirmă adresa de email"; +/* Domain management choose site card title */ +"domain.management.site.card.title" = "Site WordPress.com existent"; -/* Status of a domain in `Verifying` state */ -"domain.status.verifying" = "Verific"; +/* Domain Management Screen Title */ +"domain.management.title" = "Toate domeniile"; + +/* Domain Purchase Completion footer */ +"domain.purchase.preview.footer" = "Poate să dureze până la 30 de minute până când domeniul tău personalizat va începe să funcționeze."; + +/* Domain Purchase Completion description (only for FREE domains). */ +"domain.purchase.preview.free.description" = "În continuare, te vom ajuta să îl pregătești pentru a fi răsfoit."; + +/* Domain Purchase Completion description (only for PAID domains). */ +"domain.purchase.preview.paid.description" = "Ți-am trimis chitanța prin email. În continuare, te vom ajuta să îl pregătești pentru toată lumea."; + +/* Reflects that site is live when domain purchase feature flag is ON. */ +"domain.purchase.preview.title" = "Felicitări, site-ul tău este live!"; /* The 'Best Alternative' label under the domain name in 'Choose a domain' screen */ "domain.suggestions.row.best-alternative" = "Cea mai bună alternativă"; @@ -9936,12 +9788,24 @@ Example: Reply to Pamela Nguyen */ /* The text to display for paid domains in 'Site Creation > Choose a domain' screen */ "domain.suggestions.row.yearly" = "pe an"; +/* Title for the checkout screen. */ +"domains.checkout.title" = "Finalizare"; + /* Action shown in a bottom notice to dismiss it. */ "domains.failure.dismiss" = "Respinge"; /* Content show when the domain selection action fails. */ "domains.failure.title" = "Regret, domeniul pe care încerci să-l adaugi nu poate fi cumpărat acum din aplicația Jetpack."; +/* Title for the screen where the user can choose how to use the domain they're end up purchasing. */ +"domains.purchase.choice.title" = "Cumpără domeniul"; + +/* Back button title that navigates back to the search domains screen. */ +"domains.search.backButton.title" = "Caută"; + +/* Title of screen where user chooses a site to connect to their selected domain */ +"domains.sitePicker.title" = "Alege site-ul"; + /* No comment provided by engineer. */ "double-tap to change unit" = "atinge de două ori pentru a schimba unitatea"; @@ -9959,6 +9823,15 @@ Example: Reply to Pamela Nguyen */ Site Address placeholder */ "example.com" = "exemplu.com"; +/* Title for confirmation navigation bar button item */ +"externalMediaPicker.add" = "Adaugă"; + +/* Bottom toolbar title in the selection mode */ +"externalMediaPicker.toolbarSelectItemsPrompt" = "Selectează imagini"; + +/* Bottom toolbar title in the selection mode */ +"externalMediaPicker.toolbarViewSelected" = "Vezi cele selectate (%@)"; + /* Title of screen the displays the details of an advertisement campaign. */ "feature.blaze.campaignDetails.title" = "Detalii campanie"; @@ -10058,9 +9931,6 @@ Example: Reply to Pamela Nguyen */ /* (placeholder) Help the user enter a URL into the field */ "http:\/\/my-site-address (URL)" = "http:\/\/my-site-address (URL)"; -/* Label displayed on image media items. */ -"image" = "imagine"; - /* Sentence to justify why the app is asking permission from the user to use their camera. */ "infoplist.NSCameraUsageDescription" = "Să faci poze sau videouri pe care să le folosești în articolele tale."; @@ -10361,6 +10231,9 @@ Example: Reply to Pamela Nguyen */ /* Indicating that referrer was marked as spam */ "marked as spam" = "marcat ca spam"; +/* Products header text in Me Screen. */ +"me.products.header" = "Produse"; + /* Title of error prompt shown when a sync fails. */ "media.syncFailed" = "Nu pot să sincronizez elementele media"; @@ -10373,18 +10246,24 @@ Example: Reply to Pamela Nguyen */ /* Message of an alert informing users that the video they are trying to select is not allowed. */ "mediaExporter.videoLimitExceededError" = "Pentru a încărca videouri mai lungi de 5 minute, trebuie să ai un plan plătit."; -/* Verb. User action to dismiss error alert when failing to load media item. */ -"mediaItemTable.errorAlert.dismissButton" = "Refuză"; - /* Accessibility hint for add button to add items to the user's media library */ "mediaLibrary.addButtonAccessibilityHint" = "Adaugă element media nou"; /* Accessibility label for add button to add items to the user's media library */ "mediaLibrary.addButtonAccessibilityLabel" = "Adaugă"; +/* Button name in the more menu */ +"mediaLibrary.aspectRatioGrid" = "Grilă cu rapoarte aspect"; + +/* Context menu button */ +"mediaLibrary.buttonDelete" = "Șterge"; + /* Media screen navigation bar button Select title */ "mediaLibrary.buttonSelect" = "Selectează"; +/* Context menu button */ +"mediaLibrary.buttonShare" = "Partajează"; + /* Verb. Button title. Tapping cancels an action. */ "mediaLibrary.deleteConfirmationCancel" = "Anulează"; @@ -10406,6 +10285,21 @@ Example: Reply to Pamela Nguyen */ /* Text displayed in HUD after successfully deleting a media item */ "mediaLibrary.deletionSuccessMessage" = "Ștearsă!"; +/* The name of the media filter */ +"mediaLibrary.filterAll" = "Tot"; + +/* The name of the media filter */ +"mediaLibrary.filterAudio" = "Audio"; + +/* The name of the media filter */ +"mediaLibrary.filterDocuments" = "Documente"; + +/* The name of the media filter */ +"mediaLibrary.filterImages" = "Imagini"; + +/* The name of the media filter */ +"mediaLibrary.filterVideos" = "Videouri"; + /* User action to delete un-uploaded media. */ "mediaLibrary.retryOptionsAlert.delete" = "Șterge"; @@ -10418,6 +10312,12 @@ Example: Reply to Pamela Nguyen */ /* Message displayed when no results are returned from a media library search. Should match Calypso. */ "mediaLibrary.searchResultsEmptyTitle" = "Nu s-a potrivit niciun element media cu căutarea ta"; +/* Text displayed in HUD if there was an error attempting to share a group of media items. */ +"mediaLibrary.sharingFailureMessage" = "Nu pot să partajez elementele selectate."; + +/* Button name in the more menu */ +"mediaLibrary.squareGrid" = "Grilă pătrată"; + /* Media screen navigation title */ "mediaLibrary.title" = "Media"; @@ -10439,6 +10339,12 @@ Example: Reply to Pamela Nguyen */ /* The title of the button to dismiss the alert shown when the picked media cannot be imported into stories. */ "mediaPicker.failedMediaExportAlert.dismissButton" = "Refuză"; +/* Error message when picked media cannot be imported into stories. */ +"mediaPicker.failedMediaExportAlert.message" = "Elementul media nu a putut fi exportat. Dacă problema persistă poți să ne contactezi folosind ecranul Eu > Ajutor și suport."; + +/* Error title when picked media cannot be imported into stories. */ +"mediaPicker.failedMediaExportAlert.title" = "Exportul media a eșuat"; + /* Message for alert when access to camera is not granted */ "mediaPicker.noCameraAccessMessage" = "Această aplicație are nevoie de permisiunea de a accesa Camera pentru a captura elemente media noi, te rog schimbă setările de confidențialitate dacă dorești să permiți asta."; @@ -10472,6 +10378,12 @@ Example: Reply to Pamela Nguyen */ /* The name of the action in the context menu */ "mediaPicker.takeVideo" = "Fă un video"; +/* Navigation title for media preview. Example: 1 of 3 */ +"mediaPreview.NofM" = "%1$@ din %2$@"; + +/* Max image size in pixels (e.g. 300x300px) */ +"mediaSizeSlider.valueFormat" = "%1$d × %2$d px"; + /* The description in the Delete WordPress screen */ "migration.deleteWordpress.description" = "Se pare că încă ai instalată aplicația WordPress."; @@ -10484,9 +10396,6 @@ Example: Reply to Pamela Nguyen */ /* The title in the Delete WordPress screen */ "migration.deleteWordpress.title" = "Nu mai ai nevoie de aplicația WordPress pe dispozitivul tău"; -/* Primary button title in the migration done screen. */ -"migration.done.actions.primary.title" = "Finalizează"; - /* Footer for the migration done screen. */ "migration.done.footer" = "Recomandăm să dezinstalezi aplicația WordPress pe dispozitivul tău ca să eviți conflictele."; @@ -10496,6 +10405,9 @@ Example: Reply to Pamela Nguyen */ /* Primary description in the migration done screen. */ "migration.done.primaryDescription" = "Am transferat toate datele și setările tale. Toate sunt acolo, exact unde erau."; +/* Secondary description (second paragraph) in the migration done screen. */ +"migration.done.secondaryDescription" = "Este timpul să-ți continui călătoria WordPress în aplicația Jetpack!"; + /* Title of the migration done screen. */ "migration.done.title" = "Îți mulțumim că ai comutat la Jetpack!"; @@ -10544,6 +10456,9 @@ Example: Reply to Pamela Nguyen */ /* The title in the migration welcome screen */ "migration.welcome.title" = "Bine ai venit la Jetpack!"; +/* Primary button title in the migration done screen. */ +"migrationDone.actions.primaryTitle" = "Să-i dăm drumul"; + /* Description for the static screen displayed prompting users to switch the Jetpack app. */ "movedToJetpack.description" = "Aplicația Jetpack are toate funcționalitățile aplicației WordPress, cu acces exclusiv la Statistici, Cititor, Notificări și altele."; @@ -10619,6 +10534,30 @@ Example: Reply to Pamela Nguyen */ /* Message title for when a user has no sites. */ "mySite.noSites.title" = "Nu ai niciun site"; +/* Menu title for the add site option */ +"mySite.siteActions.addSite" = "Adaugă site"; + +/* Button that reveals more site actions */ +"mySite.siteActions.button" = "Acțiuni pe site"; + +/* Accessibility hint for button used to show more site actions */ +"mySite.siteActions.hint" = "Atinge pentru a afișa mai multe acțiuni pe site"; + +/* Menu title for the personalize home option */ +"mySite.siteActions.personalizeHome" = "Personalizează prima pagină"; + +/* Menu title for the change site icon option */ +"mySite.siteActions.siteIcon" = "Schimbă iconul site-ului"; + +/* Menu title for the change site title option */ +"mySite.siteActions.siteTitle" = "Schimbă titlul site-ului"; + +/* Menu title for the switch site option */ +"mySite.siteActions.switchSite" = "Comută site-ul"; + +/* Menu title for the visit site option */ +"mySite.siteActions.visitSite" = "Vizitează site-ul"; + /* Dismiss button title. */ "noResultsViewController.dismissButton" = "Închide"; @@ -10634,14 +10573,20 @@ Example: Reply to Pamela Nguyen */ /* This is one of the buttons we display when prompting the user for a review */ "notifications.appRatings.sendFeedback.yes.buttonTitle" = "Trimite impresii"; -/* Word separating the current index from the total amount. I.e.: 7 of 9 */ -"of" = "din"; +/* Badge for page cells */ +"pageList.badgeHomepage" = "Prima pagină"; + +/* Badge for page cells */ +"pageList.badgeLocalChanges" = "Schimbări locale"; -/* Label displayed on media items that are not video, image, or audio. */ -"other" = "altele"; +/* Badge for page cells */ +"pageList.badgePendingReview" = "Revizie în așteptare"; -/* Promote the page with Blaze. */ -"pages.blaze.actionTitle" = "Promovează cu Blaze"; +/* Badge for page cells */ +"pageList.badgePosts" = "Pagină articole"; + +/* Badge for page cells */ +"pageList.badgePrivate" = "Private"; /* Subtitle of the theme template homepage cell */ "pages.template.subtitle" = "Prima ta pagină folosește un șablon din temă și se va deschide în editorul web."; @@ -10649,6 +10594,36 @@ Example: Reply to Pamela Nguyen */ /* Title of the theme template homepage cell */ "pages.template.title" = "Prima pagină"; +/* Message informing the user that their static homepage page was set successfully */ +"pages.updatePage.successTitle" = "Am actualizat cu succes pagina"; + +/* Delete option in the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.actionTitle" = "Șterge definitiv"; + +/* Message of the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.alertMessage" = "Sigur vrei să ștergi definitiv această pagină?"; + +/* Title of the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.alertTitle" = "Ștergi definitiv?"; + +/* Menu option for filtering posts by everyone */ +"pagesList.pagesByEveryone" = "Paginile tuturor"; + +/* Menu option for filtering posts by me */ +"pagesList.pagesByMe" = "Paginile mele"; + +/* Trash option in the trash page confirmation alert. */ +"pagesList.trash.actionTitle" = "Aruncă la gunoi"; + +/* Message of the trash page confirmation alert. */ +"pagesList.trash.alertMessage" = "Sigur vrei să arunci la gunoi această pagină?"; + +/* Title of the trash page confirmation alert. */ +"pagesList.trash.alertTitle" = "Arunci la gunoi această pagină?"; + +/* Cancels an Action */ +"pagesList.trash.cancel" = "Anulează"; + /* No comment provided by engineer. */ "password" = "parolă"; @@ -10688,6 +10663,51 @@ Example: Reply to Pamela Nguyen */ /* Register Domain - Domain contact information field Phone */ "phone number" = "număr de telefon"; +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.createdTimeAgo" = "Creat %@"; + +/* Status mesasge for post cells */ +"post.deletingPostPermanentlyStatusMessage" = "Șterg articolul..."; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.editedTimeAgo" = "Editat %@"; + +/* Status mesasge for post cells */ +"post.movingToTrashStatusMessage" = "Arunc articolul la gunoi..."; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.publishedTimeAgo" = "Publicat %@"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.scheduledForDate" = "Programat %@"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.trashedTimeAgo" = "Aruncat la gunoi %@"; + +/* Accessibility label for the post author in the post list. The parameter is the author name. For example, \"By Elsa.\" */ +"postList.a11y.authorChunkFormat" = "De %@."; + +/* Accessibility label for a post's excerpt in the post list. The parameter is the post excerpt. For example, \"Excerpt. This is the first paragraph.\" */ +"postList.a11y.exerptChunkFormat" = "Rezumat. %@."; + +/* Accessibility label for a sticky post in the post list. */ +"postList.a11y.sticky" = "Reprezentativ."; + +/* Accessibility label for a post in the post list. The first placeholder is the post title. The second placeholder is the date. */ +"postList.a11y.titleAndDateChunkFormat" = "%1$@, %2$@."; + +/* Title for the 'Trash' post list row swipe action */ +"postList.swipeActionDelete" = "Aruncă la gunoi"; + +/* Title for the 'Delete' post list row swipe action */ +"postList.swipeActionDeletePermanently" = "Șterge"; + +/* Title for the 'Share' post list row swipe action */ +"postList.swipeActionShare" = "Partajează"; + +/* Title for the 'View' post list row swipe action */ +"postList.swipeActionView" = "Vizualizează"; + /* User action to dismiss featured media options. */ "postSettings.featuredImageUploadActionSheet.dismiss" = "Închide"; @@ -10706,9 +10726,84 @@ Example: Reply to Pamela Nguyen */ /* Button in Post Settings */ "postSettings.setFeaturedImageButton" = "Stabilește imaginea reprezentativă"; +/* Error message on post/page settings screen */ +"postSettings.updateFailedMessage" = "Actualizarea setărilor pentru articole a eșuat"; + /* Promote the post with Blaze. */ "posts.blaze.actionTitle" = "Promovează cu Blaze"; +/* Label for the Post List option that cancels automatic uploading of a post. */ +"posts.cancelUpload.actionTitle" = "Anulează încărcarea"; + +/* Label for post comments option. Tapping displays comments for a post. */ +"posts.comments.actionTitle" = "Comentarii"; + +/* Label for the delete post option. Tapping permanently deletes a post. */ +"posts.delete.actionTitle" = "Șterge definitiv"; + +/* Label for an option that moves a post to the draft folder */ +"posts.draft.actionTitle" = "Mută în ciorne"; + +/* Label for post duplicate option. Tapping creates a copy of the post. */ +"posts.duplicate.actionTitle" = "Fă duplicat"; + +/* Opens a submenu for page attributes. */ +"posts.pageAttributes.actionTitle" = "Atribute pagină"; + +/* Label for the preview post button. Tapping displays the post as it appears on the web. */ +"posts.preview.actionTitle" = "Previzualizează"; + +/* Label for an option that moves a publishes a post immediately */ +"posts.publish.actionTitle" = "Publică acum"; + +/* Retry uploading the post. */ +"posts.retry.actionTitle" = "Reîncearcă"; + +/* Set the selected page as the homepage. */ +"posts.setHomepage.actionTitle" = "Setează ca prima pagină"; + +/* Set the parent page for the selected page. */ +"posts.setParent.actionTitle" = "Setează un părinte"; + +/* Set the selected page as a posts page. */ +"posts.setPostsPage.actionTitle" = "Setează ca pagină articole"; + +/* Set the selected page as a regular page. */ +"posts.setRegularPage.actionTitle" = "Setează ca pagină obișnuită"; + +/* Label for post settings option. Tapping displays settings for a post. */ +"posts.settings.actionTitle" = "Setări"; + +/* Share the post. */ +"posts.share.actionTitle" = "Partajează"; + +/* Label for post stats option. Tapping displays statistics for a post. */ +"posts.stats.actionTitle" = "Statistici"; + +/* Label for a option that moves a post to the trash folder */ +"posts.trash.actionTitle" = "Aruncă la gunoi"; + +/* Label for the view post button. Tapping displays the post as it appears on the web. */ +"posts.view.actionTitle" = "Vizualizează"; + +/* A short message explaining that a page was deleted permanently. */ +"postsList.deletePage.message" = "Am șters definitiv pagina"; + +/* A short message explaining that a post was deleted permanently. */ +"postsList.deletePost.message" = "Am șters definitiv articolul"; + +/* A short message explaining that a page was moved to the trash bin. */ +"postsList.movePageToTrash.message" = "Am aruncat pagina la gunoi"; + +/* A short message explaining that a post was moved to the trash bin. */ +"postsList.movePostToTrash.message" = "Am aruncat articolul la gunoi"; + +/* Menu option for filtering posts by everyone */ +"postsList.postsByEveryone" = "Articolele tuturor"; + +/* Menu option for filtering posts by me */ +"postsList.postsByMe" = "Articolele mele"; + /* Title for the button to subscribe to Jetpack Social on the remaining shares view */ "postsettings.social.remainingshares.subscribe" = "Abonează-te acum ca să partajezi mai mult"; @@ -10853,13 +10948,15 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Text for the 'Like' button on the reader post card cell. */ "reader.post.button.like" = "Apreciază"; -/* Accessibility hint for the like button on the reader post card cell - Accessibility hint for the liked button on the reader post card cell */ +/* Accessibility hint for the like button on the reader post card cell */ "reader.post.button.like.accessibility.hint" = "Apreciază articolul."; /* Text for the 'Liked' button on the reader post card cell. */ "reader.post.button.liked" = "Apreciat"; +/* Accessibility hint for the liked button on the reader post card cell */ +"reader.post.button.liked.accessibility.hint" = "Anulează aprecierea articolului."; + /* Accessibility hint for the site header on the reader post card cell */ "reader.post.button.menu.accessibility.hint" = "Deschide un meniu cu mai multe acțiuni."; @@ -10923,6 +11020,12 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Title for the tooltip anchor. */ "readerDetail.tooltipAnchorTitle.accessibilityLabel" = "Nou"; +/* The button title for the transfer footer view in Register Domain screen */ +"register.domain.transfer.button.title" = "Transferă domeniul"; + +/* The title for the transfer footer view in Register Domain screen */ +"register.domain.transfer.title" = "Vrei să transferi un domeniu pe care îl deții deja?"; + /* Information of what related post are and how they are presented */ "relatedPostsSettings.optionsFooter" = "Articole similare afișează conținut relevant de pe site-ul tău sub articolele tale."; @@ -11022,6 +11125,21 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Accessibility hint for actions when displaying media items. */ "siteMedia.cellAccessibilityHint" = "Selectează media."; +/* Accessibility hint for media item preview for user's viewing an item in their media library */ +"siteMediaItem.contentViewAccessibilityHint" = "Atinge pentru a vizualiza elementul media pe ecran complet"; + +/* Accessibility label for media item preview for user's viewing an item in their media library */ +"siteMediaItem.contentViewAccessibilityLabel" = "Previzualizează elementul media"; + +/* Title for confirmation navigation bar button item */ +"siteMediaPicker.add" = "Adaugă"; + +/* Button selection media in media picker */ +"siteMediaPicker.deselect" = "Anulează selecția"; + +/* Button selection media in media picker */ +"siteMediaPicker.select" = "Selectează"; + /* Media screen navigation title */ "siteMediaPicker.title" = "Media"; @@ -11029,7 +11147,7 @@ Example: given a notice format "Following %@" and empty site name, this will be "siteSettings.privacy.title" = "Confidențialitate"; /* Hint for users when hidden privacy setting is set */ -"siteVisibility.hidden.hint" = "Site-ul tău este vizibil pentru toată lumea, dar cere motoarelor de căutare să nu-ți indexeze site-ul."; +"siteVisibility.hidden.hint" = "Până când va fi pregătit pentru vizualizare, site-ul tău este ascuns pentru vizitatori în spatele unei notificări „În curând”."; /* Text for privacy settings: Hidden */ "siteVisibility.hidden.title" = "Ascuns"; @@ -11185,11 +11303,17 @@ Example: given a notice format "Following %@" and empty site name, this will be "stats.insights.totalLikes.guideText.plural" = "Ultimul tău articol %1$@ a avut %2$@ aprecieri."; /* A hint shown to the user in stats informing the user that one of their posts has received a like. The %1$@ placeholder will be replaced with the title of a post, and the %2$@ will be replaced by the numeral one. */ -"stats.insights.totalLikes.guideText.singular" = "Ultimul tău articol %1$@ a avut %2$@ aprecieri."; +"stats.insights.totalLikes.guideText.singular" = "Ultimul tău articol %1$@ a avut %2$@ apreciere."; /* Dismiss the AlertView */ "stockPhotos.strings.dismiss" = "Refuză"; +/* Subtitle for placeholder in Free Photos. The company name 'Pexels' should always be written as it is. */ +"stockPhotos.subtitle" = "Fotografii oferite de Pexels"; + +/* Title for placeholder in Free Photos */ +"stockPhotos.title" = "Caută pentru a găsi fotografii gratuite pe care să le adaugi în biblioteca ta Media!"; + /* Section title for prominent suggestions */ "suggestions.section.prominent" = "În această conversație"; @@ -11337,6 +11461,9 @@ Example: given a notice format "Following %@" and empty site name, this will be /* View title for Help & Support page. */ "support.title" = "Ajutor"; +/* Title for placeholder in Tenor picker */ +"tenor.welcomeMessage" = "Caută pentru a găsi imagini GIF pe care să le adaugi în biblioteca ta Media!"; + /* Header of delete screen section listing things that will be deleted. */ "these items will be deleted:" = "aceste elemente vor fi șterse:"; @@ -11352,9 +11479,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Displayed in the confirmation alert when marking unread notifications as read. */ "unread" = "necitite"; -/* Label displayed on video media items. */ -"video" = "video"; - /* Portion of a message for Jetpack users that have multisite WP installation, thus Restore is not available. This part is a link, colored with a different color. */ "visit our documentation page" = "vizitează pagina cu documentație"; diff --git a/WordPress/Resources/ru.lproj/Localizable.strings b/WordPress/Resources/ru.lproj/Localizable.strings index 2d4c9586cb02..c10b87ebb087 100644 --- a/WordPress/Resources/ru.lproj/Localizable.strings +++ b/WordPress/Resources/ru.lproj/Localizable.strings @@ -1,4 +1,4 @@ -/* Translation-Revision-Date: 2023-10-16 18:22:32+0000 */ +/* Translation-Revision-Date: 2024-01-03 14:54:09+0000 */ /* Plural-Forms: nplurals=3; plural=(n % 10 == 1 && n % 100 != 11) ? 0 : ((n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14)) ? 1 : 2); */ /* Generator: GlotPress/4.0.0-alpha.11 */ /* Language: ru */ @@ -152,9 +152,6 @@ /* Difference label for Period Overview stat, indicating change from previous period. Ex: +99.9K (5%) */ "%@%@ (%@%%)" = "%1$@%2$@ (%3$@%%)"; -/* Accessibility label for a post in the post list. The parameters are the title, and date respectively. For example, \"Let it Go, 1 hour ago.\" */ -"%@, %@." = "%1$@, %2$@."; - /* Accessibility value for a Stats' Posting Activity Month if the user selected a day with posts. The first parameter is day (e.g. November 2019). The second parameter is the number of posts. */ "%@. %d posts." = "%1$@. %2$d записей."; @@ -210,9 +207,6 @@ /* Age between dates over one year. */ "%d years" = "%d лет"; -/* Max image size in pixels (e.g. 300x300px) */ -"%dx%dpx" = "%1$dx%2$d пикс."; - /* One menu area available in the theme */ "%i menu area in this theme" = "Число разделов меню в этой теме: %i"; @@ -271,6 +265,9 @@ translators: Block name. %s: The localized block name */ /* translators: %s: social link name e.g: \"Instagram\". */ "%s social icon" = "Значок соцсети %s"; +/* translators: displayed right after the classic block is converted to blocks. %s: The localized classic block name */ +"'%s' block converted to blocks" = "Блок '%s' сконвертирован в блоки"; + /* translators: Missing block alert title. %s: The localized block name */ "'%s' is not fully-supported" = "'%s' полностью не поддерживается"; @@ -478,13 +475,6 @@ translators: Block name. %s: The localized block name */ /* Label for the Activity Type filter button when there are more than 1 activity type selected */ "Activity Type (%1$d)" = "Тип активности (%1$d)"; -/* Accessibility label for add button to add items to the user's media library - Remove asset from media picker list */ -"Add" = "Добавить"; - -/* Action for Media Picker to indicate selection of media. The argument in the string represents the number of elements (as numeric digits) selected */ -"Add %@" = "Добавить %@"; - /* No comment provided by engineer. */ "Add Block After" = "Добавить блок после"; @@ -581,9 +571,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Add menu item to children" = "Добавить элемент меню к дочерним объектам"; -/* Accessibility hint for add button to add items to the user's media library */ -"Add new media" = "Добавить медиафайл"; - /* Screen reader text for Menus button that adds a new menu to a site. */ "Add new menu" = "Добавить новое меню"; @@ -649,9 +636,6 @@ translators: Block name. %s: The localized block name */ /* Option to select the Airmail app when logging in with magic links */ "Airmail" = "Airmail"; -/* Description of albums in the photo libraries */ -"Albums" = "Альбомы"; - /* Image alignment option title. Title of the screen for choosing an image's alignment. */ "Alignment" = "Выравнивание"; @@ -665,6 +649,9 @@ translators: Block name. %s: The localized block name */ Title of the drafts filter. This filter shows a list of draft posts. */ "All" = "Все"; +/* Information about redeeming domain credit on site dashboard. */ +"All WordPress.com annual plans include a custom domain name. Register your free domain now." = "Все годовые тарифные планы WordPress.com включают пользовательский домен. Зарегистрируйте ваш бесплатный домен."; + /* Footer of the free domain registration section for a paid plan. Information about redeeming domain credit on site dashboard. */ "All WordPress.com plans include a custom domain name. Register your free premium domain now." = "Все тарифы WordPress.com включают возможность зарегистрировать пользовательское имя домена. Сделайте это сейчас."; @@ -730,10 +717,13 @@ translators: Block name. %s: The localized block name */ "Alt Text" = "Атрибут alt"; /* No comment provided by engineer. */ -"Alternatively, you can detach and edit these blocks separately by tapping “Detach patterns”." = "Кроме того, вы можете отсоединить и отредактировать эти блоки отдельно, нажав «Отсоединить паттерны»."; +"Alternatively, you can convert the content to blocks." = "Альтернативно вы можете конвертировать содержимое в блоки."; /* No comment provided by engineer. */ -"Alternatively, you can detach and edit this block separately by tapping “Detach pattern”." = "Кроме того, вы можете отсоединить и отредактировать этот блок отдельно, нажав «Отсоединить паттерн»."; +"Alternatively, you can detach and edit this block separately by tapping “Detach”." = "Кроме того, вы можете отсоединить и отредактировать этот блок отдельно, нажав «Отсоединить»."; + +/* translators: Alternative option included in a warning related to having blocks deeply nested. */ +"Alternatively, you can flatten the content by ungrouping the block." = "Альтернативно вы можете сгладить содержимое, разгруппировав блок."; /* Instruction text to explain to help users type their password instead of using magic link login option. */ "Alternatively, you may enter the password for this account." = "В качестве альтернативы можно ввести пароль для этой учётной записи."; @@ -882,15 +872,9 @@ translators: Block name. %s: The localized block name */ /* Message prompting the user to confirm that they want to disconnect Jetpack from the site. */ "Are you sure you want to disconnect Jetpack from the site?" = "Вы уверены, что хотите отключить Jetpack от этого сайта?"; -/* Message prompting the user to confirm that they want to permanently delete a group of media items. */ -"Are you sure you want to permanently delete these items?" = "Вы уверены, что хотите безвозвратно удалить выбранное?"; - /* Message prompting the user to confirm that they want to permanently delete a media item. Should match Calypso. */ "Are you sure you want to permanently delete this item?" = "Вы хотите навсегда удалить этот элемент?"; -/* Message of the confirmation alert when deleting a page from the trash. */ -"Are you sure you want to permanently delete this page?" = "Вы уверены, что хотите удалить эту страницу навсегда?"; - /* Message of the confirmation alert when deleting a post from the trash. */ "Are you sure you want to permanently delete this post?" = "Вы точно хотите удалить эту запись навсегда?"; @@ -922,9 +906,6 @@ translators: Block name. %s: The localized block name */ /* Title of message shown when user taps submit for review. */ "Are you sure you want to submit for review?" = "Вы точно хотите отправить на одобрение?"; -/* Message of the trash page confirmation alert. */ -"Are you sure you want to trash this page?" = "Вы уверены, что хотите переместить страницу в корзину?"; - /* Message of the trash confirmation alert. */ "Are you sure you want to trash this post?" = "Вы уверены, что хотите удалить эту запись?"; @@ -965,9 +946,6 @@ translators: Block name. %s: The localized block name */ /* translators: accessibility text. Empty Audio caption. */ "Audio caption. Empty" = "Подпись к аудиозаписи. Пустая"; -/* Accessibility label for audio items in the media collection view. The parameter is the creation date of the audio. */ -"Audio, %@" = "Аудио, %@"; - /* No comment provided by engineer. */ "Authenticating" = "Идёт авторизация"; @@ -1178,10 +1156,7 @@ translators: Block name. %s: The localized block name */ "Blocks menu" = "Меню \"Блоки\""; /* translators: Warning related to having blocks deeply nested. %d: The deepest nesting level. */ -"Blocks nested deeper than %d levels may not render properly in the mobile editor. For this reason, we recommend flattening the content by ungrouping the block or editing the block using the web editor." = "Блоки, вложенные глубже, чем %d уровней, могут отображаться неправильно в мобильном редакторе. По этой причине мы рекомендуем сгладить содержимое, разгруппировав блок или отредактировать блок с помощью веб-редактора."; - -/* translators: Warning related to having blocks deeply nested. %d: The deepest nesting level. */ -"Blocks nested deeper than %d levels may not render properly in the mobile editor. For this reason, we recommend flattening the content by ungrouping the block or editing the block using your web browser." = "Блоки, вложенные глубже, чем %d уровней, могут отображаться неправильно в мобильном редакторе. По этой причине мы рекомендуем сгладить содержимое, разгруппировав блок или отредактировать его с помощью веб-браузера."; +"Blocks nested deeper than %d levels may not render properly in the mobile editor." = "Блоки, вложенные глубже, чем %d уровней, могут отображаться неправильно в мобильном редакторе."; /* Title of a button that displays the WordPress.com blog */ "Blog" = "Блог"; @@ -1259,9 +1234,6 @@ translators: Block name. %s: The localized block name */ /* Label for the post author in the post detail. */ "By " = "Автор: "; -/* Accessibility label for the post author in the post list. The parameter is the author name. For example, \"By Elsa.\" */ -"By %@." = "Автор: %@."; - /* Legal disclaimer for logging in. The underscores _..._ denote underline. */ "By continuing, you agree to our _Terms of Service_." = "Продолжив, вы соглашаетесь с нашими _Правилами пользования_."; @@ -1281,8 +1253,7 @@ translators: Block name. %s: The localized block name */ /* Label for size of media while it's being calculated. */ "Calculating..." = "Идет подсчет..."; -/* Accessibility label for taking an image or video with the camera on formatting toolbar. - Accessibility label for the camera tile in the collection view */ +/* Accessibility label for taking an image or video with the camera on formatting toolbar. */ "Camera" = "Камера"; /* Title of an alert informing the user the camera permission for the app is disabled and its needed to proceed */ @@ -1333,10 +1304,7 @@ translators: Block name. %s: The localized block name */ Cancels an Action Cancels an alert. Cancels the mark all as read action. - Dismiss the post action sheet Editing GIF alert cancel action button. - Label for a cancel button - Label for the auto-upload cancelation button in the post list. Tapping will prevent the app from auto-uploading the post. Menus cancel button for deleting a menu. Menus cancel button within text bar while editing items. Menus: Cancel button title for canceling an edited menu item. @@ -1353,10 +1321,6 @@ translators: Block name. %s: The localized block name */ Verb. Title for Jetpack Restore cancel button. */ "Cancel" = "Отменить"; -/* Label for the Post List option that cancels automatic uploading of a post. - Media Library option to cancel an in-progress or failed upload. */ -"Cancel Upload" = "Отменить загрузку"; - /* Menus alert button title to cancel discarding changes and not createa a new menu. Menus alert button title to cancel discarding changes and not select a new menu Menus alert button title to cancel discarding changes and not select a new menu location */ @@ -1415,9 +1379,6 @@ translators: Block name. %s: The localized block name */ Main title */ "Change Password" = "Изменить пароль"; -/* Title of button that takes user to the system Settings section for the app */ -"Change Settings" = "Изменить настройки"; - /* Change Username title. */ "Change Username" = "Сменить имя пользователя"; @@ -1557,9 +1518,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Choose file" = "Выбрать файл"; -/* Menu option for selecting media from the device's photo library. */ -"Choose from My Device" = "Выбрать с моего устройства"; - /* Explanatory text for Homepage Settings homepage type selection. */ "Choose from a homepage that displays your latest posts (classic blog) or a fixed \/ static page." = "Выберите в качестве главной страницы страницу последних записей (классический блог) или фиксированную (статическую) страницу."; @@ -1760,9 +1718,6 @@ translators: Block name. %s: The localized block name */ /* Community & Non-Profit site intent topic */ "Community & Non-Profit" = "Общество и некоммерческая деятельность"; -/* Accessibility indication that the current Post List style is currently Compact. */ -"Compact" = "Компактный"; - /* The action is completed */ "Completed" = "Завершено"; @@ -1948,10 +1903,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Copied block" = "Скопированный блок"; -/* Copy the post url and paste anywhere in phone - Label for page copy link. Tapping copy the url of page */ -"Copy Link" = "Скопировать ссылку"; - /* Copy link to oomment button title */ "Copy Link to Comment" = "Скопировать ссылку в комментарий"; @@ -2060,9 +2011,6 @@ translators: Block name. %s: The localized block name */ /* Error title displayed when unable to close user account. */ "Couldn’t close account automatically" = "Не получается автоматически закрыть учётную запись"; -/* Message to show while media data source is finding the number of items available. */ -"Counting media items..." = "Подсчёт медиафайлов…"; - /* Period Stats 'Countries' header */ "Countries" = "Страны"; @@ -2313,7 +2261,6 @@ translators: Block name. %s: The localized block name */ /* Delete Delete button title for the warning shown to the user when he refuses to re-login when the authToken is missing. Title for button that permanently deletes a media item (photo / video) - Title for button that permanently deletes one or more media items (photos / videos) Title of a delete button Title of the trash confirmation alert. */ "Delete" = "Удалить"; @@ -2321,15 +2268,11 @@ translators: Block name. %s: The localized block name */ /* Menus confirmation button for deleting a menu. */ "Delete Menu" = "Удалить меню"; -/* Delete option in the confirmation alert when deleting a page from the trash. - Delete option in the confirmation alert when deleting a post from the trash. - Label for a button permanently deletes a page. - Label for the delete post option. Tapping permanently deletes a post. +/* Delete option in the confirmation alert when deleting a post from the trash. Title for button on the comment details page that deletes the comment when tapped. */ "Delete Permanently" = "Удалить навсегда"; -/* Title of the confirmation alert when deleting a page from the trash. - Title of the confirmation alert when deleting a post from the trash. */ +/* Title of the confirmation alert when deleting a post from the trash. */ "Delete Permanently?" = "Удалить навсегда?"; /* Button label for deleting the current site @@ -2448,7 +2391,6 @@ translators: Block name. %s: The localized block name */ /* Accessibility label for button to dismiss a bottom sheet Accessibility label for the transparent space above the login dialog which acts as a button to dismiss the dialog. Accessibility label for the transparent space above the signup dialog which acts as a button to dismiss the dialog. - Action to show on alert when view asset fails. Dismiss a view. Verb */ "Dismiss" = "Игнорировать"; @@ -2466,12 +2408,6 @@ translators: Block name. %s: The localized block name */ User's Display Name */ "Display Name" = "Отображаемое имя"; -/* Accessibility label for other media items in the media collection view. The parameter is the creation date of the document. */ -"Document, %@" = "Документ, %@"; - -/* Accessibility label for other media items in the media collection view. The parameter is the filename file. */ -"Document: %@" = "Документ: %@"; - /* Message shown when all tours have been completed */ "Doesn't it feel good to cross things off a list?" = "не правда ли, хорошо выбирать вещи из списка?"; @@ -2632,8 +2568,7 @@ translators: Block name. %s: The localized block name */ /* Description of a Quick Start Tour */ "Draft and publish a post." = "Создайте черновик и опубликуйте запись."; -/* Title of the drafts filter. This filter shows a list of draft posts. - Title of the drafts header in search list. */ +/* Title of the drafts filter. This filter shows a list of draft posts. */ "Drafts" = "Черновики"; /* No comment provided by engineer. */ @@ -2645,10 +2580,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Drag to adjust focal point" = "Перетащите для изменения фокальной точки"; -/* Label for page duplicate option. Tapping creates a copy of the page. - Label for post duplicate option. Tapping creates a copy of the post. */ -"Duplicate" = "Дублировать"; - /* No comment provided by engineer. */ "Duplicate block" = "Дублировать блок"; @@ -2661,13 +2592,9 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Each block has its own settings. To find them, tap on a block. Its settings will appear on the toolbar at the bottom of the screen." = "У каждого блока свои настройки. Чтобы найти их, нажмите на блок. Его настройки появятся на панели инструментов внизу экрана."; -/* Accessibility label for edit button to enable multi selection mode in the user's media library - Edit the post. - Editing GIF alert default action button. +/* Editing GIF alert default action button. Edits a Comment Edits the comment - Label for a button that opens the Edit Page view controller - Label for the edit post button. Tapping displays the editor. User action to edit media details. Verb, edit a comment */ "Edit" = "Изменить"; @@ -2675,9 +2602,6 @@ translators: Block name. %s: The localized block name */ /* Title for the edit more button section */ "Edit \"More\" button" = "Изменить кнопку «Ещё»"; -/* Button that displays the media editor to the user */ -"Edit %@" = "Изменить %@"; - /* Blocklist Keyword Edition Title */ "Edit Blocklist Word" = "Редактировать слово из списка блокировки"; @@ -2870,9 +2794,6 @@ translators: Block name. %s: The localized block name */ /* Secondary message shown when there are no domains that match the user entered text. */ "Enter different words above and we'll look for an address that matches it." = "Введите другие слова выше и мы поищем адреса совпадающие с ними."; -/* Accessibility hint for edit button to enable multi selection mode in the user's media library */ -"Enter edit mode to enable multi select to delete" = "Перейдите в режим редактирования, чтобы включить множественный выбор для удаления"; - /* (placeholder) Help enter WordPress password Placeholder of a field to type a password to protect the post. */ "Enter password" = "Введите пароль"; @@ -3028,9 +2949,6 @@ translators: Block name. %s: The localized block name */ /* Short title telling the user they will receive a blogging reminder every day of the week. */ "Every day at %@" = "Каждый день в %@"; -/* Label for the post author filter. This filter shows posts for all users on the blog. */ -"Everyone" = "Все"; - /* Example story title description */ "Example story title" = "Пример названия истории"; @@ -3040,9 +2958,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Excerpt length (words)" = "Длина отрывка (слов)"; -/* Accessibility label for a post's excerpt in the post list. The parameter is the post excerpt. For example, \"Excerpt. This is the first paragraph.\" */ -"Excerpt. %@." = "Отрывок. %@."; - /* Should be the same as the text displayed if the user clicks the (i) in Calypso. */ "Excerpts are optional hand-crafted summaries of your content." = "Отрывки — это предложения, кратко описывающие содержание вашей записи."; @@ -3052,8 +2967,7 @@ translators: Block name. %s: The localized block name */ /* Accessibility Label for the exit full screen button on the full screen comment reply mode */ "Exit Full Screen" = "Выйти из полноэкранного режима"; -/* Accessibility indication that the current Post List style is currently Expanded. - Screen reader text to represent the expanded state of a UI control */ +/* Screen reader text to represent the expanded state of a UI control */ "Expanded" = "Развернутый"; /* Accessibility hint */ @@ -3103,9 +3017,6 @@ translators: Block name. %s: The localized block name */ The action failed */ "Failed" = "Произошла ошибка"; -/* Error title when picked media cannot be imported into stories. */ -"Failed Media Export" = "Экспорт медиа неудачен"; - /* Message for mark all as read success notice */ "Failed marking Notifications as read" = "Ошибка отметки всех уведомлений как прочитанных"; @@ -3307,6 +3218,12 @@ translators: Block name. %s: The localized block name */ /* An example tag used in the login prologue screens. */ "Football" = "Футбол"; +/* translators: Recommendation included in a warning related to having blocks deeply nested. */ +"For this reason, we recommend editing the block using the web editor." = "По этой причине мы рекомендуем редактировать блок с помощью веб-редактора."; + +/* translators: Recommendation included in a warning related to having blocks deeply nested. */ +"For this reason, we recommend editing the block using your web browser." = "По этой причине мы рекомендуем редактировать блок с помощью веб-браузера."; + /* Register Domain - Domain contact information section header description */ "For your convenience, we have pre-filled your WordPress.com contact information. Please review to be sure it’s the correct information you want to use for this domain." = "Для вашего удобства, мы заполнили вашу контактную информацию WordPress.com. Пожалуйста, перепроверьте её на корректность, действительно ли вы хотите использовать её для этого домена."; @@ -3624,8 +3541,7 @@ translators: Block name. %s: The localized block name */ "Home" = "Главная"; /* Title for setting which shows the current page assigned as a site's homepage - Title for the homepage section in site settings screen - Title of the Homepage Badge */ + Title for the homepage section in site settings screen */ "Homepage" = "Главная страница"; /* Label for Homepage Settings site settings section @@ -3722,9 +3638,6 @@ translators: Block name. %s: The localized block name */ /* Hint for image title on image settings. */ "Image title" = "Заголовок изображения"; -/* Accessibility label for image thumbnails in the media collection view. The parameter is the creation date of the image. */ -"Image, %@" = "Изображение, %@"; - /* Undated post time label */ "Immediately" = "Немедленно"; @@ -4210,9 +4123,6 @@ translators: Block name. %s: The localized block name */ Settings: Comments Approval settings */ "Links in comments" = "Ссылки в комментариях"; -/* The accessibility label for the list style button in the Post List. */ -"List style" = "Стиль списка"; - /* Title of the screen that load selected the revisions. */ "Load" = "Загрузка"; @@ -4228,18 +4138,12 @@ translators: Block name. %s: The localized block name */ /* Text displayed while loading the activity feed for a site */ "Loading Backups..." = "Загружаются резервные копии..."; -/* Phrase to show when the user has searched for GIFs and they are being loaded. */ -"Loading GIFs..." = "Загрузка GIF..."; - /* Menus label text displayed while menus are loading */ "Loading Menus..." = "Загрузка меню..."; /* Text displayed while loading site People. */ "Loading People..." = "Загрузка информации о людях..."; -/* Phrase to show when the user has searched for images and they are being loaded. */ -"Loading Photos..." = "Загрузка фото..."; - /* Text displayed while loading plans details */ "Loading Plan..." = "Загрузка тарифного плана..."; @@ -4300,8 +4204,7 @@ translators: Block name. %s: The localized block name */ /* Local Services site intent topic */ "Local Services" = "Местные услуги"; -/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. - Title of the Local Changes Badge */ +/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. */ "Local changes" = "Локальные изменения"; /* Title for alert when a generic error happened when trying to find the location of the device */ @@ -4465,7 +4368,6 @@ translators: Block name. %s: The localized block name */ "Max Video Upload Size" = "Максимальный размер загружаемого видеофайла"; /* Accessibility label for the Me button in My Site. - Label for the post author filter. This filter shows posts only authored by the current user. Me page title Noun. Title. Links to the Me screen. The accessibility value of the me tab. @@ -4473,9 +4375,7 @@ translators: Block name. %s: The localized block name */ "Me" = "Я"; /* Noun. Title. Links to the blog's Media library. - Tab bar title for the Media tab in Media Picker The menu item to select during a guided tour. - Title for Media Library section of the app. Title for the media section in site settings screen Title label for the media settings section in the app settings */ "Media" = "Медиа"; @@ -4487,13 +4387,6 @@ translators: Block name. %s: The localized block name */ /* Label for size of media cache in the app. */ "Media Cache Size" = "Размер кэша медиафайлов"; -/* Title for alert when access to media capture is not granted */ -"Media Capture" = "Получение медиаданных"; - -/* Title for alert when a generic error happened when loading media - Title for alert when access to the media library is not granted by the user */ -"Media Library" = "Библиотека медиафайлов"; - /* Title for action sheet with media options. */ "Media Options" = "Параметры медиафайлов"; @@ -4516,9 +4409,6 @@ translators: Block name. %s: The localized block name */ /* translators: %s: block title e.g: \"Paragraph\". */ "Media options" = "Настройки мультимедиа"; -/* Alert title when there is issues loading an asset to preview. */ -"Media preview failed." = "Не удалось загрузить медиафайл для предварительного просмотра."; - /* Alert displayed to the user when multiple media items have uploaded successfully. */ "Media uploaded (%ld files)" = "Загружено медиафайлов (%ld)"; @@ -4556,9 +4446,6 @@ translators: Block name. %s: The localized block name */ Label for the share message field on the post settings. */ "Message" = "Сообщение"; -/* Title of section containing image / video metadata such as size and file type */ -"Metadata" = "Метаданные"; - /* Option to select the Microsft Outlook app when logging in with magic links */ "Microsoft Outlook" = "Microsoft Outlook"; @@ -4578,13 +4465,11 @@ translators: Block name. %s: The localized block name */ "Months and Years" = "Месяцы и года"; /* Accessibility label for more button in dashboard quick start card. - Accessibility label for the More button in Page List. Accessibility label for the More button in Post List (compact view). Accessibility label for the More button on formatting toolbar. Accessibility label for the More button on Reader Cell Accessibility label for the More button on Reader's post details - Action button to display more available options - Label for the more post button. Tapping displays an action sheet with post options. */ + Action button to display more available options */ "More" = "Еще"; /* Action button to display more available options @@ -4642,15 +4527,8 @@ translators: Block name. %s: The localized block name */ /* Screen reader text for button that will move the menu item */ "Move menu item" = "Перемещение элемента меню"; -/* Label for a button that moves a page to the draft folder - Label for an option that moves a post to the draft folder */ -"Move to Draft" = "Переместить в черновики"; - -/* Label for a button that moves a page to the trash folder - Label for a option that moves a post to the trash folder - Title for button on the comment details page that moves the comment to trash when tapped. +/* Title for button on the comment details page that moves the comment to trash when tapped. Trash option in the trash confirmation alert. - Trash option in the trash page confirmation alert. Trashes the comment */ "Move to Trash" = "Удалить"; @@ -4682,7 +4560,8 @@ translators: Block name. %s: The localized block name */ Title of My Site tab */ "My Site" = "Мой сайт"; -/* Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ +/* Title for site picker screen. + Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ "My Sites" = "Мои сайты"; /* Siri Suggestion to open My Sites */ @@ -4932,9 +4811,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title for the view when there aren't any Activities to display in the Activity Log for a given filter. */ "No matching events found." = "Соответствующие события не найдены."; -/* Message displayed when no results are returned from a media library search. Should match Calypso. - Phrase to show when the user search for images but there are no result to show. - Phrase to show when the user searches for GIFs but there are no result to show. */ +/* Message displayed when no results are returned from a media library search. Should match Calypso. */ "No media matching your search" = "Нет медиафайлов по критериям поиска"; /* Menus text shown when no menus were available for loading the Menus editor. */ @@ -4952,8 +4829,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Displayed in the Notifications Tab as a title, when there are no notifications */ "No notifications yet" = "Уведомлений пока нет"; -/* Displayed when the user is searching the pages list and there are no matching pages - Text displayed when there's no matching with the text search */ +/* Text displayed when there's no matching with the text search */ "No pages matching your search" = "Нет страниц по критериям поиска"; /* Text displayed when search for plugins returns no results */ @@ -4974,9 +4850,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Message shown whent the reader finds no posts for the chosen tag */ "No posts have been made recently with this tag." = "В последнее время не было записей с этой меткой."; -/* Displayed when the user is searching the posts list and there are no matching posts */ -"No posts matching your search" = "Нет записей по критериям поиска"; - /* Accessibility value for a Stats' Posting Activity Month if there are no posts. */ "No posts." = "Записей нет."; @@ -5077,9 +4950,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* A message title */ "Nothing liked yet" = "Пока без отметок \"нравится\""; -/* Default message for empty media picker */ -"Nothing to show" = "Нечего показать"; - /* Notifications Details Accessibility Identifier */ "Notification Details Table" = "Таблица сведений об уведомлениях"; @@ -5139,7 +5009,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Alert dismissal title Button title. Acknowledges a prompt. Button title. An acknowledgement of the message displayed in a prompt. - Confirmation of action Default action Dismisses the alert Marks all notifications as read. @@ -5201,9 +5070,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* No comment provided by engineer. */ "Only show excerpt" = "Показывать только отрывок"; -/* Message telling the user that they've only enabled limited photo library permissions for the app. */ -"Only the selected photos you've given access to are available." = "Доступны только выбранные фотографии, к которым вы предоставили доступ."; - /* An informal exclaimation that means `something went wrong`. Title for the view when there's an error loading a comment. Title for the view when there's an error loading Activity Log @@ -5238,9 +5104,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title of a button that opens the apps settings in the system Settings.app */ "Open Settings" = "Открыть «Настройки»"; -/* Editor button to swich the media picker from quick mode to full picker */ -"Open full media picker" = "Открыть инструмент выбора медиафайлов полностью"; - /* No comment provided by engineer. */ "Open in Safari" = "Открыть в Safari"; @@ -5280,6 +5143,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Divider on initial auth view separating auth options. */ "Or" = "или"; +/* Instruction text for other forms of two-factor auth methods. */ +"Or choose another form of authentication." = "Или выберите иную форму авторизации."; + /* Label for button to log in using site address. Underscores _..._ denote underline. */ "Or log in by _entering your site address_." = "Или войдите с _адресом вашего сайта_."; @@ -5338,15 +5204,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title shown when selecting a post type of Page from the Share Extension. */ "Page" = "Страница"; -/* Prompts the user that a restored page was moved to the drafts list. */ -"Page Restored to Drafts" = "Страница восстановлена в списке \"Черновики\""; - -/* Prompts the user that a restored page was moved to the published list. */ -"Page Restored to Published" = "Страница восстановлена в списке \"Публикации\""; - -/* Prompts the user that a restored page was moved to the scheduled list. */ -"Page Restored to Scheduled" = "Страница восстановлена в списке \"Планируемые публикации\""; - /* Name of the button to open the page settings The title of the Page Settings screen. */ "Page Settings" = "Настройки страницы"; @@ -5363,9 +5220,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of notification displayed when a page has failed to upload. */ "Page failed to upload" = "Загрузка страницы не удалась"; -/* A short message explaining that a page was moved to the trash bin. */ -"Page moved to trash." = "Страница перемещена в корзину."; - /* Title of notification displayed when a page has been successfully saved as a draft. */ "Page pending review" = "Страница ожидает одобрения"; @@ -5437,8 +5291,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title of pending Comments filter. */ "Pending" = "Ожидает проверки"; -/* Name for the status of a post pending review. - Title of the Pending Review Badge */ +/* Name for the status of a post pending review. */ "Pending review" = "Ожидает проверки"; /* Noun. Title of the people management feature. @@ -5467,12 +5320,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Photography site intent topic */ "Photography" = "Фотография"; -/* Tab bar title for the Photos tab in Media Picker */ -"Photos" = "Фотографии"; - -/* Subtitle for placeholder in Free Photos. The company name 'Pexels' should always be written as it is. */ -"Photos provided by Pexels" = "Фото от Pexels"; - /* Title for selecting a new username in the site creation flow. */ "Pick username" = "Выберите имя пользователя"; @@ -5565,7 +5412,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Please enter the password for your WordPress.com account to log in with your Apple ID." = "Введите пароль учётной записи WordPress.com, чтобы войти с вашим Apple ID."; /* Instruction text on the two-factor screen. */ -"Please enter the verification code from your authenticator app, or tap the link below to receive a code via SMS." = "Введите проверочный код из приложения аутентификатора, или нажмите на ссылку ниже, чтобы получить код по SMS."; +"Please enter the verification code from your authenticator app." = "Введите код подтверждения из приложения-аутентификатора."; /* Popup message to ask for user credentials (fields shown below). */ "Please enter your credentials" = "Пожалуйста, введите ваши учётные данные"; @@ -5660,15 +5507,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ The post formats available for the post. Should be the same as in core WP. */ "Post Format" = "Формат записи"; -/* Prompts the user that a restored post was moved to the drafts list. */ -"Post Restored to Drafts" = "Запись восстановлена и перемещена в черновики"; - -/* Prompts the user that a restored post was moved to the published list. */ -"Post Restored to Published" = "Запись восстановлена и опубликована"; - -/* Prompts the user that a restored post was moved to the scheduled list. */ -"Post Restored to Scheduled" = "Запись восстановлена и отложена"; - /* Name of the button to open the post settings The title of the Post Settings screen. */ "Post Settings" = "Настройки записи"; @@ -5688,9 +5526,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of notification displayed when a post has failed to upload. */ "Post failed to upload" = "Загрузка записи не удалась"; -/* A short message explaining that a post was moved to the trash bin. */ -"Post moved to trash." = "Запись перемещена в корзину."; - /* Title of notification displayed when a post has been successfully saved as a draft. */ "Post pending review" = "Запись ожидает одобрения"; @@ -5749,9 +5584,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Period Stats 'Posts and Pages' header */ "Posts and Pages" = "Записи и страницы"; -/* Title of the Posts Page Badge */ -"Posts page" = "Страница записей"; - /* Message informing the user that their static homepage for posts was set successfully */ "Posts page successfully updated" = "Страница записей обновлена"; @@ -5764,9 +5596,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* A message explaining the Posts I Like feature in the reader */ "Posts that you like will appear here." = "Записи, которые вам нравятся, будут отображаться здесь."; -/* Subtitle for placeholder in Tenor picker. `The company name 'Tenor' should always be written as it is. */ -"Powered by Tenor" = "Предоставлено Tenor"; - /* Browse premium themes selection title */ "Premium" = "Премиум"; @@ -5785,18 +5614,12 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title for screen to preview a static content. */ "Preview" = "Просмотр"; -/* Action for Media Picker to preview the selected media items. The argument in the string represents the number of elements (as numeric digits) selected */ -"Preview %@" = "Предпросмотр %@"; - /* Title for web preview device switching button */ "Preview Device" = "Устройство предварительного просмотра"; /* Title on display preview error */ "Preview Unavailable" = "Предварительный просмотр недоступен"; -/* Accessibility label for media item preview for user's viewing an item in their media library */ -"Preview media" = "Предварительный просмотр медиафайла"; - /* No comment provided by engineer. */ "Preview page" = "Предварительный просмотр страницы"; @@ -5843,8 +5666,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Privacy notice for California users" = "Уведомление о конфиденциальности для жителей Калифорнии"; /* Name for the status of a post that is marked private. - Privacy setting for posts set to 'Private'. Should be the same as in core WP. - Title of the Private Badge */ + Privacy setting for posts set to 'Private'. Should be the same as in core WP. */ "Private" = "Личное"; /* No comment provided by engineer. */ @@ -5894,12 +5716,10 @@ translators: %s: Select control button label e.g. \"Button width\" */ Label for the publish date button. */ "Publish Date" = "Дата публикации"; -/* A short phrase indicating a post is due to be immedately published. - Label for a button that moves a page to the published folder, publishing with the current date/time. */ +/* A short phrase indicating a post is due to be immedately published. */ "Publish Immediately" = "Опубликовать немедленно"; /* Label for a button that publishes the post - Label for an option that moves a publishes a post immediately Title of button allowing the user to immediately publish the post they are editing. */ "Publish Now" = "Опубликовать"; @@ -5917,8 +5737,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Name for the status of a published post. Period Stats 'Published' header - Title of the published filter. This filter shows a list of posts that the user has published. - Title of the published header in search list. */ + Title of the published filter. This filter shows a list of posts that the user has published. */ "Published" = "Опубликовано"; /* Precedes the name of the blog just posted on */ @@ -6060,8 +5879,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of the completion screen of the Blogging Reminders Settings screen when the reminders are removed. */ "Reminders removed" = "Напоминания удалены"; -/* Add asset to media picker list - Alert button to confirm a plugin to be removed +/* Alert button to confirm a plugin to be removed Button label when removing a blog Label for the Remove Feature Image icon. Tapping will show a confirmation screen for removing the feature image from the post. Remove Action @@ -6214,9 +6032,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of secondary button on alert prompting verify their accounts while attempting to publish */ "Resend" = "Отправить заново"; -/* Title of the reset button */ -"Reset" = "Сбросить"; - /* Accessibility label for the reset activity type button */ "Reset Activity Type filter" = "Сброс фильтра типа активности"; @@ -6271,12 +6086,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ Button title, displayed when media has failed to upload. Allows the user to try the upload again. Button title. Retries uploading a post. If a user taps the button with this label, the action that evinced this error view will be retried. - Label for a button that attempts to re-upload a page that previously failed to upload. - Label for the retry post upload button. Tapping attempts to upload the post again. Opens the media library . Retries the upload of a user's post. Retry updating User's Role - Retry uploading the post. Retry. Action Retry. Verb – retry a failed media upload. The Jetpack view button title used when an error occurred @@ -6288,9 +6100,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Button title that triggers a scan */ "Retry Scan" = "Повторить проверку"; -/* User action to retry media upload. */ -"Retry Upload" = "Повторить загрузку"; - /* User action to retry all failed media uploads. */ "Retry all" = "Попробовать снова для всех"; @@ -6388,9 +6197,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Accessibility label for the 'Save Post' button when a post has been saved. */ "Saved Post" = "Сохраненная запись"; -/* Text displayed in HUD when a media item's metadata (title, etc) is saved successfully. */ -"Saved!" = "Сохранено!"; - /* Accessibility hint for the 'Save Post' button. */ "Saves this post for later." = "Сохраняет эту запись на потом."; @@ -6401,7 +6207,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Saving post…" = "Сохранение записи..."; /* Menus save button title while it is saving a Menu. - Text displayed in HUD while a media item's metadata (title, etc) is being saved. Text displayed in HUD while a post is being saved as a draft. */ "Saving..." = "Сохранение..."; @@ -6492,21 +6297,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* No comment provided by engineer. */ "Search or type URL" = "Введите поисковый запрос или URL"; -/* Text displayed when the search controller will be presented */ -"Search pages" = "Искать страницы"; - -/* Text displayed when the search controller will be presented */ -"Search posts" = "Поиск записей"; - /* No comment provided by engineer. */ "Search settings" = "Настройки поиска"; -/* Title for placeholder in Tenor picker */ -"Search to find GIFs to add to your Media Library!" = "Найти свободные фото и добавить их в вашу медиатеку!"; - -/* Title for placeholder in Free Photos */ -"Search to find free photos to add to your Media Library!" = "Найдите бесплатные фотографии для добавления в медиатеку!"; - /* Menus search bar placeholder text. */ "Search..." = "Поиск..."; @@ -6577,9 +6370,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Register Domain - Domain contact information field placeholder for Country */ "Select Country" = "Выберите страну"; -/* Title of button that allows the user to select more photos to access within the app */ -"Select More" = "Выбрать ещё"; - /* Blog Picker's Title */ "Select Site" = "Выбрать сайт"; @@ -6601,9 +6391,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Register domain - Title for the Choose domain button of Suggested domains screen */ "Select domain" = "Выберите домен"; -/* Accessibility hint for actions when displaying media items. */ -"Select media." = "Выберите медиафайл."; - /* Accessibility label for selecting paragraph style button on formatting toolbar. */ "Select paragraph style" = "Выбрать стиль абзацев"; @@ -6707,19 +6494,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label for connected service in Publicize stat. */ "Service" = "Служба"; -/* Label for a button that opens the Set Parent options view controller - Navigation title displayed on the navigation bar */ +/* Navigation title displayed on the navigation bar */ "Set Parent" = "Назначить родителя"; /* No comment provided by engineer. */ "Set as Featured Image" = "Установить как избранное изображение"; -/* Label for a button that sets the selected page as the site's Homepage */ -"Set as Homepage" = "Установить как главную страницу"; - -/* Label for a button that sets the selected page as the site's Posts page */ -"Set as Posts Page" = "Установить как страницу записей"; - /* Notice confirming that an image has been set as the post's featured image. */ "Set as featured image" = "Использовать как изображение записи"; @@ -6763,7 +6543,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Accessibility label for share buttons in nav bars Button label to share a post Button label to share a web page - Share the post. Shares the comment URL Spoken accessibility label Title for a button that allows the user to share their answer to the prompt. @@ -7149,8 +6928,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Name of setting configured when a site uses a static page as its homepage */ "Static Homepage" = "Статическая главная страница"; -/* Label for post stats option. Tapping displays statistics for a post. - Noun. Abbreviation of Statistics. Name of the Stats feature +/* Noun. Abbreviation of Statistics. Name of the Stats feature Noun. Abbv. of Statistics. Links to a blog's Stats screen. Stats 3D Touch Shortcut Stats window title @@ -7181,9 +6959,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label text that defines a post marked as sticky */ "Sticky" = "Прикреплено"; -/* Accessibility label for a sticky post in the post list. */ -"Sticky." = "Прикреплено"; - /* User action to stop upload. */ "Stop upload" = "Прекратить загрузку"; @@ -7240,7 +7015,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ Theme Support action title */ "Support" = "Поддержка"; -/* Button used to switch site */ +/* Title for back button that leads to the site picker screen. */ "Switch Site" = "Сменить сайт"; /* Switches the Editor to HTML Mode */ @@ -7328,9 +7103,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label explaining why users might want to add tags. */ "Tags help tell readers what a post is about. Separate different tags with commas." = "Метки позволяют представить читателям смысл записи. Разделяйте разные метки запятыми."; -/* Menu option for taking an image or video with the device's camera. */ -"Take Photo or Video" = "Сделать фотоснимок или записать видео"; - /* No comment provided by engineer. */ "Take a Photo" = "Сделайте фото"; @@ -7401,12 +7173,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Accessibility hint */ "Tap to select the previous period" = "Нажмите для выбора предыдущего периода"; -/* Accessibility hint for button used to switch site */ -"Tap to switch to another site, or add a new site" = "Нажмите для переключения на другой сайт или для добавления нового сайта"; - -/* Accessibility hint for media item preview for user's viewing an item in their media library */ -"Tap to view media in full screen" = "Нажмите для просмотра медиафайла на полном экране"; - /* Accessibility hint for a button that opens a new view with more details. */ "Tap to view more details." = "Коснитесь, чтобы увидеть больше сведений."; @@ -7452,10 +7218,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* No comment provided by engineer. */ "Text formatting controls are located within the toolbar positioned above the keyboard while editing a text block" = "Элементы управления форматированием текста расположены на панели инструментов над клавиатурой при редактировании текстового блока"; -/* Button title - The button's title text to send a 2FA code via SMS text message. */ +/* Button title */ "Text me a code instead" = "Пришлите мне сообщение с кодом"; +/* The button's title text to send a 2FA code via SMS text message. */ +"Text me a code via SMS" = "Прислать код по SMS"; + /* Message of alert when theme activation succeeds */ "Thanks for choosing %@ by %@" = "Благодарим вас за выбор темы %1$@ от %2$@!"; @@ -7483,9 +7251,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Error message shown to a user who is trying to share to Facebook but does not have any available Facebook Pages. */ "The Facebook connection cannot find any Pages. Publicize cannot connect to Facebook Profiles, only published Pages." = "Подключение к Facebook не может обнаружить страниц, Publicize не может использовать профили Facebook, только страницы."; -/* Message shown when a GIF failed to load while trying to add it to the Media library. */ -"The GIF could not be added to the Media Library." = "Невозможно добавить GIF-файл в библиотеку медиафайлов."; - /* Description shown when a user logs in with Google but no matching WordPress.com account is found */ "The Google account \"%@\" doesn't match any account on WordPress.com" = "Учетная запись Google \"%@\" не соответствует ни одной учетной записи WordPress.com"; @@ -7613,7 +7378,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Error message shown when user attempts to remove the site owner. */ "The user you are trying to remove is the owner of this site. Please contact support for assistance." = "Пользователь, которого вы пытаетесь удалить, является владельцем сайта. Обратитесь в службу поддержки."; -/* Error message informing a user about an invalid password. */ +/* No comment provided by engineer. */ "The username or password stored in the app may be out of date. Please re-enter your password in the settings and try again." = "Имя пользователя или пароль, сохранённые в приложении, могли устареть. Введите ещё раз ваш пароль в настройках и попробуйте снова."; /* Message shown when a video failed to load while trying to add it to the Media library. */ @@ -7681,9 +7446,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* A short error message letting the user know about a problem displaying a post. */ "There was a problem displaying this post." = "При размещении этой записи произошла ошибка."; -/* Error message displayed when the Media Library is unable to load a full sized preview of an item. */ -"There was a problem loading the media item." = "При загрузке медиафайла произошла ошибка."; - /* The loading view subtitle displayed when an error occurred */ "There was a problem loading your data, refresh your page to try again." = "Возникла проблема с загрузкой ваших данных, обновите страницу, чтобы попробовать еще раз."; @@ -7696,9 +7458,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Explaining to the user there was an error trying to obtain the current location of the user. */ "There was a problem when trying to access your location. Please try again later." = "При доступе к данным о вашем местоположении произошла ошибка. Повторите попытку позже."; -/* Explaining to the user there was an generic error accesing media. */ -"There was a problem when trying to access your media. Please try again later." = "При доступе к медиафайлам произошла ошибка. Повторите попытку позже."; - /* Message for stories unknown error. */ "There was a problem with the Stories editor. If the problem persists you can contact us via the Me > Help & Support screen." = "Возникла проблема с редактором историй. Если проблема сохранится, вы можете обратиться в поддержку через \"Мой профиль - Помощь и поддержка\"."; @@ -7769,9 +7528,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* A description informing the user in order to proceed with this feature we will need camera permissions, and how to enable it. */ "This app needs permission to access the Camera to scan login codes, tap on the Open Settings button to enable it." = "Для сканирования кодов на вход этому приложению нужен доступ к камере. Нажмите кнопку \"Открыть настройки\", чтобы предоставить доступ."; -/* Explaining to the user why the app needs access to the device media library. */ -"This app needs permission to access your device media library in order to add photos and\/or video to your posts. Please change the privacy settings if you wish to allow this." = "Для добавления фотографий и видео к вашим записям этому приложению необходим доступ к библиотеке медиафайлов на вашем устройстве. Если вы хотите разрешить доступ, измените настройки конфиденциальности."; - /* No comment provided by engineer. */ "This color combination may be hard for people to read. Try using a brighter background color and\/or a darker text color." = "Людям будет трудно воспринимать эту комбинацию цветов. Попробуйте использовать более светлый фон и (или) более тёмный текст."; @@ -7881,6 +7637,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Description of a Quick Start Tour */ "Time to finish setting up your site! Our checklist walks you through the next steps." = "Пора завершить настройки сайта! Наш список ведет вас к следующему шагу."; +/* Error when the uses takes more than 1 minute to submit a security key. */ +"Time's up, but don't worry, your security is our priority. Please try again!" = "Время вышло, но не волнуйтесь, ваша безопасность — наш приоритет. Пожалуйста, попробуйте еще раз!"; + /* WordPress.com Marketing Footer Text */ "Tips for getting the most out of WordPress.com." = "Советы по максимально эффективному использованию WordPress.com."; @@ -8004,24 +7763,20 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title for the traffic section in site settings screen */ "Traffic" = "Трафик"; +/* Describes a domain that was transferred from elsewhere to wordpress.com */ +"Transferred Domain" = "Перенесенный домен"; + /* translators: %s: block title e.g: \"Paragraph\". */ "Transform %s to" = "Преобразовать %s в"; /* No comment provided by engineer. */ "Transform block…" = "Преобразовать блок..."; -/* Accessibility label for trash button to delete items from the user's media library - Accessibility label for trash buttons in nav bars +/* Accessibility label for trash buttons in nav bars Trashes a comment Trashes the comment */ "Trash" = "В корзину"; -/* Accessibility hint for trash button to delete items from the user's media library */ -"Trash selected media" = "Удалить выбранный медиафайл в корзину"; - -/* Title of the trash page confirmation alert. */ -"Trash this page?" = "Удалить страницу в корзину?"; - /* Title of the trash confirmation alert. */ "Trash this post?" = "Удалить запись в корзину?"; @@ -8139,9 +7894,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Shown when a user logs in with Google but it subsequently fails to work as login to WordPress.com */ "Unable To Connect" = "Невозможно подключиться"; -/* An error message. */ -"Unable to Connect" = "Невозможно установить соединение"; - /* Title for stories unknown error. */ "Unable to Create Stories Editor" = "Невозможно использовать редактор историй"; @@ -8157,9 +7909,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* An error message shown when there is an issue creating new invite links. */ "Unable to create new invite links." = "Нельзя создать новые пригласительные ссылки."; -/* Text displayed in HUD if there was an error attempting to delete a group of media items. */ -"Unable to delete all media items." = "Невозможно удалить все медиафайлы."; - /* Text displayed in HUD if there was an error attempting to delete a media item. */ "Unable to delete media item." = "Невозможно удалить медиафайл."; @@ -8223,12 +7972,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Message displayed when sharing a link to the downloadable backup fails. */ "Unable to share link" = "Не получается поделиться ссылкой"; -/* Message that appears when a user tries to trash a page while their device is offline. */ -"Unable to trash pages while offline. Please try again later." = "Невозможно удалить страницы корзину при отсутствии подключения к сети. Попробуйте позже."; - -/* Message that appears when a user tries to trash a post while their device is offline. */ -"Unable to trash posts while offline. Please try again later." = "Невозможно удалить записи в корзину при отсутствии подключения к сети. Попробуйте позже."; - /* Notice title when turning site notifications off fails. */ "Unable to turn off site notifications" = "Не удалось выключить уведомления сайта"; @@ -8301,8 +8044,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ Button title. Reverts the previous notification operation Revert an operation Revert enabling notification after successfully subcribing to the comments for the post. - The title of an 'undo' button. Tapping the button moves a trashed page out of the trash folder. - The title of an 'undo' button. Tapping the button moves a trashed post out of the trash folder. Undo action */ "Undo" = "Назад"; @@ -8345,9 +8086,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title for Unknown HTML Editor */ "Unknown HTML" = "Неизвестный HTML"; -/* Label to use when creation date from media asset is not know. */ -"Unknown creation date" = "Неизвестная дата создания"; - /* No comment provided by engineer. */ "Unknown error" = "Неизвестная ошибка"; @@ -8513,6 +8251,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of a row displayed on the debug screen used to configure the sandbox store use in the App. */ "Use Sandbox Store" = "Использовать магазин-песочницу"; +/* The button's title text to use a security key. */ +"Use a security key" = "Использовать ключ безопасности"; + /* Option to enable the block editor for new posts */ "Use block editor" = "Использовать редактор блоков"; @@ -8588,15 +8329,10 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of an alert informing users that the video they are trying to select is not allowed. */ "Video not uploaded" = "Видеофайл не загружен"; -/* Accessibility label for video thumbnails in the media collection view. The parameter is the creation date of the video. */ -"Video, %@" = "Видеофайл, %@"; - /* Period Stats 'Videos' header */ "Videos" = "Видео"; /* Button title. Displays a summary / sharing screen for a specific post. - Label for a button that opens the page when tapped. - Label for the view post button. Tapping displays the post as it appears on the web. Opens the post epilogue screen to allow sharing / viewing of a post. Theme View action title Verb. The screen title shown when viewing a post inside the app. */ @@ -8711,6 +8447,10 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Message shown on screen while waiting for Google to finish its signup process. */ "Waiting for Google to complete…" = "Ожидание Google..."; +/* Text while the webauthn signature is being verified + Text while waiting for a security key challenge */ +"Waiting for security key" = "Ожидание ключа безопасности"; + /* View title during the Google auth process. */ "Waiting..." = "Ожидайте..."; @@ -9085,6 +8825,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* An error message shown when a wpcom user provides the wrong password. */ "Whoops, something went wrong and we couldn't log you in. Please try again!" = "Произошла ошибка: не удалось выполнить вход. Повторите попытку."; +/* Generic error on the 2FA screen */ +"Whoops, something went wrong. Please try again!" = "Что-то пошло не так. Пожалуйста, попробуйте еще раз!"; + +/* Error when the uses chooses an invalid security key on the 2FA screen. */ +"Whoops, that security key does not seem valid. Please try again with another one" = "Ой, кажется, этот ключ безопасности недействителен. Пожалуйста, попробуйте еще раз с другим"; + /* Error message shown when an incorrect two factor code is provided. */ "Whoops, that's not a valid two-factor verification code. Double-check your code and try again!" = "Проверочный код для двухфакторной аутентификации недействителен. Проверьте код и повторите попытку."; @@ -9112,9 +8858,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Siri Suggestion to open Support */ "WordPress Help" = "Справка по WordPress"; -/* Name for the WordPress Media Library */ -"WordPress Media" = "Медиафайлы WordPress"; - /* Accessibility label for selecting an image or video from the user's WordPress media library on formatting toolbar. */ "WordPress Media Library" = "Библиотека медиафайлов WordPress"; @@ -9429,9 +9172,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Share extension error dialog text. */ "Your account does not have permission to upload media to this site. The Site Administrator can change these permissions." = "Ваша учетная запись не имеет прав для загрузки медиафайлов на сайт. Разрешения могут быть изменены администратором сайта."; -/* Explaining to the user why the app needs access to the device media library. */ -"Your app is not authorized to access media library due to active restrictions such as parental controls. Please check your parental control settings in this device." = "Приложение не может получить доступ к медиатеке, возможно из-за ограничений, например родительского контроля. Пожалуйста, проверьте настройки родительского контроля на этом устройстве."; - /* Title for the Jetpack Backup Complete message. */ "Your backup is now available for download" = "Резервная копия доступна для скачивания"; @@ -9450,9 +9190,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of the site address section in the Domains Dashboard. */ "Your free WordPress.com address is" = "Адрес вашего сайта (бесплатный) на WordPress.com -"; -/* Error message when picked media cannot be imported into stories. */ -"Your media could not be exported. If the problem persists you can contact us via the Me > Help & Support screen." = "Медиафайл не может быть экспортирован. Если проблема сохранится, вы можете обратиться в поддержку через \"Мой профиль - Помощь и поддержка\"."; - /* Details about recently acquired domain on domain credit redemption success screen */ "Your new domain %@ is being set up. It may take up to 30 minutes for your domain to start working." = "Ваш новый домен %@ настраивается, это может занять до 30 минут пока он станет доступен."; @@ -9576,8 +9313,38 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* This is the string we display when prompting the user to review the WordPress app */ "appRatings.wordpress.prompt" = "Каково ваше мнение о WordPress?"; -/* Label displayed on audio media items. */ -"audio" = "аудио"; +/* Option to enable the optimization of images when uploading. */ +"appSettings.media.imageOptimizationRow" = "Оптимизировать изображения"; + +/* Indicates an image will use high quality when uploaded. */ +"appSettings.media.imageQuality.high" = "Высокое"; + +/* Indicates an image will use low quality when uploaded. */ +"appSettings.media.imageQuality.low" = "Низкое"; + +/* Indicates an image will use maximum quality when uploaded. */ +"appSettings.media.imageQuality.maximum" = "Максимальное"; + +/* Indicates an image will use medium quality when uploaded. */ +"appSettings.media.imageQuality.medium" = "Среднее"; + +/* The quality of image used when uploading */ +"appSettings.media.imageQuality.title" = "Качество"; + +/* Title for the image quality settings option. */ +"appSettings.media.imageQualityRow" = "Качество изображения"; + +/* Message of an alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.message" = "Оптимизация изображений сжимает изображения для более быстрой загрузки.\n\nЭта опция включена по умолчанию, но вы можете изменить ее в настройках приложения в любой момент."; + +/* Title of an alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.title" = "Продолжать оптимизировать изображения?"; + +/* Title of button for turning off image optimization, displayed in the alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.turnOff" = "Нет, выключить"; + +/* Title of button for leaving on image optimization, displayed in the alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.turnOn" = "Да, продолжить"; /* translators: displays audio file extension. e.g. MP3 audio file */ "audio file" = "аудио файл"; @@ -9691,7 +9458,44 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "blogHeader.actionCopyURL" = "Копировать URL"; /* Context menu button title */ -"blogHeader.actionOpenInBrowser" = "Открыть в браузере"; +"blogHeader.actionVisitSite" = "Перейти на сайт"; + +/* Title for a button that, when tapped, shows more info about participating in Bloganuary. */ +"bloganuary.dashboard.card.button.learnMore" = "Подробнее"; + +/* Short description for the Bloganuary event, shown right below the title. */ +"bloganuary.dashboard.card.description" = "В январе подсказки по ведению блога будут рассылаться через Bloganuary — челлендж сообщества, призванный выработать привычку ежедневно вести блог в новом году."; + +/* Title for the Bloganuary dashboard card while Bloganuary is running. */ +"bloganuary.dashboard.card.runningTitle" = "Bloganuary уже идёт!"; + +/* Title for the Bloganuary dashboard card. */ +"bloganuary.dashboard.card.title" = "Bloganuary уже на пороге!"; + +/* Title of a button that calls the user to enable the Blogging Prompts feature. */ +"bloganuary.learnMore.modal.button.promptsDisabled" = "Включить подсказки по ведению блога"; + +/* Title of a button that will dismiss the Bloganuary modal when tapped. +Note that the word 'go' here should have a closer meaning to 'start' rather than 'move forward'. */ +"bloganuary.learnMore.modal.button.promptsEnabled" = "Поехали!"; + +/* The second line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.description.second" = "Публикуйте свои ответы."; + +/* The third line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.description.third" = "Читайте ответы других блогеров, вдохновляйтесь и заводите новые знакомства."; + +/* The first line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.descriptions.first" = "Ежедневно вы будете получать подсказку с идеей для публикации."; + +/* An additional piece of information shown in case the user has the Blogging Prompts feature disabled. */ +"bloganuary.learnMore.modal.footer.addition" = "Чтобы присоединиться к Bloganuary, необходимо включить подсказки по ведению блога."; + +/* An informative excerpt shown in a subtler tone. */ +"bloganuary.learnMore.modal.footer.text" = "С помощью опции ежедневных подсказок по ведению блога Bloganuary будет рассылать темы январских публикаций."; + +/* The headline text of the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.headline" = "Присоединяйтесь к нашему писательскому челленджу, который продлится месяц"; /* Verb. Dismisses the blogging prompt notification. */ "bloggingPrompt.pushNotification.customActionDescription.dismiss" = "Закрыть"; @@ -9720,6 +9524,12 @@ Example: Comment on Example: Reply to Pamela Nguyen */ "comment.header.subText.reply" = "Ответ пользователю %1$@"; +/* Error message informing a user about an invalid password. */ +"common.reEnterPasswordMessage" = "Имя пользователя или пароль, сохранённые в приложении, могли устареть. Введите ещё раз ваш пароль в настройках и попробуйте снова."; + +/* An error message. */ +"common.unableToConnect" = "Ошибка соединения"; + /* Footnote for the privacy compliance popover. */ "compliance.analytics.popover.footnote" = "Эти файлы cookie позволяют нам оптимизировать работу, собирая информацию о том, как пользователи взаимодействуют с нашими веб-сайтами."; @@ -9870,50 +9680,92 @@ Example: Reply to Pamela Nguyen */ /* Title for a menu action in the context menu on the Jetpack install card. */ "domain.dashboard.card.menu.hide" = "Свернуть"; -/* Domain Purchase Completion footer */ -"domain.purchase.preview.footer" = "Это может занять до 30 минут, после чего ваш особый домен начнёт работать."; +/* Search domain - Title for the Suggested domains screen */ +"domain.management.addDomain.search.title" = "Поиск домена"; -/* Domain Purchase Completion description (only for FREE domains). */ -"domain.purchase.preview.free.description" = "Далее мы поможем вам сделать домен доступным для посетителей."; +/* Domain management buy domain card button title */ +"domain.management.buy.card.button.title" = "Получить домен"; -/* Domain Purchase Completion description (only for PAID domains). */ -"domain.purchase.preview.paid.description" = "Мы отправили вам чек по эл. почте. Далее мы поможем вам подготовить домен к работе."; +/* Domain management buy domain card subtitle */ +"domain.management.buy.card.subtitle" = "Добавить сайт позже."; -/* Reflects that site is live when domain purchase feature flag is ON. */ -"domain.purchase.preview.title" = "Ура, ваш сайт работает!"; +/* Domain management buy domain card title */ +"domain.management.buy.card.title" = "Просто купить домен"; + +/* The expired label of the domain card in All Domains screen. */ +"domain.management.card.expired.label" = "Срок истек"; + +/* The renews label of the domain card in All Domains screen. */ +"domain.management.card.renews.label" = "Продление"; + +/* The empty state button title in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.button.title" = "Найти домен"; + +/* The empty state description in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.description" = "Нажмите ниже, чтобы найти ваш идеальный домен."; + +/* The empty state title in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.title" = "У вас еще нет доменов"; + +/* The empty state description in All Domains screen when an error occurs */ +"domain.management.error.empty.state.description" = "При загрузке ваших доменов произошла ошибка. Пожалуйста, свяжитесь со службой поддержки, если проблема не исчезнет."; + +/* The empty state title in All Domains screen when an error occurs */ +"domain.management.error.empty.state.title" = "Что-то пошло не так"; + +/* The empty state button title in All Domains screen when an error occurs */ +"domain.management.error.state.button.title" = "Попробовать снова"; + +/* The empty state description in All Domains screen when the user is offline */ +"domain.management.offline.empty.state.description" = "Проверьте ваше подключение к сети и попробуйте снова."; + +/* The empty state title in All Domains screen when the user is offline */ +"domain.management.offline.empty.state.title" = "Нет подключения к сети"; -/* Status of a domain in `Action Required` state */ -"domain.status.action.required" = "Требуется действие"; +/* Domain management choose site card button title */ +"domain.management.purchase.footer" = "* Бесплатный домен на один год доступен со всеми тарифными планами с ежегодной оплатой"; -/* Status of a domain in `Active` state */ -"domain.status.active" = "Активен"; +/* Domain management purchase domain screen title */ +"domain.management.purchase.subtitle" = "Не волнуйтесь, вы сможете с лёгкостью добавить сайт позже."; -/* Status of a domain in `Complete Setup` state */ -"domain.status.complete.setup" = "Завершить установку"; +/* Domain management purchase domain screen title. */ +"domain.management.purchase.title" = "Выберите, как вы будете использовать ваш домен"; -/* Status of a domain in `Error` state */ -"domain.status.error" = "Ошибка"; +/* The search bar title in All Domains screen. */ +"domain.management.search-bar.title" = "Поиск доменов"; -/* Status of a domain in `Expired` state */ -"domain.status.expired" = "Просрочен"; +/* The empty state description in All Domains screen when the are no domains matching the search criteria */ +"domain.management.search.empty.state.description" = "Мы не смогли найти ни один домен, соответствующий вашим критериям поиска '%@'"; -/* Status of a domain in `Expiring Soon` state */ -"domain.status.expiring.soon" = "Срок регистрации скоро истекает"; +/* The empty state title in All Domains screen when the are no domains matching the search criteria */ +"domain.management.search.empty.state.title" = "Подходящие домены не найдены"; -/* Status of a domain in `Failed` state */ -"domain.status.failed" = "Сбой"; +/* Domain management choose site card button title */ +"domain.management.site.card.button.title" = "Выберите сайт"; -/* Status of a domain in `In Progress` state */ -"domain.status.in.progress" = "В процессе"; +/* Domain management choose site card subtitle */ +"domain.management.site.card.footer" = "Бесплатный домен на первый год*"; -/* Status of a domain in `Renew` state */ -"domain.status.renew" = "Продлить"; +/* Domain management choose site card subtitle */ +"domain.management.site.card.subtitle" = "Использовать с уже имеющимся сайтом."; -/* Status of a domain in `Verify Email` state */ -"domain.status.verify.email" = "Подтвердить Email"; +/* Domain management choose site card title */ +"domain.management.site.card.title" = "Существующий сайт WordPress.com"; -/* Status of a domain in `Verifying` state */ -"domain.status.verifying" = "Проверка"; +/* Domain Management Screen Title */ +"domain.management.title" = "Все домены"; + +/* Domain Purchase Completion footer */ +"domain.purchase.preview.footer" = "Это может занять до 30 минут, после чего ваш особый домен начнёт работать."; + +/* Domain Purchase Completion description (only for FREE domains). */ +"domain.purchase.preview.free.description" = "Далее мы поможем вам сделать домен доступным для посетителей."; + +/* Domain Purchase Completion description (only for PAID domains). */ +"domain.purchase.preview.paid.description" = "Мы отправили вам чек по эл. почте. Далее мы поможем вам подготовить домен к работе."; + +/* Reflects that site is live when domain purchase feature flag is ON. */ +"domain.purchase.preview.title" = "Ура, ваш сайт работает!"; /* The 'Best Alternative' label under the domain name in 'Choose a domain' screen */ "domain.suggestions.row.best-alternative" = "Лучшая альтернатива"; @@ -9936,12 +9788,24 @@ Example: Reply to Pamela Nguyen */ /* The text to display for paid domains in 'Site Creation > Choose a domain' screen */ "domain.suggestions.row.yearly" = "в год"; +/* Title for the checkout screen. */ +"domains.checkout.title" = "Оформить"; + /* Action shown in a bottom notice to dismiss it. */ "domains.failure.dismiss" = "Закрыть"; /* Content show when the domain selection action fails. */ "domains.failure.title" = "К сожалению, домен, который вы хотите добавить, пока нельзя приобрести в приложении Jetpack."; +/* Title for the screen where the user can choose how to use the domain they're end up purchasing. */ +"domains.purchase.choice.title" = "Купить домен"; + +/* Back button title that navigates back to the search domains screen. */ +"domains.search.backButton.title" = "Поиск"; + +/* Title of screen where user chooses a site to connect to their selected domain */ +"domains.sitePicker.title" = "Выберите сайт"; + /* No comment provided by engineer. */ "double-tap to change unit" = "нажмите дважды для смены единиц"; @@ -9959,6 +9823,15 @@ Example: Reply to Pamela Nguyen */ Site Address placeholder */ "example.com" = "example.com"; +/* Title for confirmation navigation bar button item */ +"externalMediaPicker.add" = "Добавить"; + +/* Bottom toolbar title in the selection mode */ +"externalMediaPicker.toolbarSelectItemsPrompt" = "Выбрать изображения"; + +/* Bottom toolbar title in the selection mode */ +"externalMediaPicker.toolbarViewSelected" = "Показать выбранное (%@)"; + /* Title of screen the displays the details of an advertisement campaign. */ "feature.blaze.campaignDetails.title" = "Детали кампании"; @@ -10058,9 +9931,6 @@ Example: Reply to Pamela Nguyen */ /* (placeholder) Help the user enter a URL into the field */ "http:\/\/my-site-address (URL)" = "http:\/\/мой-адрес-сайта (URL)"; -/* Label displayed on image media items. */ -"image" = "Изображение"; - /* Sentence to justify why the app is asking permission from the user to use their camera. */ "infoplist.NSCameraUsageDescription" = "Для создания фотографий и видео, которые будут размещаться в ваших записях."; @@ -10361,6 +10231,9 @@ Example: Reply to Pamela Nguyen */ /* Indicating that referrer was marked as spam */ "marked as spam" = "отмечено как спам"; +/* Products header text in Me Screen. */ +"me.products.header" = "Товары"; + /* Title of error prompt shown when a sync fails. */ "media.syncFailed" = "Невозможно синхронизировать медиа"; @@ -10373,18 +10246,24 @@ Example: Reply to Pamela Nguyen */ /* Message of an alert informing users that the video they are trying to select is not allowed. */ "mediaExporter.videoLimitExceededError" = "Загрузка видео длительностью более 5 минут требует перехода на платный тариф."; -/* Verb. User action to dismiss error alert when failing to load media item. */ -"mediaItemTable.errorAlert.dismissButton" = "Закрыть"; - /* Accessibility hint for add button to add items to the user's media library */ "mediaLibrary.addButtonAccessibilityHint" = "Добавить медиафайл"; /* Accessibility label for add button to add items to the user's media library */ "mediaLibrary.addButtonAccessibilityLabel" = "Добавить"; +/* Button name in the more menu */ +"mediaLibrary.aspectRatioGrid" = "Сетка с соотношением сторон"; + +/* Context menu button */ +"mediaLibrary.buttonDelete" = "Удалить"; + /* Media screen navigation bar button Select title */ "mediaLibrary.buttonSelect" = "Выбрать"; +/* Context menu button */ +"mediaLibrary.buttonShare" = "Поделиться"; + /* Verb. Button title. Tapping cancels an action. */ "mediaLibrary.deleteConfirmationCancel" = "Отменить"; @@ -10406,6 +10285,21 @@ Example: Reply to Pamela Nguyen */ /* Text displayed in HUD after successfully deleting a media item */ "mediaLibrary.deletionSuccessMessage" = "Удалено!"; +/* The name of the media filter */ +"mediaLibrary.filterAll" = "Все"; + +/* The name of the media filter */ +"mediaLibrary.filterAudio" = "Аудио"; + +/* The name of the media filter */ +"mediaLibrary.filterDocuments" = "Документы"; + +/* The name of the media filter */ +"mediaLibrary.filterImages" = "Изображения"; + +/* The name of the media filter */ +"mediaLibrary.filterVideos" = "Видео"; + /* User action to delete un-uploaded media. */ "mediaLibrary.retryOptionsAlert.delete" = "Удалить"; @@ -10418,6 +10312,12 @@ Example: Reply to Pamela Nguyen */ /* Message displayed when no results are returned from a media library search. Should match Calypso. */ "mediaLibrary.searchResultsEmptyTitle" = "Нет медиафайлов по критериям поиска"; +/* Text displayed in HUD if there was an error attempting to share a group of media items. */ +"mediaLibrary.sharingFailureMessage" = "Невозможно поделиться выбранными элементами."; + +/* Button name in the more menu */ +"mediaLibrary.squareGrid" = "Квадратная сетка"; + /* Media screen navigation title */ "mediaLibrary.title" = "Медиа"; @@ -10439,6 +10339,12 @@ Example: Reply to Pamela Nguyen */ /* The title of the button to dismiss the alert shown when the picked media cannot be imported into stories. */ "mediaPicker.failedMediaExportAlert.dismissButton" = "Закрыть"; +/* Error message when picked media cannot be imported into stories. */ +"mediaPicker.failedMediaExportAlert.message" = "Медиафайл не может быть экспортирован. Если проблема сохранится, вы можете обратиться в поддержку через \"Мой профиль - Помощь и поддержка\"."; + +/* Error title when picked media cannot be imported into stories. */ +"mediaPicker.failedMediaExportAlert.title" = "Экспорт медиа неудачен"; + /* Message for alert when access to camera is not granted */ "mediaPicker.noCameraAccessMessage" = "Этому приложению необходим доступ к камере для получения новых медиаданных. Если вы хотите разрешить доступ, измените настройки конфиденциальности."; @@ -10472,6 +10378,12 @@ Example: Reply to Pamela Nguyen */ /* The name of the action in the context menu */ "mediaPicker.takeVideo" = "Записать видео"; +/* Navigation title for media preview. Example: 1 of 3 */ +"mediaPreview.NofM" = "%1$@ из %2$@"; + +/* Max image size in pixels (e.g. 300x300px) */ +"mediaSizeSlider.valueFormat" = "%1$d × %2$d пикс."; + /* The description in the Delete WordPress screen */ "migration.deleteWordpress.description" = "Кажется, у вас всё ещё установлено приложение WordPress."; @@ -10484,9 +10396,6 @@ Example: Reply to Pamela Nguyen */ /* The title in the Delete WordPress screen */ "migration.deleteWordpress.title" = "Вам больше не требуется приложение WordPress"; -/* Primary button title in the migration done screen. */ -"migration.done.actions.primary.title" = "Завершить"; - /* Footer for the migration done screen. */ "migration.done.footer" = "Рекомендуем удалить приложение WordPress с устройства, чтобы избежать конфликтов данных."; @@ -10496,6 +10405,9 @@ Example: Reply to Pamela Nguyen */ /* Primary description in the migration done screen. */ "migration.done.primaryDescription" = "Ваши данные и настройки были перенесены. Всё представлено в том же виде, как вы это оставили."; +/* Secondary description (second paragraph) in the migration done screen. */ +"migration.done.secondaryDescription" = "Пришло время продолжить путешествие по WordPress в приложении Jetpack!"; + /* Title of the migration done screen. */ "migration.done.title" = "Спасибо за выбор Jetpack!"; @@ -10544,6 +10456,9 @@ Example: Reply to Pamela Nguyen */ /* The title in the migration welcome screen */ "migration.welcome.title" = "Jetpack приветствует вас!"; +/* Primary button title in the migration done screen. */ +"migrationDone.actions.primaryTitle" = "Поехали!"; + /* Description for the static screen displayed prompting users to switch the Jetpack app. */ "movedToJetpack.description" = "Приложение Jetpack обладает всеми возможностями приложения WordPress, а теперь и исключительным доступом к статистике, «Чтиву», уведомлениям и другим функциям."; @@ -10619,6 +10534,30 @@ Example: Reply to Pamela Nguyen */ /* Message title for when a user has no sites. */ "mySite.noSites.title" = "У вас нет сайтов"; +/* Menu title for the add site option */ +"mySite.siteActions.addSite" = "Добавить сайт"; + +/* Button that reveals more site actions */ +"mySite.siteActions.button" = "Действия с сайтом"; + +/* Accessibility hint for button used to show more site actions */ +"mySite.siteActions.hint" = "Нажмите, чтобы показать больше действий с сайтом"; + +/* Menu title for the personalize home option */ +"mySite.siteActions.personalizeHome" = "Настроить страницу «Главная»"; + +/* Menu title for the change site icon option */ +"mySite.siteActions.siteIcon" = "Сменить значок сайта"; + +/* Menu title for the change site title option */ +"mySite.siteActions.siteTitle" = "Изменить название сайта"; + +/* Menu title for the switch site option */ +"mySite.siteActions.switchSite" = "Переключиться на другой сайт"; + +/* Menu title for the visit site option */ +"mySite.siteActions.visitSite" = "Перейти на сайт"; + /* Dismiss button title. */ "noResultsViewController.dismissButton" = "Закрыть"; @@ -10634,14 +10573,20 @@ Example: Reply to Pamela Nguyen */ /* This is one of the buttons we display when prompting the user for a review */ "notifications.appRatings.sendFeedback.yes.buttonTitle" = "Отправить отзыв"; -/* Word separating the current index from the total amount. I.e.: 7 of 9 */ -"of" = "из"; +/* Badge for page cells */ +"pageList.badgeHomepage" = "Домашняя страница"; + +/* Badge for page cells */ +"pageList.badgeLocalChanges" = "Локальные изменения"; -/* Label displayed on media items that are not video, image, or audio. */ -"other" = "другое"; +/* Badge for page cells */ +"pageList.badgePendingReview" = "Ожидает одобрения"; -/* Promote the page with Blaze. */ -"pages.blaze.actionTitle" = "Продвигайте содержимое с помощью Blaze"; +/* Badge for page cells */ +"pageList.badgePosts" = "Страница записей"; + +/* Badge for page cells */ +"pageList.badgePrivate" = "Личное"; /* Subtitle of the theme template homepage cell */ "pages.template.subtitle" = "На главной странице используется шаблон \"Тема\", и она откроется в веб-редакторе."; @@ -10649,6 +10594,36 @@ Example: Reply to Pamela Nguyen */ /* Title of the theme template homepage cell */ "pages.template.title" = "Главная страница"; +/* Message informing the user that their static homepage page was set successfully */ +"pages.updatePage.successTitle" = "Страница обновлена"; + +/* Delete option in the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.actionTitle" = "Удалить навсегда"; + +/* Message of the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.alertMessage" = "Вы уверены, что хотите удалить эту страницу навсегда?"; + +/* Title of the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.alertTitle" = "Удалить навсегда?"; + +/* Menu option for filtering posts by everyone */ +"pagesList.pagesByEveryone" = "Страницы от всех"; + +/* Menu option for filtering posts by me */ +"pagesList.pagesByMe" = "Мои страницы"; + +/* Trash option in the trash page confirmation alert. */ +"pagesList.trash.actionTitle" = "В корзину"; + +/* Message of the trash page confirmation alert. */ +"pagesList.trash.alertMessage" = "Вы уверены, что хотите переместить страницу в корзину?"; + +/* Title of the trash page confirmation alert. */ +"pagesList.trash.alertTitle" = "Удалить страницу в корзину?"; + +/* Cancels an Action */ +"pagesList.trash.cancel" = "Отмена"; + /* No comment provided by engineer. */ "password" = "пароль"; @@ -10688,6 +10663,51 @@ Example: Reply to Pamela Nguyen */ /* Register Domain - Domain contact information field Phone */ "phone number" = "номер телефона"; +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.createdTimeAgo" = "Создана %@"; + +/* Status mesasge for post cells */ +"post.deletingPostPermanentlyStatusMessage" = "Удаление записи..."; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.editedTimeAgo" = "Изменено %@"; + +/* Status mesasge for post cells */ +"post.movingToTrashStatusMessage" = "Удаление записи в корзину..."; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.publishedTimeAgo" = "Опубликовано %@"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.scheduledForDate" = "Запланировано %@"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.trashedTimeAgo" = "В корзине %@"; + +/* Accessibility label for the post author in the post list. The parameter is the author name. For example, \"By Elsa.\" */ +"postList.a11y.authorChunkFormat" = "Автор: %@."; + +/* Accessibility label for a post's excerpt in the post list. The parameter is the post excerpt. For example, \"Excerpt. This is the first paragraph.\" */ +"postList.a11y.exerptChunkFormat" = "Отрывок. %@."; + +/* Accessibility label for a sticky post in the post list. */ +"postList.a11y.sticky" = "Закреплена"; + +/* Accessibility label for a post in the post list. The first placeholder is the post title. The second placeholder is the date. */ +"postList.a11y.titleAndDateChunkFormat" = "%1$@, %2$@."; + +/* Title for the 'Trash' post list row swipe action */ +"postList.swipeActionDelete" = "В корзину"; + +/* Title for the 'Delete' post list row swipe action */ +"postList.swipeActionDeletePermanently" = "Удалить"; + +/* Title for the 'Share' post list row swipe action */ +"postList.swipeActionShare" = "Поделиться"; + +/* Title for the 'View' post list row swipe action */ +"postList.swipeActionView" = "Просмотр"; + /* User action to dismiss featured media options. */ "postSettings.featuredImageUploadActionSheet.dismiss" = "Закрыть"; @@ -10706,9 +10726,84 @@ Example: Reply to Pamela Nguyen */ /* Button in Post Settings */ "postSettings.setFeaturedImageButton" = "Установить изображение записи"; +/* Error message on post/page settings screen */ +"postSettings.updateFailedMessage" = "Не удалось обновить настройки записи"; + /* Promote the post with Blaze. */ "posts.blaze.actionTitle" = "Продвигайте содержимое с помощью Blaze"; +/* Label for the Post List option that cancels automatic uploading of a post. */ +"posts.cancelUpload.actionTitle" = "Отменить загрузку"; + +/* Label for post comments option. Tapping displays comments for a post. */ +"posts.comments.actionTitle" = "Комментарии"; + +/* Label for the delete post option. Tapping permanently deletes a post. */ +"posts.delete.actionTitle" = "Удалить навсегда"; + +/* Label for an option that moves a post to the draft folder */ +"posts.draft.actionTitle" = "Переместить в черновик"; + +/* Label for post duplicate option. Tapping creates a copy of the post. */ +"posts.duplicate.actionTitle" = "Дублировать"; + +/* Opens a submenu for page attributes. */ +"posts.pageAttributes.actionTitle" = "Атрибуты страницы"; + +/* Label for the preview post button. Tapping displays the post as it appears on the web. */ +"posts.preview.actionTitle" = "Предварительный просмотр"; + +/* Label for an option that moves a publishes a post immediately */ +"posts.publish.actionTitle" = "Опубликовать сейчас"; + +/* Retry uploading the post. */ +"posts.retry.actionTitle" = "Повторить попытку"; + +/* Set the selected page as the homepage. */ +"posts.setHomepage.actionTitle" = "Установить как главную страницу"; + +/* Set the parent page for the selected page. */ +"posts.setParent.actionTitle" = "Назначить родителя"; + +/* Set the selected page as a posts page. */ +"posts.setPostsPage.actionTitle" = "Установить как страницу записей"; + +/* Set the selected page as a regular page. */ +"posts.setRegularPage.actionTitle" = "Установить как обычную страницу"; + +/* Label for post settings option. Tapping displays settings for a post. */ +"posts.settings.actionTitle" = "Настройки"; + +/* Share the post. */ +"posts.share.actionTitle" = "Поделиться"; + +/* Label for post stats option. Tapping displays statistics for a post. */ +"posts.stats.actionTitle" = "Статистика"; + +/* Label for a option that moves a post to the trash folder */ +"posts.trash.actionTitle" = "Удалить в корзину"; + +/* Label for the view post button. Tapping displays the post as it appears on the web. */ +"posts.view.actionTitle" = "Просмотр"; + +/* A short message explaining that a page was deleted permanently. */ +"postsList.deletePage.message" = "Страница удалена навсегда"; + +/* A short message explaining that a post was deleted permanently. */ +"postsList.deletePost.message" = "Запись удалена навсегда"; + +/* A short message explaining that a page was moved to the trash bin. */ +"postsList.movePageToTrash.message" = "Страница перемещена в корзину"; + +/* A short message explaining that a post was moved to the trash bin. */ +"postsList.movePostToTrash.message" = "Запись перемещена в корзину"; + +/* Menu option for filtering posts by everyone */ +"postsList.postsByEveryone" = "Записи от всех"; + +/* Menu option for filtering posts by me */ +"postsList.postsByMe" = "Мои записи"; + /* Title for the button to subscribe to Jetpack Social on the remaining shares view */ "postsettings.social.remainingshares.subscribe" = "Подпишитесь, чтобы делиться больше"; @@ -10853,13 +10948,15 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Text for the 'Like' button on the reader post card cell. */ "reader.post.button.like" = "Нравится"; -/* Accessibility hint for the like button on the reader post card cell - Accessibility hint for the liked button on the reader post card cell */ +/* Accessibility hint for the like button on the reader post card cell */ "reader.post.button.like.accessibility.hint" = "Отмечает запись как понравившуюся."; /* Text for the 'Liked' button on the reader post card cell. */ "reader.post.button.liked" = "Понравилось"; +/* Accessibility hint for the liked button on the reader post card cell */ +"reader.post.button.liked.accessibility.hint" = "Снимает отметку \"понравилось\" с записи."; + /* Accessibility hint for the site header on the reader post card cell */ "reader.post.button.menu.accessibility.hint" = "Открывает меню с другими действиями."; @@ -10923,6 +11020,12 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Title for the tooltip anchor. */ "readerDetail.tooltipAnchorTitle.accessibilityLabel" = "Создано"; +/* The button title for the transfer footer view in Register Domain screen */ +"register.domain.transfer.button.title" = "Перенос домена"; + +/* The title for the transfer footer view in Register Domain screen */ +"register.domain.transfer.title" = "Хотите перенести уже принадлежащий вам домен?"; + /* Information of what related post are and how they are presented */ "relatedPostsSettings.optionsFooter" = "Похожие записи показывают связанное содержимое вашего сайта под содержимым записей."; @@ -11022,6 +11125,21 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Accessibility hint for actions when displaying media items. */ "siteMedia.cellAccessibilityHint" = "Выберите медиафайл."; +/* Accessibility hint for media item preview for user's viewing an item in their media library */ +"siteMediaItem.contentViewAccessibilityHint" = "Нажмите для просмотра медиафайла на полном экране"; + +/* Accessibility label for media item preview for user's viewing an item in their media library */ +"siteMediaItem.contentViewAccessibilityLabel" = "Предварительный просмотр медиафайла"; + +/* Title for confirmation navigation bar button item */ +"siteMediaPicker.add" = "Добавить"; + +/* Button selection media in media picker */ +"siteMediaPicker.deselect" = "Отменить выбор"; + +/* Button selection media in media picker */ +"siteMediaPicker.select" = "Выбрать"; + /* Media screen navigation title */ "siteMediaPicker.title" = "Медиа"; @@ -11029,7 +11147,7 @@ Example: given a notice format "Following %@" and empty site name, this will be "siteSettings.privacy.title" = "Конфиденциальность"; /* Hint for users when hidden privacy setting is set */ -"siteVisibility.hidden.hint" = "Ваш сайт доступен всем посетителям, но для него настроен запрет на индексацию поисковыми системами."; +"siteVisibility.hidden.hint" = "Ваш сайт скрыт от посетителей табличкой \"Скоро запуск\" до готовности."; /* Text for privacy settings: Hidden */ "siteVisibility.hidden.title" = "Скрытый"; @@ -11190,6 +11308,12 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Dismiss the AlertView */ "stockPhotos.strings.dismiss" = "Закрыть"; +/* Subtitle for placeholder in Free Photos. The company name 'Pexels' should always be written as it is. */ +"stockPhotos.subtitle" = "Фотографии предоставлены Pexels"; + +/* Title for placeholder in Free Photos */ +"stockPhotos.title" = "Найдите бесплатные фотографии и добавьте их в вашу Библиотеку файлов!"; + /* Section title for prominent suggestions */ "suggestions.section.prominent" = "В этом обсуждении"; @@ -11337,6 +11461,9 @@ Example: given a notice format "Following %@" and empty site name, this will be /* View title for Help & Support page. */ "support.title" = "Справка"; +/* Title for placeholder in Tenor picker */ +"tenor.welcomeMessage" = "Найдите GIF-изображения и добавьте их в вашу Библиотеку файлов!"; + /* Header of delete screen section listing things that will be deleted. */ "these items will be deleted:" = "эти элементы будут удалены:"; @@ -11352,9 +11479,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Displayed in the confirmation alert when marking unread notifications as read. */ "unread" = "непрочитано"; -/* Label displayed on video media items. */ -"video" = "видео"; - /* Portion of a message for Jetpack users that have multisite WP installation, thus Restore is not available. This part is a link, colored with a different color. */ "visit our documentation page" = "посетите нашу страницу документации"; diff --git a/WordPress/Resources/sk.lproj/Localizable.strings b/WordPress/Resources/sk.lproj/Localizable.strings index cd4cedb6b456..50bbd623fc27 100644 --- a/WordPress/Resources/sk.lproj/Localizable.strings +++ b/WordPress/Resources/sk.lproj/Localizable.strings @@ -81,9 +81,6 @@ /* Age between dates over one year. */ "%d years" = "Počet rokov: %d"; -/* Max image size in pixels (e.g. 300x300px) */ -"%dx%dpx" = "%1$dx%2$dpx"; - /* One menu area available in the theme */ "%i menu area in this theme" = "%i oblasť pre menu v téme"; @@ -204,13 +201,6 @@ /* No comment provided by engineer. */ "Activity Logs" = "Záznamy o činnosti"; -/* Accessibility label for add button to add items to the user's media library - Remove asset from media picker list */ -"Add" = "Pridať"; - -/* Action for Media Picker to indicate selection of media. The argument in the string represents the number of elements (as numeric digits) selected */ -"Add %@" = "Pridať %@"; - /* Alert option to add document contents into a blog post. */ "Add Contents to Post" = "Pridať obsah do príspevku"; @@ -254,9 +244,6 @@ /* Title for the advanced section in site settings screen */ "Advanced" = "Pokročilé"; -/* Description of albums in the photo libraries */ -"Albums" = "Albumy"; - /* Image alignment option title. Title of the screen for choosing an image's alignment. */ "Alignment" = "Zarovnanie"; @@ -400,9 +387,6 @@ /* Message prompting the user to confirm that they want to disconnect Jetpack from the site. */ "Are you sure you want to disconnect Jetpack from the site?" = "Určite chcete odpojiť Jetpack z webovej stránky?"; -/* Message prompting the user to confirm that they want to permanently delete a group of media items. */ -"Are you sure you want to permanently delete these items?" = "Naozaj chcete tieto položky natrvalo odstrániť?"; - /* Message prompting the user to confirm that they want to permanently delete a media item. Should match Calypso. */ "Are you sure you want to permanently delete this item?" = "Naozaj chcete túto položku natrvalo odstrániť?"; @@ -446,9 +430,6 @@ /* Alert option to embed a doc link into a blog post. */ "Attach File as Link" = "Pridať súbor ako odkaz"; -/* Accessibility label for audio items in the media collection view. The parameter is the creation date of the audio. */ -"Audio, %@" = "Audio, %@"; - /* No comment provided by engineer. */ "Authenticating" = "Overuje sa"; @@ -562,8 +543,7 @@ /* Label for size of media while it's being calculated. */ "Calculating..." = "Prepočítava sa..."; -/* Accessibility label for taking an image or video with the camera on formatting toolbar. - Accessibility label for the camera tile in the collection view */ +/* Accessibility label for taking an image or video with the camera on formatting toolbar. */ "Camera" = "Fotoaparát"; /* Title of an alert letting the user know */ @@ -605,10 +585,7 @@ Cancels an Action Cancels an alert. Cancels the mark all as read action. - Dismiss the post action sheet Editing GIF alert cancel action button. - Label for a cancel button - Label for the auto-upload cancelation button in the post list. Tapping will prevent the app from auto-uploading the post. Menus cancel button for deleting a menu. Menus cancel button within text bar while editing items. Menus: Cancel button title for canceling an edited menu item. @@ -625,10 +602,6 @@ Verb. Title for Jetpack Restore cancel button. */ "Cancel" = "Zrušiť"; -/* Label for the Post List option that cancels automatic uploading of a post. - Media Library option to cancel an in-progress or failed upload. */ -"Cancel Upload" = "Zrušiť nahrávanie"; - /* Menus alert button title to cancel discarding changes and not createa a new menu. Menus alert button title to cancel discarding changes and not select a new menu Menus alert button title to cancel discarding changes and not select a new menu location */ @@ -707,9 +680,6 @@ Title of a Quick Start Tour */ "Choose a theme" = "Vyberte tému"; -/* Menu option for selecting media from the device's photo library. */ -"Choose from My Device" = "Vybrať z Môjho zariadenia"; - /* Label for button that clears all media cache. */ "Clear Device Media Cache" = "Vymazanie vyrovnávacej pamäte zariadenia"; @@ -905,10 +875,6 @@ /* Title of button that displays the WordPress.org contributor page */ "Contribute" = "Prispieť"; -/* Copy the post url and paste anywhere in phone - Label for page copy link. Tapping copy the url of page */ -"Copy Link" = "Kopírovať odkaz"; - /* Copy link to oomment button title */ "Copy Link to Comment" = "Skopírovať odkaz do komentáru"; @@ -969,9 +935,6 @@ /* The title for an alert that says to the user that the featured image he selected couldn't be uploaded. */ "Couldn't upload the featured image" = "Nepodarilo sa nahrať odporúčaný obrázok"; -/* Message to show while media data source is finding the number of items available. */ -"Counting media items..." = "Počítam množstvo súborov..."; - /* Period Stats 'Countries' header */ "Countries" = "Krajiny"; @@ -1071,7 +1034,6 @@ /* Delete Delete button title for the warning shown to the user when he refuses to re-login when the authToken is missing. Title for button that permanently deletes a media item (photo / video) - Title for button that permanently deletes one or more media items (photos / videos) Title of a delete button Title of the trash confirmation alert. */ "Delete" = "Odstrániť"; @@ -1079,15 +1041,11 @@ /* Menus confirmation button for deleting a menu. */ "Delete Menu" = "Zmazať menu"; -/* Delete option in the confirmation alert when deleting a page from the trash. - Delete option in the confirmation alert when deleting a post from the trash. - Label for a button permanently deletes a page. - Label for the delete post option. Tapping permanently deletes a post. +/* Delete option in the confirmation alert when deleting a post from the trash. Title for button on the comment details page that deletes the comment when tapped. */ "Delete Permanently" = "Zmazať natrvalo"; -/* Title of the confirmation alert when deleting a page from the trash. - Title of the confirmation alert when deleting a post from the trash. */ +/* Title of the confirmation alert when deleting a post from the trash. */ "Delete Permanently?" = "Vymazať na trvalo?"; /* Button label for deleting the current site @@ -1166,7 +1124,6 @@ /* Accessibility label for button to dismiss a bottom sheet Accessibility label for the transparent space above the login dialog which acts as a button to dismiss the dialog. Accessibility label for the transparent space above the signup dialog which acts as a button to dismiss the dialog. - Action to show on alert when view asset fails. Dismiss a view. Verb */ "Dismiss" = "Zrušiť"; @@ -1175,9 +1132,6 @@ User's Display Name */ "Display Name" = "Zobraziť meno"; -/* Accessibility label for other media items in the media collection view. The parameter is the filename file. */ -"Document: %@" = "Dokument: %@"; - /* Message shown when all tours have been completed */ "Doesn't it feel good to cross things off a list?" = "Nie je to dobrý pocit odškrtávať veci zo zoznamu?"; @@ -1211,17 +1165,12 @@ /* Name for the status of a draft post. */ "Draft" = "Koncept"; -/* Title of the drafts filter. This filter shows a list of draft posts. - Title of the drafts header in search list. */ +/* Title of the drafts filter. This filter shows a list of draft posts. */ "Drafts" = "Koncepty"; -/* Accessibility label for edit button to enable multi selection mode in the user's media library - Edit the post. - Editing GIF alert default action button. +/* Editing GIF alert default action button. Edits a Comment Edits the comment - Label for a button that opens the Edit Page view controller - Label for the edit post button. Tapping displays the editor. User action to edit media details. Verb, edit a comment */ "Edit" = "Upraviť"; @@ -1410,9 +1359,6 @@ /* Title for the activity detail view */ "Event" = "Udalosť"; -/* Label for the post author filter. This filter shows posts for all users on the blog. */ -"Everyone" = "Každý"; - /* Label for the excerpt field. Should be the same as WP core. */ "Excerpt" = "Zhrnutie"; @@ -1672,8 +1618,7 @@ "Hold for Moderation" = "Podržať pre moderovanie"; /* Title for setting which shows the current page assigned as a site's homepage - Title for the homepage section in site settings screen - Title of the Homepage Badge */ + Title for the homepage section in site settings screen */ "Homepage" = "Domovská stránka"; /* Label for Homepage Settings site settings section @@ -1719,9 +1664,6 @@ /* Hint for image title on image settings. */ "Image title" = "Nadpis obrázka"; -/* Accessibility label for image thumbnails in the media collection view. The parameter is the creation date of the image. */ -"Image, %@" = "Obrázok, %@"; - /* Undated post time label */ "Immediately" = "Okamžite"; @@ -1963,15 +1905,9 @@ /* Text displayed while loading the activity feed for a site */ "Loading Activities..." = "Načítavanie Aktivít..."; -/* Phrase to show when the user has searched for GIFs and they are being loaded. */ -"Loading GIFs..." = "Nahrávanie GIFov ... "; - /* Menus label text displayed while menus are loading */ "Loading Menus..." = "Načítanie menu..."; -/* Phrase to show when the user has searched for images and they are being loaded. */ -"Loading Photos..." = "Nahrávanie fotografií ..."; - /* Text displayed while loading plans details */ "Loading Plan..." = "Načítava sa paušál..."; @@ -2014,8 +1950,7 @@ /* Status for Media object that is only exists locally. */ "Local" = "Miestne"; -/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. - Title of the Local Changes Badge */ +/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. */ "Local changes" = "Miestne zmeny"; /* Title for alert when a generic error happened when trying to find the location of the device */ @@ -2112,7 +2047,6 @@ "Max Video Upload Size" = "Maximálna veľkosť nahraného videa"; /* Accessibility label for the Me button in My Site. - Label for the post author filter. This filter shows posts only authored by the current user. Me page title Noun. Title. Links to the Me screen. The accessibility value of the me tab. @@ -2120,9 +2054,7 @@ "Me" = "Ja"; /* Noun. Title. Links to the blog's Media library. - Tab bar title for the Media tab in Media Picker The menu item to select during a guided tour. - Title for Media Library section of the app. Title for the media section in site settings screen Title label for the media settings section in the app settings */ "Media" = "Súbory"; @@ -2130,13 +2062,6 @@ /* Label for size of media cache in the app. */ "Media Cache Size" = "Veľkosť cache súborov"; -/* Title for alert when access to media capture is not granted */ -"Media Capture" = "Zachytiť súbor"; - -/* Title for alert when a generic error happened when loading media - Title for alert when access to the media library is not granted by the user */ -"Media Library" = "Knižnica súborov"; - /* Title for action sheet with media options. */ "Media Options" = "Nastavenia súborov"; @@ -2153,9 +2078,6 @@ /* Error message to show to users when trying to upload a media object with file size is larger than the max file size allowed in the site */ "Media filesize (%@) is too large to upload. Maximum allowed is %@" = "Veľkosť súboru (%1$@) je príliš veľká. Maximálna povolená veľkosť je %2$@"; -/* Alert title when there is issues loading an asset to preview. */ -"Media preview failed." = "Náhľad súboru zlyhal."; - /* Alert displayed to the user when multiple media items have uploaded successfully. */ "Media uploaded (%ld files)" = "Nahrávajú sa média (%ld súborov)"; @@ -2181,9 +2103,6 @@ Label for the share message field on the post settings. */ "Message" = "Správa"; -/* Title of section containing image / video metadata such as size and file type */ -"Metadata" = "Metadáta"; - /* Jetpack Monitor Settings: Monitor site's uptime */ "Monitor your site's uptime" = "Sledujte dobu prevádzky vašej webovej stránky"; @@ -2194,13 +2113,11 @@ "Months and Years" = "Mesiace a roky"; /* Accessibility label for more button in dashboard quick start card. - Accessibility label for the More button in Page List. Accessibility label for the More button in Post List (compact view). Accessibility label for the More button on formatting toolbar. Accessibility label for the More button on Reader Cell Accessibility label for the More button on Reader's post details - Action button to display more available options - Label for the more post button. Tapping displays an action sheet with post options. */ + Action button to display more available options */ "More" = "Viac"; /* Action button to display more available options @@ -2210,15 +2127,8 @@ /* Label for the posting activity legend. */ "More Posts" = "Viac článkov"; -/* Label for a button that moves a page to the draft folder - Label for an option that moves a post to the draft folder */ -"Move to Draft" = "Prejsť na koncept"; - -/* Label for a button that moves a page to the trash folder - Label for a option that moves a post to the trash folder - Title for button on the comment details page that moves the comment to trash when tapped. +/* Title for button on the comment details page that moves the comment to trash when tapped. Trash option in the trash confirmation alert. - Trash option in the trash page confirmation alert. Trashes the comment */ "Move to Trash" = "Presunúť do koša"; @@ -2234,7 +2144,8 @@ Title of My Site tab */ "My Site" = "Moja webová stránka"; -/* Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ +/* Title for site picker screen. + Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ "My Sites" = "Moje stránky"; /* Siri Suggestion to open My Sites */ @@ -2358,9 +2269,7 @@ /* Displayed in the Notifications Tab as a title, when the Likes Filter shows no notifications */ "No likes yet" = "Zatiaľ žiadne lajky"; -/* Message displayed when no results are returned from a media library search. Should match Calypso. - Phrase to show when the user search for images but there are no result to show. - Phrase to show when the user searches for GIFs but there are no result to show. */ +/* Message displayed when no results are returned from a media library search. Should match Calypso. */ "No media matching your search" = "Žiadne média nezodpovedajú vyhľadávaniu"; /* Menus text shown when no menus were available for loading the Menus editor. */ @@ -2369,8 +2278,7 @@ /* Displayed in the Notifications Tab as a title, when there are no notifications */ "No notifications yet" = "Zatiaľ žiadne upozornenia"; -/* Displayed when the user is searching the pages list and there are no matching pages - Text displayed when there's no matching with the text search */ +/* Text displayed when there's no matching with the text search */ "No pages matching your search" = "Žiadne stránky nezodpovedajú vyhľadávaniu"; /* Text displayed when search for plugins returns no results */ @@ -2391,9 +2299,6 @@ /* Message shown whent the reader finds no posts for the chosen tag */ "No posts have been made recently with this tag." = "Žiadne nedávne články s touto značkou."; -/* Displayed when the user is searching the posts list and there are no matching posts */ -"No posts matching your search" = "Žiadne články nezodpovedajú vyhľadávaniu"; - /* A message title */ "No recent posts" = "Žiadne nedávne články"; @@ -2449,9 +2354,6 @@ /* A message title */ "Nothing liked yet" = "Žiaden príspevok označený \"Páči sa mi to\""; -/* Default message for empty media picker */ -"Nothing to show" = "Žiadne možnosti"; - /* Notifications Details Accessibility Identifier */ "Notification Details Table" = "Detailná tabuľka notifikácií"; @@ -2484,7 +2386,6 @@ Alert dismissal title Button title. Acknowledges a prompt. Button title. An acknowledgement of the message displayed in a prompt. - Confirmation of action Default action Dismisses the alert Marks all notifications as read. @@ -2547,9 +2448,6 @@ Title of a button that opens the apps settings in the system Settings.app */ "Open Settings" = "Otvoriť nastavenia"; -/* Editor button to swich the media picker from quick mode to full picker */ -"Open full media picker" = "Otvoriť výber všetkých médií"; - /* No comment provided by engineer. */ "Open in Safari" = "Otvoriť v Safari"; @@ -2602,24 +2500,12 @@ Title shown when selecting a post type of Page from the Share Extension. */ "Page" = "Stránka"; -/* Prompts the user that a restored page was moved to the drafts list. */ -"Page Restored to Drafts" = "Stránka obnovená do konceptov"; - -/* Prompts the user that a restored page was moved to the published list. */ -"Page Restored to Published" = "Stránka obnovená do publikovaných"; - -/* Prompts the user that a restored page was moved to the scheduled list. */ -"Page Restored to Scheduled" = "Stránka obnovená do naplánovaných"; - /* Title of notification displayed when a page has been successfully saved as a draft. */ "Page draft uploaded" = "Koncept stránky sa nahral"; /* Title of notification displayed when a page has failed to upload. */ "Page failed to upload" = "Zlyhalo nahranie stránky"; -/* A short message explaining that a page was moved to the trash bin. */ -"Page moved to trash." = "Stránka presunutá do koša."; - /* Title of notification displayed when a page has been successfully saved as a draft. */ "Page pending review" = "Stránka čaká na kontrolu"; @@ -2664,8 +2550,7 @@ Title of pending Comments filter. */ "Pending" = "Čakajúce"; -/* Name for the status of a post pending review. - Title of the Pending Review Badge */ +/* Name for the status of a post pending review. */ "Pending review" = "Čakajúce na kontrolu"; /* Noun. Title of the people management feature. @@ -2685,12 +2570,6 @@ /* Accessibility label for selecting an image or video from the device's photo library on formatting toolbar. */ "Photo Library" = "Knižnica fotografií"; -/* Tab bar title for the Photos tab in Media Picker */ -"Photos" = "Fotky"; - -/* Subtitle for placeholder in Free Photos. The company name 'Pexels' should always be written as it is. */ -"Photos provided by Pexels" = "Fotografie poskytnuté službou Pexels"; - /* Title for selecting a new username in the site creation flow. */ "Pick username" = "Vyberte užívateľské meno"; @@ -2781,15 +2660,6 @@ The post formats available for the post. Should be the same as in core WP. */ "Post Format" = "Pridať formát"; -/* Prompts the user that a restored post was moved to the drafts list. */ -"Post Restored to Drafts" = "Článok obnovený ako koncept"; - -/* Prompts the user that a restored post was moved to the published list. */ -"Post Restored to Published" = "Článok obnovený ako publikovaný"; - -/* Prompts the user that a restored post was moved to the scheduled list. */ -"Post Restored to Scheduled" = "Článok obnovený ako naplánovaný"; - /* Name of the button to open the post settings The title of the Post Settings screen. */ "Post Settings" = "Nastavenia článku"; @@ -2806,9 +2676,6 @@ /* Title of notification displayed when a post has failed to upload. */ "Post failed to upload" = "Zlyhalo nahranie článku"; -/* A short message explaining that a post was moved to the trash bin. */ -"Post moved to trash." = "Článok presunutý do koša."; - /* Title of notification displayed when a post has been successfully saved as a draft. */ "Post pending review" = "Článok čaká na kontrolu"; @@ -2864,9 +2731,6 @@ Title for screen to preview a static content. */ "Preview" = "Náhľad"; -/* Action for Media Picker to preview the selected media items. The argument in the string represents the number of elements (as numeric digits) selected */ -"Preview %@" = "Náhľad %@"; - /* No comment provided by engineer. */ "Preview post" = "Predchádzajúce články"; @@ -2892,8 +2756,7 @@ "Privacy Settings" = "Nastavenia súkromia"; /* Name for the status of a post that is marked private. - Privacy setting for posts set to 'Private'. Should be the same as in core WP. - Title of the Private Badge */ + Privacy setting for posts set to 'Private'. Should be the same as in core WP. */ "Private" = "Súkromné"; /* Error message title informing the user that a search for sites in the Reader could not be loaded. */ @@ -2916,12 +2779,10 @@ Title for the publish settings view */ "Publish" = "Zverejniť"; -/* A short phrase indicating a post is due to be immedately published. - Label for a button that moves a page to the published folder, publishing with the current date/time. */ +/* A short phrase indicating a post is due to be immedately published. */ "Publish Immediately" = "Zverejniť okamžite"; /* Label for a button that publishes the post - Label for an option that moves a publishes a post immediately Title of button allowing the user to immediately publish the post they are editing. */ "Publish Now" = "Publikovať"; @@ -2933,8 +2794,7 @@ /* Name for the status of a published post. Period Stats 'Published' header - Title of the published filter. This filter shows a list of posts that the user has published. - Title of the published header in search list. */ + Title of the published filter. This filter shows a list of posts that the user has published. */ "Published" = "Zverejnené"; /* Precedes the name of the blog just posted on */ @@ -3022,8 +2882,7 @@ /* Label for selecting the related posts options */ "Related Posts" = "Súvisiace články"; -/* Add asset to media picker list - Alert button to confirm a plugin to be removed +/* Alert button to confirm a plugin to be removed Button label when removing a blog Label for the Remove Feature Image icon. Tapping will show a confirmation screen for removing the feature image from the post. Remove Action @@ -3116,9 +2975,6 @@ /* Title of secondary button on alert prompting verify their accounts while attempting to publish */ "Resend" = "Znovu odoslať"; -/* Title of the reset button */ -"Reset" = "Obnoviť"; - /* Accessibility label for the reset filter button in the reader. */ "Reset filter" = "Resetovať filtre"; @@ -3142,12 +2998,9 @@ Button title, displayed when media has failed to upload. Allows the user to try the upload again. Button title. Retries uploading a post. If a user taps the button with this label, the action that evinced this error view will be retried. - Label for a button that attempts to re-upload a page that previously failed to upload. - Label for the retry post upload button. Tapping attempts to upload the post again. Opens the media library . Retries the upload of a user's post. Retry updating User's Role - Retry uploading the post. Retry. Action Retry. Verb – retry a failed media upload. The Jetpack view button title used when an error occurred @@ -3156,9 +3009,6 @@ User action to retry media upload. */ "Retry" = "Obnoviť"; -/* User action to retry media upload. */ -"Retry Upload" = "Nahrať znova"; - /* User action to retry all failed media uploads. */ "Retry all" = "Zopakovať všetko"; @@ -3226,9 +3076,6 @@ /* Accessibility label for the 'Save Post' button when a post has been saved. */ "Saved Post" = "Uložený príspevok"; -/* Text displayed in HUD when a media item's metadata (title, etc) is saved successfully. */ -"Saved!" = "Uložené!"; - /* Accessibility hint for the 'Save Post' button. */ "Saves this post for later." = "Uložiť tento príspevok na neskôr."; @@ -3236,7 +3083,6 @@ "Saving post…" = "Článok sa ukladá..."; /* Menus save button title while it is saving a Menu. - Text displayed in HUD while a media item's metadata (title, etc) is being saved. Text displayed in HUD while a post is being saved as a draft. */ "Saving..." = "Ukladá sa..."; @@ -3270,15 +3116,6 @@ /* Placeholder text for the Reader search feature. */ "Search WordPress" = "Prehľadávať WordPress"; -/* Text displayed when the search controller will be presented */ -"Search pages" = "Vyhľadať stránky"; - -/* Title for placeholder in Tenor picker */ -"Search to find GIFs to add to your Media Library!" = "Vyhľadajte GIFy a pridajte ich do Knižnice médií!"; - -/* Title for placeholder in Free Photos */ -"Search to find free photos to add to your Media Library!" = "Vyhľadajte zadarmo fotografie, ktoré si pridáte do Knižnice médií"; - /* Menus search bar placeholder text. */ "Search..." = "Hľadať..."; @@ -3307,9 +3144,6 @@ /* No comment provided by engineer. */ "Select a color" = "Vybrať farbu"; -/* Accessibility hint for actions when displaying media items. */ -"Select media." = "Vybrať súbor."; - /* Accessibility label for selecting paragraph style button on formatting toolbar. */ "Select paragraph style" = "Vyberte štýl odseku"; @@ -3361,8 +3195,7 @@ /* Label for connected service in Publicize stat. */ "Service" = "Služba"; -/* Label for a button that opens the Set Parent options view controller - Navigation title displayed on the navigation bar */ +/* Navigation title displayed on the navigation bar */ "Set Parent" = "Nastaviť Nadradený"; /* No comment provided by engineer. */ @@ -3393,7 +3226,6 @@ /* Accessibility label for share buttons in nav bars Button label to share a post Button label to share a web page - Share the post. Shares the comment URL Spoken accessibility label Title for a button that allows the user to share their answer to the prompt. @@ -3623,8 +3455,7 @@ Title of Start Over settings page */ "Start Over" = "Začať znova"; -/* Label for post stats option. Tapping displays statistics for a post. - Noun. Abbreviation of Statistics. Name of the Stats feature +/* Noun. Abbreviation of Statistics. Name of the Stats feature Noun. Abbv. of Statistics. Links to a blog's Stats screen. Stats 3D Touch Shortcut Stats window title @@ -3678,7 +3509,7 @@ Theme Support action title */ "Support" = "Podpora"; -/* Button used to switch site */ +/* Title for back button that leads to the site picker screen. */ "Switch Site" = "Prepnúť webovú stránku"; /* Switches the Editor to HTML Mode */ @@ -3733,9 +3564,6 @@ /* Displayed when the user views tags in blog settings and there are no tags */ "Tags created here can be quickly added to new posts" = "Vytvorené značky sa dajp rýchlo pridať do nového článku"; -/* Menu option for taking an image or video with the device's camera. */ -"Take Photo or Video" = "Odfotogravovať alebo nasnímať video"; - /* A hint displayed in the Saved Posts section of the Reader. The '[bookmark-outline]' placeholder will be replaced by an icon at runtime – please leave that string intact. */ "Tap [bookmark-outline] to save a post to your list." = "Klepnutím na položku [bookmark-outline] uložte príspevok do svojho zoznamu."; @@ -3763,8 +3591,7 @@ /* No comment provided by engineer. */ "Text color" = "Farba textu"; -/* Button title - The button's title text to send a 2FA code via SMS text message. */ +/* Button title */ "Text me a code instead" = "Radšej mi pošlite kód"; /* Message of alert when theme activation succeeds */ @@ -3791,9 +3618,6 @@ /* No comment provided by engineer. */ "That username is not allowed." = "Toto používateľské meno nie je povolené."; -/* Message shown when a GIF failed to load while trying to add it to the Media library. */ -"The GIF could not be added to the Media Library." = "GIF formát sa nepodarilo pripojiť do Knižnice médií. "; - /* Error message shown when a media upload fails because the user isn't connected to the Internet. Message of error prompt shown when a user tries to perform an action without an internet connection. */ "The Internet connection appears to be offline." = "Zdá sa, že internetové pripojenie je vypnuté."; @@ -3861,7 +3685,7 @@ /* Error message shown when user attempts to remove the site owner. */ "The user you are trying to remove is the owner of this site. Please contact support for assistance." = "Používateľ, ktorého sa pokúšate odstrániť, je vlastníkom tejto stránky. Ak potrebujete pomoc, kontaktujte technickú podporu."; -/* Error message informing a user about an invalid password. */ +/* No comment provided by engineer. */ "The username or password stored in the app may be out of date. Please re-enter your password in the settings and try again." = "Používateľské meno alebo heslo v aplikácii už môže byť neaktuálne. Zadajte vaše heslo v nastaveniach a skúste to znova."; /* Message shown when a video failed to load while trying to add it to the Media library. */ @@ -3917,9 +3741,6 @@ /* A short error message letting the user know about a problem displaying a post. */ "There was a problem displaying this post." = "Došlo k chybe pri zobrazení tohto príspevku."; -/* Error message displayed when the Media Library is unable to load a full sized preview of an item. */ -"There was a problem loading the media item." = "Pri načítavaní mediálneho súboru sa vyskytol problém."; - /* Error message informing the user that there was a problem clearing the block on site preventing its posts from displaying in the reader. */ "There was a problem removing the block for specified site." = "Nastal problém pri odstraňovaní bloku z určenej webovej stránky,"; @@ -3929,9 +3750,6 @@ /* Explaining to the user there was an error trying to obtain the current location of the user. */ "There was a problem when trying to access your location. Please try again later." = "Došlo k chybe pri pokuse načítať vašu polohu. Skúste to znovu pozdejšie."; -/* Explaining to the user there was an generic error accesing media. */ -"There was a problem when trying to access your media. Please try again later." = "Vyskytol sa problém pri pokuse sprístupniť vaše súbory. Skúste to znovu neskôr."; - /* Message for stories unknown error. */ "There was a problem with the Stories editor. If the problem persists you can contact us via the Me > Help & Support screen." = "Vyskytol sa problém so sledovaním webovej stránky. Ak problém aj naďalej trvá, kontaktujte nás cez Profil > Pomocník & Podpora."; @@ -3965,9 +3783,6 @@ /* An error message display if the users device does not have a camera input available */ "This app needs permission to access the Camera to capture new media, please change the privacy settings if you wish to allow this." = "Táto aplikácia potrebuje povolenie na prístup do fotoaparátu na zachytenie nových súborov, prosím zmeňte nastavenia súkromia, pokiaľ si prajete jeho prijatie."; -/* Explaining to the user why the app needs access to the device media library. */ -"This app needs permission to access your device media library in order to add photos and\/or video to your posts. Please change the privacy settings if you wish to allow this." = "Táto aplikácia potrebuje povolenie na prístup do knižnice súborov vo vašom zariadení, aby ste mohli pridať fotografie a\/alebo video do vášho článku. Prosím zmeňte nastavenia súkromia, pokiaľ si prajete ich pridanie."; - /* An error message informing the user the email address they entered did not match a WordPress.com account. */ "This email address is not registered on WordPress.com." = "Táto e-mailová adresa nie je registrovaná na WordPress.com."; @@ -4093,8 +3908,7 @@ /* Title for the traffic section in site settings screen */ "Traffic" = "Prenos"; -/* Accessibility label for trash button to delete items from the user's media library - Accessibility label for trash buttons in nav bars +/* Accessibility label for trash buttons in nav bars Trashes a comment Trashes the comment */ "Trash" = "Kôš"; @@ -4159,18 +3973,12 @@ /* Shown when a user logs in with Google but it subsequently fails to work as login to WordPress.com */ "Unable To Connect" = "Nedá sa pripojiť"; -/* An error message. */ -"Unable to Connect" = "Nedá sa pripojiť"; - /* Title of a prompt saying the app needs an internet connection before it can load posts */ "Unable to Load Posts" = "Nepodarilo sa načítať články"; /* Title of error prompt shown when a sync the user initiated fails. */ "Unable to Sync" = "Nie je možné synchronizovať"; -/* Text displayed in HUD if there was an error attempting to delete a group of media items. */ -"Unable to delete all media items." = "Nedajú sa zmazať všetky mediálne súbory."; - /* Text displayed in HUD if there was an error attempting to delete a media item. */ "Unable to delete media item." = "Nedá sa zmazať mediálny súbor."; @@ -4240,8 +4048,6 @@ Button title. Reverts the previous notification operation Revert an operation Revert enabling notification after successfully subcribing to the comments for the post. - The title of an 'undo' button. Tapping the button moves a trashed page out of the trash folder. - The title of an 'undo' button. Tapping the button moves a trashed post out of the trash folder. Undo action */ "Undo" = "Zrušiť"; @@ -4271,9 +4077,6 @@ /* Title for Unknown HTML Editor */ "Unknown HTML" = "Neznámy kód HTML"; -/* Label to use when creation date from media asset is not know. */ -"Unknown creation date" = "Neznámy dátum vytvorenia"; - /* No comment provided by engineer. */ "Unknown error" = "Neznáma chyba"; @@ -4424,15 +4227,10 @@ /* Message shown if a video export is canceled by the user. */ "Video export canceled." = "Export videa bol zrušený."; -/* Accessibility label for video thumbnails in the media collection view. The parameter is the creation date of the video. */ -"Video, %@" = "Video, %@"; - /* Period Stats 'Videos' header */ "Videos" = "Videá"; /* Button title. Displays a summary / sharing screen for a specific post. - Label for a button that opens the page when tapped. - Label for the view post button. Tapping displays the post as it appears on the web. Opens the post epilogue screen to allow sharing / viewing of a post. Theme View action title Verb. The screen title shown when viewing a post inside the app. */ @@ -4610,9 +4408,6 @@ /* Siri Suggestion to open Support */ "WordPress Help" = "Pomoc WordPress"; -/* Name for the WordPress Media Library */ -"WordPress Media" = "WordPress média"; - /* Accessibility label for selecting an image or video from the user's WordPress media library on formatting toolbar. */ "WordPress Media Library" = "Knižnica médií WordPress"; @@ -4838,9 +4633,6 @@ /* Age between dates equaling one hour. */ "an hour" = "hodina"; -/* Label displayed on audio media items. */ -"audio" = "audio"; - /* Used when displaying author of a plugin. */ "by %@" = "kým %@"; @@ -4850,9 +4642,6 @@ /* (placeholder) Help the user enter a URL into the field */ "http:\/\/my-site-address (URL)" = "http:\/\/moja-stranka.sk (URL adresa)"; -/* Label displayed on image media items. */ -"image" = "obrázok"; - /* This text is used when the user is configuring the iOS widget to suggest them to select the site to configure the widget for */ "ios-widget.gpCwrM" = "Select Site"; @@ -4862,21 +4651,12 @@ /* Later today */ "later today" = "neskôr dnes"; -/* Word separating the current index from the total amount. I.e.: 7 of 9 */ -"of" = "z"; - -/* Label displayed on media items that are not video, image, or audio. */ -"other" = "ďalšie"; - /* Header of delete screen section listing things that will be deleted. */ "these items will be deleted:" = "nasledujúce položky budú zmazané:"; /* Used when the response doesn't have a valid url to display */ "unknown url" = "neznáma url adresa"; -/* Label displayed on video media items. */ -"video" = "video"; - /* Title of posts label in all time widget */ "widget.alltime.posts.label" = "Články"; diff --git a/WordPress/Resources/sq.lproj/Localizable.strings b/WordPress/Resources/sq.lproj/Localizable.strings index 192001400889..c9c4ffd4f5ed 100644 --- a/WordPress/Resources/sq.lproj/Localizable.strings +++ b/WordPress/Resources/sq.lproj/Localizable.strings @@ -1,4 +1,4 @@ -/* Translation-Revision-Date: 2023-10-18 12:01:40+0000 */ +/* Translation-Revision-Date: 2024-01-04 11:04:02+0000 */ /* Plural-Forms: nplurals=2; plural=n != 1; */ /* Generator: GlotPress/4.0.0-alpha.11 */ /* Language: sq_AL */ @@ -152,9 +152,6 @@ /* Difference label for Period Overview stat, indicating change from previous period. Ex: +99.9K (5%) */ "%@%@ (%@%%)" = "%1$@%2$@ (%3$@%%)"; -/* Accessibility label for a post in the post list. The parameters are the title, and date respectively. For example, \"Let it Go, 1 hour ago.\" */ -"%@, %@." = "%1$@, %2$@."; - /* Accessibility value for a Stats' Posting Activity Month if the user selected a day with posts. The first parameter is day (e.g. November 2019). The second parameter is the number of posts. */ "%@. %d posts." = "%1$@. %2$d postime."; @@ -210,9 +207,6 @@ /* Age between dates over one year. */ "%d years" = "%d vjet"; -/* Max image size in pixels (e.g. 300x300px) */ -"%dx%dpx" = "%1$dx%2$dpx"; - /* One menu area available in the theme */ "%i menu area in this theme" = "%i fushë menuje në këtë skemë"; @@ -271,6 +265,9 @@ translators: Block name. %s: The localized block name */ /* translators: %s: social link name e.g: \"Instagram\". */ "%s social icon" = "Ikonë shoqërorësh %s"; +/* translators: displayed right after the classic block is converted to blocks. %s: The localized classic block name */ +"'%s' block converted to blocks" = "Blloku “%s ” u shndërrua në blloqe"; + /* translators: Missing block alert title. %s: The localized block name */ "'%s' is not fully-supported" = "'%s' s’mbulohet plotësisht"; @@ -278,7 +275,7 @@ translators: Block name. %s: The localized block name */ "(No Title)" = "(Pa Titull)"; /* Menus title label text for a post that has no set title. */ -"(Untitled)" = "(I patitull)"; +"(Untitled)" = "(Pa titull)"; /* Lets a user know that a local draft does not have a title. */ "(no title)" = "(pa titull)"; @@ -421,6 +418,9 @@ translators: Block name. %s: The localized block name */ /* Label for selecting the Accelerated Mobile Pages (AMP) Blog Traffic Setting */ "Accelerated Mobile Pages (AMP)" = "Faqe Për Celular të Përshpejtuara (AMP)"; +/* No comment provided by engineer. */ +"Access this Paywall block on your web browser for advanced settings." = "Për rregullime të mëtejshme, hapeni këtë bllok Paywall në shfletuesin tuaj."; + /* Title for the account section in site settings screen */ "Account" = "Llogari"; @@ -475,13 +475,6 @@ translators: Block name. %s: The localized block name */ /* Label for the Activity Type filter button when there are more than 1 activity type selected */ "Activity Type (%1$d)" = "Lloj Veprimtarie (%1$d)"; -/* Accessibility label for add button to add items to the user's media library - Remove asset from media picker list */ -"Add" = "Shtoje"; - -/* Action for Media Picker to indicate selection of media. The argument in the string represents the number of elements (as numeric digits) selected */ -"Add %@" = "Shtoni %@"; - /* No comment provided by engineer. */ "Add Block After" = "Shtoni Bllok Pas"; @@ -578,9 +571,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Add menu item to children" = "Shtoni element menuje te pjellat"; -/* Accessibility hint for add button to add items to the user's media library */ -"Add new media" = "Shtoni media të re"; - /* Screen reader text for Menus button that adds a new menu to a site. */ "Add new menu" = "Shtoni menu të re"; @@ -646,9 +636,6 @@ translators: Block name. %s: The localized block name */ /* Option to select the Airmail app when logging in with magic links */ "Airmail" = "Airmail"; -/* Description of albums in the photo libraries */ -"Albums" = "Albume"; - /* Image alignment option title. Title of the screen for choosing an image's alignment. */ "Alignment" = "Drejtim"; @@ -662,6 +649,9 @@ translators: Block name. %s: The localized block name */ Title of the drafts filter. This filter shows a list of draft posts. */ "All" = "Krejt"; +/* Information about redeeming domain credit on site dashboard. */ +"All WordPress.com annual plans include a custom domain name. Register your free domain now." = "Krejt planet vjetore WordPress.com përmbajnë një emër përkatësie vetjake. Regjistrohuni që tani për të marrë falas përkatësinë tuaj."; + /* Footer of the free domain registration section for a paid plan. Information about redeeming domain credit on site dashboard. */ "All WordPress.com plans include a custom domain name. Register your free premium domain now." = "Krejt planet WordPress.com përfshijnë një emër vetjak përkatësie. Regjistroni falas që tani përkatësinë tuaj."; @@ -727,10 +717,13 @@ translators: Block name. %s: The localized block name */ "Alt Text" = "Tekst Alternativ"; /* No comment provided by engineer. */ -"Alternatively, you can detach and edit these blocks separately by tapping “Detach patterns”." = "Ndryshe, mund t’i shkëputni dhe përpunoni ndarazi këto blloqe, duke prekur “Shkëputi modelet”."; +"Alternatively, you can convert the content to blocks." = "Ndryshe, mund ta shndërroni lëndën në blloqe."; /* No comment provided by engineer. */ -"Alternatively, you can detach and edit this block separately by tapping “Detach pattern”." = "Ndryshe, mund ta shkëputni dhe përpunoni ndarazi këtë bllok, duke prekur “Shkëpute modelin”."; +"Alternatively, you can detach and edit this block separately by tapping “Detach”." = "Ndryshe, mund ta shkëputni dhe përpunoni ndarazi këtë bllok, duke prekur “Shkëpute”."; + +/* translators: Alternative option included in a warning related to having blocks deeply nested. */ +"Alternatively, you can flatten the content by ungrouping the block." = "Ndryshe, mund ta sheshoni lëndën duke hequr bllokun nga grupi."; /* Instruction text to explain to help users type their password instead of using magic link login option. */ "Alternatively, you may enter the password for this account." = "Ndryshe, mund të jepni fjalëkalimin për këtë llogari."; @@ -879,15 +872,9 @@ translators: Block name. %s: The localized block name */ /* Message prompting the user to confirm that they want to disconnect Jetpack from the site. */ "Are you sure you want to disconnect Jetpack from the site?" = "Jeni i sigurt se doni të shkëputet Jetpack-u nga sajti?"; -/* Message prompting the user to confirm that they want to permanently delete a group of media items. */ -"Are you sure you want to permanently delete these items?" = "Jeni i sigurt që doni të fshihen përgjithmonë këto objekte?"; - /* Message prompting the user to confirm that they want to permanently delete a media item. Should match Calypso. */ "Are you sure you want to permanently delete this item?" = "Jeni i sigurt që doni të fshihet përgjithmonë ky objekt?"; -/* Message of the confirmation alert when deleting a page from the trash. */ -"Are you sure you want to permanently delete this page?" = "Jeni i sigurt se doni të fshihet përgjithmonë kjo faqe?"; - /* Message of the confirmation alert when deleting a post from the trash. */ "Are you sure you want to permanently delete this post?" = "Jeni i sigurt se doni të fshihet përgjithmonë ky postim?"; @@ -919,9 +906,6 @@ translators: Block name. %s: The localized block name */ /* Title of message shown when user taps submit for review. */ "Are you sure you want to submit for review?" = "Jeni i sigurt se doni ta parashtroni për shqyrtim?"; -/* Message of the trash page confirmation alert. */ -"Are you sure you want to trash this page?" = "Jeni i sigurt se doni të hidhet në hedhurina kjo faqe?"; - /* Message of the trash confirmation alert. */ "Are you sure you want to trash this post?" = "Jeni i sigurt se doni të shpihet ky postim te hedhurinat?"; @@ -962,9 +946,6 @@ translators: Block name. %s: The localized block name */ /* translators: accessibility text. Empty Audio caption. */ "Audio caption. Empty" = "Marrje audio. E zbrazët"; -/* Accessibility label for audio items in the media collection view. The parameter is the creation date of the audio. */ -"Audio, %@" = "Audio, %@"; - /* No comment provided by engineer. */ "Authenticating" = "Po bëhet mirëfilltësimi"; @@ -1116,6 +1097,9 @@ translators: Block name. %s: The localized block name */ Discoverability title for block quote keyboard shortcut. */ "Block Quote" = "Bllok Citimi"; +/* No comment provided by engineer. */ +"Block cannot be rendered because it is deeply nested. Tap here for more details." = "Blloku s’vizatohet dot, ngaqë është i futur thellë të tjerësh. Për më tepër hollësi, prekni këtu."; + /* translators: displayed right after the block is copied. */ "Block copied" = "Blloku u kopjua"; @@ -1168,6 +1152,9 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Blocks menu" = "Menu blloqesh"; +/* translators: Warning related to having blocks deeply nested. %d: The deepest nesting level. */ +"Blocks nested deeper than %d levels may not render properly in the mobile editor." = "Blloqe brenda njëri-tjetri më thellë se %d nivele mund të mos funksionojnë si duhet te përpunuesi për celular."; + /* Title of a button that displays the WordPress.com blog */ "Blog" = "Blog"; @@ -1244,9 +1231,6 @@ translators: Block name. %s: The localized block name */ /* Label for the post author in the post detail. */ "By " = "Nga "; -/* Accessibility label for the post author in the post list. The parameter is the author name. For example, \"By Elsa.\" */ -"By %@." = "Nga %@."; - /* Legal disclaimer for logging in. The underscores _..._ denote underline. */ "By continuing, you agree to our _Terms of Service_." = "Duke vazhduar, pajtoheni me _Termat tona të Shërbimit_."; @@ -1266,8 +1250,7 @@ translators: Block name. %s: The localized block name */ /* Label for size of media while it's being calculated. */ "Calculating..." = "Po llogariten…"; -/* Accessibility label for taking an image or video with the camera on formatting toolbar. - Accessibility label for the camera tile in the collection view */ +/* Accessibility label for taking an image or video with the camera on formatting toolbar. */ "Camera" = "Kamerë"; /* Title of an alert informing the user the camera permission for the app is disabled and its needed to proceed */ @@ -1318,10 +1301,7 @@ translators: Block name. %s: The localized block name */ Cancels an Action Cancels an alert. Cancels the mark all as read action. - Dismiss the post action sheet Editing GIF alert cancel action button. - Label for a cancel button - Label for the auto-upload cancelation button in the post list. Tapping will prevent the app from auto-uploading the post. Menus cancel button for deleting a menu. Menus cancel button within text bar while editing items. Menus: Cancel button title for canceling an edited menu item. @@ -1338,10 +1318,6 @@ translators: Block name. %s: The localized block name */ Verb. Title for Jetpack Restore cancel button. */ "Cancel" = "Anuloje"; -/* Label for the Post List option that cancels automatic uploading of a post. - Media Library option to cancel an in-progress or failed upload. */ -"Cancel Upload" = "Anuloje Ngarkimin"; - /* Menus alert button title to cancel discarding changes and not createa a new menu. Menus alert button title to cancel discarding changes and not select a new menu Menus alert button title to cancel discarding changes and not select a new menu location */ @@ -1400,9 +1376,6 @@ translators: Block name. %s: The localized block name */ Main title */ "Change Password" = "Ndryshoni Fjalëkalimin"; -/* Title of button that takes user to the system Settings section for the app */ -"Change Settings" = "Ndryshoni Rregullimet"; - /* Change Username title. */ "Change Username" = "Ndryshoni Emër Përdoruesi"; @@ -1542,9 +1515,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Choose file" = "Zgjidhni kartelë"; -/* Menu option for selecting media from the device's photo library. */ -"Choose from My Device" = "Zgjedhje nga Pajisja Ime"; - /* Explanatory text for Homepage Settings homepage type selection. */ "Choose from a homepage that displays your latest posts (classic blog) or a fixed \/ static page." = "Zgjidhni prej një faqeje hyrëse që shfaq postimet tuaja më të reja (blog klasik) ose një faqe të fiksuar \/ statike."; @@ -1745,9 +1715,6 @@ translators: Block name. %s: The localized block name */ /* Community & Non-Profit site intent topic */ "Community & Non-Profit" = "Bashkësi & Jofitimprurëse"; -/* Accessibility indication that the current Post List style is currently Compact. */ -"Compact" = "E ngjeshur"; - /* The action is completed */ "Completed" = "I përfunduar"; @@ -1933,10 +1900,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Copied block" = "Blloku u kopjua"; -/* Copy the post url and paste anywhere in phone - Label for page copy link. Tapping copy the url of page */ -"Copy Link" = "Kopjoji Lidhjen"; - /* Copy link to oomment button title */ "Copy Link to Comment" = "Kopjo Lidhjen për te Komenti"; @@ -2010,7 +1973,7 @@ translators: Block name. %s: The localized block name */ "Could you tell us how we could improve?" = "Mund të na tregoni si ta përmirësojmë?"; /* No comment provided by engineer. */ -"Couldn't Connect" = "Su Lidh Dot"; +"Couldn't Connect" = "S’u Lidh Dot"; /* Error message shown a URL points to a valid site but not a WordPress site. */ "Couldn't connect to the WordPress site. There is no valid WordPress site at this address. Check the site address (URL) you entered." = "S’u lidh dot te sajt WordPress. S’ka sajt WordPress të vlefshëm në këtë adresë. Kontrolloni adresën (URL) të sajtit që dhatë."; @@ -2019,7 +1982,7 @@ translators: Block name. %s: The localized block name */ "Couldn't connect. Required XML-RPC methods are missing on the server." = "S’u lidh dot. Te shërbyesi mungojnë metodat e domosdoshme XML-RPC."; /* Message to show to user when he tries to add a self-hosted site but the host returned a 403 error, meaning that the access to the /xmlrpc.php file is forbidden. */ -"Couldn't connect. We received a 403 error when trying to access your site's XMLRPC endpoint. The app needs that in order to communicate with your site. Contact your host to solve this problem." = "S’u bë dot lidhja. Morëm një gabim 403 error, kur u provua të hyhej në pikë fundore XMLRPC të sajtit tuaj. Kjo i duhet aplikacionit, që të mund të komunikojë me sajtin tuaj. Lidhuni me strehuesin tuaj që ta zgjidhni këtë problem."; +"Couldn't connect. We received a 403 error when trying to access your site's XMLRPC endpoint. The app needs that in order to communicate with your site. Contact your host to solve this problem." = "S’u bë dot lidhja. Morëm një gabim 403, kur u provua të hyhej në pikëmbarim XMLRPC të sajtit tuaj. Kjo i duhet aplikacionit, që të mund të komunikojë me sajtin tuaj. Lidhuni me strehuesin tuaj që ta zgjidhni këtë problem."; /* Message to show to user when he tries to add a self-hosted site but the host returned a 405 error, meaning that the host is blocking POST requests on /xmlrpc.php file. */ "Couldn't connect. Your host is blocking POST requests, and the app needs that in order to communicate with your site. Contact your host to solve this problem." = "S’u bë dot lidhja. Strehuesi juaj i bllokon kërkesat POST, dhe aplikacionit i duhen që të mund të komunikojë me sajtin tuaj. Lidhuni me strehuesin tuaj që ta zgjidhni këtë problem."; @@ -2045,9 +2008,6 @@ translators: Block name. %s: The localized block name */ /* Error title displayed when unable to close user account. */ "Couldn’t close account automatically" = "Llogaria s’u mbyll dot automatikisht"; -/* Message to show while media data source is finding the number of items available. */ -"Counting media items..." = "Po numërohen objekte media…"; - /* Period Stats 'Countries' header */ "Countries" = "Vende"; @@ -2295,7 +2255,6 @@ translators: Block name. %s: The localized block name */ /* Delete Delete button title for the warning shown to the user when he refuses to re-login when the authToken is missing. Title for button that permanently deletes a media item (photo / video) - Title for button that permanently deletes one or more media items (photos / videos) Title of a delete button Title of the trash confirmation alert. */ "Delete" = "Fshije"; @@ -2303,15 +2262,11 @@ translators: Block name. %s: The localized block name */ /* Menus confirmation button for deleting a menu. */ "Delete Menu" = "Fshije Menunë"; -/* Delete option in the confirmation alert when deleting a page from the trash. - Delete option in the confirmation alert when deleting a post from the trash. - Label for a button permanently deletes a page. - Label for the delete post option. Tapping permanently deletes a post. +/* Delete option in the confirmation alert when deleting a post from the trash. Title for button on the comment details page that deletes the comment when tapped. */ "Delete Permanently" = "Fshije Përgjithmonë"; -/* Title of the confirmation alert when deleting a page from the trash. - Title of the confirmation alert when deleting a post from the trash. */ +/* Title of the confirmation alert when deleting a post from the trash. */ "Delete Permanently?" = "Të Fshihet Përgjithmonë?"; /* Button label for deleting the current site @@ -2427,7 +2382,6 @@ translators: Block name. %s: The localized block name */ /* Accessibility label for button to dismiss a bottom sheet Accessibility label for the transparent space above the login dialog which acts as a button to dismiss the dialog. Accessibility label for the transparent space above the signup dialog which acts as a button to dismiss the dialog. - Action to show on alert when view asset fails. Dismiss a view. Verb */ "Dismiss" = "Hidhe tej"; @@ -2445,12 +2399,6 @@ translators: Block name. %s: The localized block name */ User's Display Name */ "Display Name" = "Emër Shfaqjeje"; -/* Accessibility label for other media items in the media collection view. The parameter is the creation date of the document. */ -"Document, %@" = "Dokument, %@"; - -/* Accessibility label for other media items in the media collection view. The parameter is the filename file. */ -"Document: %@" = "Dokument: %@"; - /* Message shown when all tours have been completed */ "Doesn't it feel good to cross things off a list?" = "A s’ju kënaq t’u hiqni vizë gjërave në një listë?"; @@ -2458,7 +2406,7 @@ translators: Block name. %s: The localized block name */ "Domain contact information" = "Të dhëna kontakti përkatësie"; /* Register Domain - Privacy Protection section header description */ -"Domain owners have to share contact information in a public database of all domains. With Privacy Protection, we publish our own information instead of yours and privately forward any communication to you." = "Të zotëve të përkatësive u duhet të japin të dhëna kontakti te një bazë publike të dhënash e krejt përkatësive. Me Mbrojtjen e Privatësisë, ne publikojmë të dhënat tona, në vend se tuajat, dhe ju përcjellim privatisht çfarëdo komunikimi për ju."; +"Domain owners have to share contact information in a public database of all domains. With Privacy Protection, we publish our own information instead of yours and privately forward any communication to you." = "Të zotëve të përkatësive u duhet të japin hollësi kontakti te një bazë publike të dhënash e krejt përkatësive. Me Mbrojtjen e Privatësisë, ne publikojmë të dhënat tona, në vend se tuajat, dhe ju përcjellim privatisht çfarëdo komunikimi për ju."; /* Noun. Title. Links to the Domains screen. */ "Domains" = "Përkatësi"; @@ -2611,8 +2559,7 @@ translators: Block name. %s: The localized block name */ /* Description of a Quick Start Tour */ "Draft and publish a post." = "Hartoni dhe botoni një postim."; -/* Title of the drafts filter. This filter shows a list of draft posts. - Title of the drafts header in search list. */ +/* Title of the drafts filter. This filter shows a list of draft posts. */ "Drafts" = "Skica"; /* No comment provided by engineer. */ @@ -2624,10 +2571,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Drag to adjust focal point" = "Që të rregulloni vatrën, tërhiqeni"; -/* Label for page duplicate option. Tapping creates a copy of the page. - Label for post duplicate option. Tapping creates a copy of the post. */ -"Duplicate" = "Përsëdyte"; - /* No comment provided by engineer. */ "Duplicate block" = "Përsëdyte bllokun"; @@ -2640,13 +2583,9 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Each block has its own settings. To find them, tap on a block. Its settings will appear on the toolbar at the bottom of the screen." = "Çdo bllok ka rregullimet e veta. Për t’i gjetur, klikoni mbi një bllok. Rregullimet e tij do të shfaqen te paneli në fund të ekranit."; -/* Accessibility label for edit button to enable multi selection mode in the user's media library - Edit the post. - Editing GIF alert default action button. +/* Editing GIF alert default action button. Edits a Comment Edits the comment - Label for a button that opens the Edit Page view controller - Label for the edit post button. Tapping displays the editor. User action to edit media details. Verb, edit a comment */ "Edit" = "Përpunoni"; @@ -2654,9 +2593,6 @@ translators: Block name. %s: The localized block name */ /* Title for the edit more button section */ "Edit \"More\" button" = "Përpunoni butonin “Më Tepër”"; -/* Button that displays the media editor to the user */ -"Edit %@" = "Përpunoni %@"; - /* Blocklist Keyword Edition Title */ "Edit Blocklist Word" = "Përpunoni Fjalë Liste Bllokimesh"; @@ -2838,7 +2774,7 @@ translators: Block name. %s: The localized block name */ "Enter Full Screen" = "Kalo nën mënyrën “Sa Krejt Ekrani”"; /* Enter a custom value */ -"Enter a custom value" = "Jepni një vlerë vetajke"; +"Enter a custom value" = "Jepni një vlerë vetjake"; /* No comment provided by engineer. */ "Enter a password" = "Jepni një fjalëkalim"; @@ -2849,9 +2785,6 @@ translators: Block name. %s: The localized block name */ /* Secondary message shown when there are no domains that match the user entered text. */ "Enter different words above and we'll look for an address that matches it." = "Jepni më sipër fjalë të ndryshme dhe do të kërkojmë për një adresë që ka të tilla."; -/* Accessibility hint for edit button to enable multi selection mode in the user's media library */ -"Enter edit mode to enable multi select to delete" = "Që të aktivizoni përzgjedhje të shumëfishtë për fshirje, kaloni nën mënyrën përpunim"; - /* (placeholder) Help enter WordPress password Placeholder of a field to type a password to protect the post. */ "Enter password" = "Jepni fjalëkalim"; @@ -2870,7 +2803,7 @@ translators: Block name. %s: The localized block name */ "Enter username" = "Jepni emër përdoruesi"; /* Enter your account information for {site url}. Asks the user to enter a username and password for their self-hosted site. */ -"Enter your account information for %@." = "Jepni të dhënat tuaja të llogarisë për %@."; +"Enter your account information for %@." = "Jepni hollësi të llogarisë tuaj për %@."; /* Instruction text on the initial email address entry screen. */ "Enter your email address to log in or create a WordPress.com account." = "Që të bëni hyrjen ose të krijoni një llogari te WordPress.com, jepni adresën tuaj email."; @@ -2963,10 +2896,10 @@ translators: Block name. %s: The localized block name */ "Error occurred during scheduling" = "Ndodhi një gabim gjatë vënies në plan"; /* Register Domain - Domain contact information error message shown to indicate an error during fetching domain contact information */ -"Error occurred fetching domain contact information" = "Ndodhi një gabim gjatë sjelljes së të dhënave të kontaktit për përkatësinë"; +"Error occurred fetching domain contact information" = "Ndodhi një gabim gjatë sjelljes së hollësive të kontaktit për përkatësinë"; /* Register Domain - Domain contact information error message shown to indicate an error during fetching list of states */ -"Error occurred fetching states" = "Ndodhi një gabim teksa silleshin të dhëna të largëta"; +"Error occurred fetching states" = "Ndodhi një gabim teksa silleshin gjendje"; /* Title of error dialog when removing a site owner fails. */ "Error removing %@" = "Gabim në heqjen e %@"; @@ -3007,9 +2940,6 @@ translators: Block name. %s: The localized block name */ /* Short title telling the user they will receive a blogging reminder every day of the week. */ "Every day at %@" = "Çdo ditë, më %@"; -/* Label for the post author filter. This filter shows posts for all users on the blog. */ -"Everyone" = "Kushdo"; - /* Example story title description */ "Example story title" = "Titull shembulli shkrimi"; @@ -3019,9 +2949,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Excerpt length (words)" = "Gjatësi copëzash (fjalë)"; -/* Accessibility label for a post's excerpt in the post list. The parameter is the post excerpt. For example, \"Excerpt. This is the first paragraph.\" */ -"Excerpt. %@." = "Copëz. %@."; - /* Should be the same as the text displayed if the user clicks the (i) in Calypso. */ "Excerpts are optional hand-crafted summaries of your content." = "Copëzat janë përmbledhje opsionale lënde, hartuar me dorën tuaj."; @@ -3031,8 +2958,7 @@ translators: Block name. %s: The localized block name */ /* Accessibility Label for the exit full screen button on the full screen comment reply mode */ "Exit Full Screen" = "Dil nga mënyra “Sa Krejt Ekrani”"; -/* Accessibility indication that the current Post List style is currently Expanded. - Screen reader text to represent the expanded state of a UI control */ +/* Screen reader text to represent the expanded state of a UI control */ "Expanded" = "E zgjeruar"; /* Accessibility hint */ @@ -3082,9 +3008,6 @@ translators: Block name. %s: The localized block name */ The action failed */ "Failed" = "Dështoi"; -/* Error title when picked media cannot be imported into stories. */ -"Failed Media Export" = "Eksportimi i Medias Dështoi"; - /* Message for mark all as read success notice */ "Failed marking Notifications as read" = "S’u arrit t’u vihej shenjë Njoftimeve si të lexuara"; @@ -3283,6 +3206,12 @@ translators: Block name. %s: The localized block name */ /* An example tag used in the login prologue screens. */ "Football" = "Futboll"; +/* translators: Recommendation included in a warning related to having blocks deeply nested. */ +"For this reason, we recommend editing the block using the web editor." = "Për këtë arsye, rekomandojmë përpunimin e bllokut duke përdorur përpunuesin web."; + +/* translators: Recommendation included in a warning related to having blocks deeply nested. */ +"For this reason, we recommend editing the block using your web browser." = "Për këtë arsye, rekomandojmë përpunimin e bllokut duke përdorur shfletuesin tuaj."; + /* Register Domain - Domain contact information section header description */ "For your convenience, we have pre-filled your WordPress.com contact information. Please review to be sure it’s the correct information you want to use for this domain." = "Që ta keni më të lehtë, kemi paraplotësuar të dhëna tuajat kontakti WordPress.com. Ju lutemi, shihini, për të qenë të sigurt se janë të dhënat e sakta që doni të përdoren për këtë përkatësi."; @@ -3405,7 +3334,7 @@ translators: Block name. %s: The localized block name */ "Getting Inspired" = "Frymëzohuni"; /* Alerts the user that wpcom account information is being retrieved. */ -"Getting account information" = "Po merren të dhëna llogarie"; +"Getting account information" = "Po merren hollësi llogarie"; /* Cancel */ "Give Up" = "Lëre Fare"; @@ -3600,8 +3529,7 @@ translators: Block name. %s: The localized block name */ "Home" = "Kreu"; /* Title for setting which shows the current page assigned as a site's homepage - Title for the homepage section in site settings screen - Title of the Homepage Badge */ + Title for the homepage section in site settings screen */ "Homepage" = "Faqe Hyrëse"; /* Label for Homepage Settings site settings section @@ -3698,9 +3626,6 @@ translators: Block name. %s: The localized block name */ /* Hint for image title on image settings. */ "Image title" = "Titull figure"; -/* Accessibility label for image thumbnails in the media collection view. The parameter is the creation date of the image. */ -"Image, %@" = "Figurë, %@"; - /* Undated post time label */ "Immediately" = "Menjëherë"; @@ -3932,7 +3857,7 @@ translators: Block name. %s: The localized block name */ "Jetpack Scan found %1$d potential threats with %2$@. Please review them below and take action or tap the fix all button. We are here to help if you need us." = "Jetpack Scan gjeti %1$d kërcënime potenciale me %2$@. Ju lutemi, shqyrtojini më poshtë dhe ndërmerrni një veprim, ose prekni butonin “Zgjidhi Krejt”. Jemi këtu për t’ju ndihmuar, nëse keni nevojë për ne."; /* Description for a label when there is a single threat on the site, displays the site's title */ -"Jetpack Scan found 1 potential threat with %1$@. Please review them below and take action or tap the fix all button. We are here to help if you need us." = "Jetpack Scan gjeti 1 kërcënim potencial me %1$@. Ju lutemi, shqyrtojeni më poshtë dhe ndërrmerrni një veprim, ose prektni butonin “Zgjidhi Krejt”. Jemi këtu për t’ju ndihmuar, nëse keni nevojë për ne."; +"Jetpack Scan found 1 potential threat with %1$@. Please review them below and take action or tap the fix all button. We are here to help if you need us." = "Jetpack Scan gjeti 1 kërcënim potencial me %1$@. Ju lutemi, shqyrtojeni më poshtë dhe ndërmerrni një veprim, ose prekni butonin “Zgjidhi Krejt”. Jemi këtu për t’ju ndihmuar, nëse keni nevojë për ne."; /* Description that explains how we will fix the threat */ "Jetpack Scan will delete the affected file or directory." = "Jetpack Scan-i do të fshijë kartelën ose drejtorinë e prekur."; @@ -3990,7 +3915,7 @@ translators: Block name. %s: The localized block name */ "Jetpack will be fixing the detected active threat." = "Jetpack-u do të ndreqë kërcënimin aktiv të pikasur."; /* Footer for the Serve images from our servers setting */ -"Jetpack will optimize your images and serve them from the server location nearest to your visitors. Using our global content delivery network will boost the loading speed of your site." = "Jetpack-u do t’i optimizojë figurat tuaja dhe shërbejë ato nga vendndodhje shërbyesi më afër vizitorëve tuaj. Përdorimi i rrjetit tonë global të shpërndarjes së lëndës do të fuqizojë shpejtësinë e ngarkimit të sajtit tuaj."; +"Jetpack will optimize your images and serve them from the server location nearest to your visitors. Using our global content delivery network will boost the loading speed of your site." = "Jetpack-u do t’i bëjë optimale figurat tuaja dhe shërbejë ato nga vendndodhje shërbyesi më afër vizitorëve tuaj. Përdorimi i rrjetit tonë global të shpërndarjes së lëndës do të fuqizojë shpejtësinë e ngarkimit të sajtit tuaj."; /* Subtitle for button displaying the Automattic Work With Us web page, indicating that Automattic employees can work from anywhere in the world */ "Join From Anywhere" = "Bëhuni Pjesë Që Ngado"; @@ -4186,9 +4111,6 @@ translators: Block name. %s: The localized block name */ Settings: Comments Approval settings */ "Links in comments" = "Lidhje në komente"; -/* The accessibility label for the list style button in the Post List. */ -"List style" = "Stil liste"; - /* Title of the screen that load selected the revisions. */ "Load" = "Ngarkoje"; @@ -4204,18 +4126,12 @@ translators: Block name. %s: The localized block name */ /* Text displayed while loading the activity feed for a site */ "Loading Backups..." = "Po ngarkohen Kopjeruajtje…"; -/* Phrase to show when the user has searched for GIFs and they are being loaded. */ -"Loading GIFs..." = "Po ngarkohen GIF-e…"; - /* Menus label text displayed while menus are loading */ "Loading Menus..." = "Po ngarkohen Menu…"; /* Text displayed while loading site People. */ "Loading People..." = "Po ngarkohen Persona…"; -/* Phrase to show when the user has searched for images and they are being loaded. */ -"Loading Photos..." = "Po ngarkohen Foto…"; - /* Text displayed while loading plans details */ "Loading Plan..." = "Po ngarkohet Plani…"; @@ -4276,8 +4192,7 @@ translators: Block name. %s: The localized block name */ /* Local Services site intent topic */ "Local Services" = "Shërbime Vendore"; -/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. - Title of the Local Changes Badge */ +/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. */ "Local changes" = "Ndryshime vendore"; /* Title for alert when a generic error happened when trying to find the location of the device */ @@ -4441,7 +4356,6 @@ translators: Block name. %s: The localized block name */ "Max Video Upload Size" = "Madhësi Maksimum Ngarkimi Videosh"; /* Accessibility label for the Me button in My Site. - Label for the post author filter. This filter shows posts only authored by the current user. Me page title Noun. Title. Links to the Me screen. The accessibility value of the me tab. @@ -4449,9 +4363,7 @@ translators: Block name. %s: The localized block name */ "Me" = "Unë"; /* Noun. Title. Links to the blog's Media library. - Tab bar title for the Media tab in Media Picker The menu item to select during a guided tour. - Title for Media Library section of the app. Title for the media section in site settings screen Title label for the media settings section in the app settings */ "Media" = "Media"; @@ -4463,13 +4375,6 @@ translators: Block name. %s: The localized block name */ /* Label for size of media cache in the app. */ "Media Cache Size" = "Madhësi Fshehtine Media"; -/* Title for alert when access to media capture is not granted */ -"Media Capture" = "Krijim Mediash"; - -/* Title for alert when a generic error happened when loading media - Title for alert when access to the media library is not granted by the user */ -"Media Library" = "Mediatekë"; - /* Title for action sheet with media options. */ "Media Options" = "Mundësi Media"; @@ -4492,9 +4397,6 @@ translators: Block name. %s: The localized block name */ /* translators: %s: block title e.g: \"Paragraph\". */ "Media options" = "Mundësi media"; -/* Alert title when there is issues loading an asset to preview. */ -"Media preview failed." = "Paraparja e medias dështoi."; - /* Alert displayed to the user when multiple media items have uploaded successfully. */ "Media uploaded (%ld files)" = "Media u ngarkua (%ld kartela)"; @@ -4532,9 +4434,6 @@ translators: Block name. %s: The localized block name */ Label for the share message field on the post settings. */ "Message" = "Mesazh"; -/* Title of section containing image / video metadata such as size and file type */ -"Metadata" = "Tejtëdhëna"; - /* Option to select the Microsft Outlook app when logging in with magic links */ "Microsoft Outlook" = "Microsoft Outlook"; @@ -4554,13 +4453,11 @@ translators: Block name. %s: The localized block name */ "Months and Years" = "Muaj dhe Vite"; /* Accessibility label for more button in dashboard quick start card. - Accessibility label for the More button in Page List. Accessibility label for the More button in Post List (compact view). Accessibility label for the More button on formatting toolbar. Accessibility label for the More button on Reader Cell Accessibility label for the More button on Reader's post details - Action button to display more available options - Label for the more post button. Tapping displays an action sheet with post options. */ + Action button to display more available options */ "More" = "Më tepër"; /* Action button to display more available options @@ -4618,15 +4515,8 @@ translators: Block name. %s: The localized block name */ /* Screen reader text for button that will move the menu item */ "Move menu item" = "Lëvizni element menuje"; -/* Label for a button that moves a page to the draft folder - Label for an option that moves a post to the draft folder */ -"Move to Draft" = "Shpjere te Skica"; - -/* Label for a button that moves a page to the trash folder - Label for a option that moves a post to the trash folder - Title for button on the comment details page that moves the comment to trash when tapped. +/* Title for button on the comment details page that moves the comment to trash when tapped. Trash option in the trash confirmation alert. - Trash option in the trash page confirmation alert. Trashes the comment */ "Move to Trash" = "Shpjere te Hedhurinat"; @@ -4643,7 +4533,7 @@ translators: Block name. %s: The localized block name */ "Moves the comment to the Trash." = "Shpjere komentin te Hedhurinat."; /* Example post title used in the login prologue screens. */ -"Museums to See In London" = "Muzeume Për T’u Parë në Londër"; +"Museums to See In London" = "Muze Për T’u Parë në Londër"; /* An example tag used in the login prologue screens. Music site intent topic */ @@ -4658,7 +4548,8 @@ translators: Block name. %s: The localized block name */ Title of My Site tab */ "My Site" = "Sajti Im"; -/* Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ +/* Title for site picker screen. + Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ "My Sites" = "Sajtet e Mi"; /* Siri Suggestion to open My Sites */ @@ -4908,9 +4799,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title for the view when there aren't any Activities to display in the Activity Log for a given filter. */ "No matching events found." = "S’u gjetën veprimtari me përputhje."; -/* Message displayed when no results are returned from a media library search. Should match Calypso. - Phrase to show when the user search for images but there are no result to show. - Phrase to show when the user searches for GIFs but there are no result to show. */ +/* Message displayed when no results are returned from a media library search. Should match Calypso. */ "No media matching your search" = "S’ka media që përputhet me kërkimin tuaj"; /* Menus text shown when no menus were available for loading the Menus editor. */ @@ -4928,8 +4817,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Displayed in the Notifications Tab as a title, when there are no notifications */ "No notifications yet" = "Ende pa njoftime"; -/* Displayed when the user is searching the pages list and there are no matching pages - Text displayed when there's no matching with the text search */ +/* Text displayed when there's no matching with the text search */ "No pages matching your search" = "Kërkimi juaj s’ka përputhje me ndonjë faqe"; /* Text displayed when search for plugins returns no results */ @@ -4950,9 +4838,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Message shown whent the reader finds no posts for the chosen tag */ "No posts have been made recently with this tag." = "S’ka postime të bëra me këtë etiketë së fundi."; -/* Displayed when the user is searching the posts list and there are no matching posts */ -"No posts matching your search" = "Kërkimi juaj s’ka përputhje me ndonjë postim"; - /* Accessibility value for a Stats' Posting Activity Month if there are no posts. */ "No posts." = "Pa postime."; @@ -5053,9 +4938,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* A message title */ "Nothing liked yet" = "Ende pa pëlqyer gjë"; -/* Default message for empty media picker */ -"Nothing to show" = "S’ka gjë për shfaqje"; - /* Notifications Details Accessibility Identifier */ "Notification Details Table" = "Tabelë Hollësish Njoftimesh"; @@ -5115,7 +4997,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Alert dismissal title Button title. Acknowledges a prompt. Button title. An acknowledgement of the message displayed in a prompt. - Confirmation of action Default action Dismisses the alert Marks all notifications as read. @@ -5177,9 +5058,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* No comment provided by engineer. */ "Only show excerpt" = "Shfaq vetëm copëz"; -/* Message telling the user that they've only enabled limited photo library permissions for the app. */ -"Only the selected photos you've given access to are available." = "Mund të përdoren vetëm foto për të cilat keni lejuar përdorim."; - /* An informal exclaimation that means `something went wrong`. Title for the view when there's an error loading a comment. Title for the view when there's an error loading Activity Log @@ -5214,9 +5092,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title of a button that opens the apps settings in the system Settings.app */ "Open Settings" = "Hap Rregullimet"; -/* Editor button to swich the media picker from quick mode to full picker */ -"Open full media picker" = "Hap zgjedhësin e medias të plotë"; - /* No comment provided by engineer. */ "Open in Safari" = "Hape në Safari"; @@ -5256,6 +5131,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Divider on initial auth view separating auth options. */ "Or" = "Ose"; +/* Instruction text for other forms of two-factor auth methods. */ +"Or choose another form of authentication." = "Ose zgjidhni një tjetër metodë mirëfilltësimi."; + /* Label for button to log in using site address. Underscores _..._ denote underline. */ "Or log in by _entering your site address_." = "Ose hyni _duke dhënë adresën e sajtit tuaj_."; @@ -5314,15 +5192,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title shown when selecting a post type of Page from the Share Extension. */ "Page" = "Faqe"; -/* Prompts the user that a restored page was moved to the drafts list. */ -"Page Restored to Drafts" = "Faqja u Rikthye te Skicat"; - -/* Prompts the user that a restored page was moved to the published list. */ -"Page Restored to Published" = "Faqja u Rikthye te të Botuarat"; - -/* Prompts the user that a restored page was moved to the scheduled list. */ -"Page Restored to Scheduled" = "Faqja u Rikthye te të Planifikuarat"; - /* Name of the button to open the page settings The title of the Page Settings screen. */ "Page Settings" = "Rregullime Faqeje"; @@ -5339,9 +5208,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of notification displayed when a page has failed to upload. */ "Page failed to upload" = "Ngarkimi i faqes dështoi"; -/* A short message explaining that a page was moved to the trash bin. */ -"Page moved to trash." = "Faqja u shpu te hedhurinat."; - /* Title of notification displayed when a page has been successfully saved as a draft. */ "Page pending review" = "Faqe në pritje të shqyrtimit"; @@ -5410,8 +5276,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title of pending Comments filter. */ "Pending" = "Pezull"; -/* Name for the status of a post pending review. - Title of the Pending Review Badge */ +/* Name for the status of a post pending review. */ "Pending review" = "Në pritje të shqyrtimit"; /* Noun. Title of the people management feature. @@ -5440,12 +5305,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Photography site intent topic */ "Photography" = "Fotografi"; -/* Tab bar title for the Photos tab in Media Picker */ -"Photos" = "Foto"; - -/* Subtitle for placeholder in Free Photos. The company name 'Pexels' should always be written as it is. */ -"Photos provided by Pexels" = "Foto të furnizuara nga Pexels"; - /* Title for selecting a new username in the site creation flow. */ "Pick username" = "Zgjidhni emër përdoruesi"; @@ -5538,7 +5397,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Please enter the password for your WordPress.com account to log in with your Apple ID." = "Ju lutemi, jepni fjalëkalimin për llogarinë tuaj te WordPress.com që të bëhet hyrja me ID-në tuaj Apple."; /* Instruction text on the two-factor screen. */ -"Please enter the verification code from your authenticator app, or tap the link below to receive a code via SMS." = "Ju lutemi, jepni kodin e verifikimit prej aplikacionit tuaj të mirëfilltësimeve, ose prekni lidhjen më poshtë që të merrni një kod me SMS."; +"Please enter the verification code from your authenticator app." = "Ju lutemi, jepni kodin e verifikimit prej aplikacionit tuaj të mirëfilltësimeve."; /* Popup message to ask for user credentials (fields shown below). */ "Please enter your credentials" = "Ju lutemi, jepni kredencialet tuaja"; @@ -5633,15 +5492,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ The post formats available for the post. Should be the same as in core WP. */ "Post Format" = "Format Postimesh"; -/* Prompts the user that a restored post was moved to the drafts list. */ -"Post Restored to Drafts" = "Postimi u Rikthye te Skicat"; - -/* Prompts the user that a restored post was moved to the published list. */ -"Post Restored to Published" = "Postimi u Rikthye te të Botuarit"; - -/* Prompts the user that a restored post was moved to the scheduled list. */ -"Post Restored to Scheduled" = "Postimi u Riktheu te të Vënët Në Plan"; - /* Name of the button to open the post settings The title of the Post Settings screen. */ "Post Settings" = "Rregullime Postimi"; @@ -5661,9 +5511,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of notification displayed when a post has failed to upload. */ "Post failed to upload" = "Ngarkimi i postimit dështoi"; -/* A short message explaining that a post was moved to the trash bin. */ -"Post moved to trash." = "Postimi u shpu te hedhurinat."; - /* Title of notification displayed when a post has been successfully saved as a draft. */ "Post pending review" = "Postim në pritje të shqyrtimit"; @@ -5722,9 +5569,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Period Stats 'Posts and Pages' header */ "Posts and Pages" = "Postime dhe Faqe"; -/* Title of the Posts Page Badge */ -"Posts page" = "Faqe postimesh"; - /* Message informing the user that their static homepage for posts was set successfully */ "Posts page successfully updated" = "Faqja Postime u përditësua me sukses"; @@ -5737,9 +5581,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* A message explaining the Posts I Like feature in the reader */ "Posts that you like will appear here." = "Postimet që pëlqeni do të shfaqen këtu."; -/* Subtitle for placeholder in Tenor picker. `The company name 'Tenor' should always be written as it is. */ -"Powered by Tenor" = "Bazuar në Tenor"; - /* Browse premium themes selection title */ "Premium" = "Me pagesë"; @@ -5758,18 +5599,12 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title for screen to preview a static content. */ "Preview" = "Paraparje"; -/* Action for Media Picker to preview the selected media items. The argument in the string represents the number of elements (as numeric digits) selected */ -"Preview %@" = "Parashiheni %@"; - /* Title for web preview device switching button */ "Preview Device" = "Pajisje Paraparjeje"; /* Title on display preview error */ "Preview Unavailable" = "S’bëhet dot Paraparje"; -/* Accessibility label for media item preview for user's viewing an item in their media library */ -"Preview media" = "Bëni paraparje të medias"; - /* No comment provided by engineer. */ "Preview page" = "Parashihni faqen"; @@ -5816,8 +5651,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Privacy notice for California users" = "Shënim mbi privatësinë për përdorues në Kalifornia"; /* Name for the status of a post that is marked private. - Privacy setting for posts set to 'Private'. Should be the same as in core WP. - Title of the Private Badge */ + Privacy setting for posts set to 'Private'. Should be the same as in core WP. */ "Private" = "Privat"; /* No comment provided by engineer. */ @@ -5867,12 +5701,10 @@ translators: %s: Select control button label e.g. \"Button width\" */ Label for the publish date button. */ "Publish Date" = "Datë Botimi"; -/* A short phrase indicating a post is due to be immedately published. - Label for a button that moves a page to the published folder, publishing with the current date/time. */ +/* A short phrase indicating a post is due to be immedately published. */ "Publish Immediately" = "Botoje Menjëherë"; /* Label for a button that publishes the post - Label for an option that moves a publishes a post immediately Title of button allowing the user to immediately publish the post they are editing. */ "Publish Now" = "Botoje Që Tani"; @@ -5890,8 +5722,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Name for the status of a published post. Period Stats 'Published' header - Title of the published filter. This filter shows a list of posts that the user has published. - Title of the published header in search list. */ + Title of the published filter. This filter shows a list of posts that the user has published. */ "Published" = "U botua"; /* Precedes the name of the blog just posted on */ @@ -6033,8 +5864,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of the completion screen of the Blogging Reminders Settings screen when the reminders are removed. */ "Reminders removed" = "Kujtuesit u hoqën"; -/* Add asset to media picker list - Alert button to confirm a plugin to be removed +/* Alert button to confirm a plugin to be removed Button label when removing a blog Label for the Remove Feature Image icon. Tapping will show a confirmation screen for removing the feature image from the post. Remove Action @@ -6187,9 +6017,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of secondary button on alert prompting verify their accounts while attempting to publish */ "Resend" = "Ridërgoje"; -/* Title of the reset button */ -"Reset" = "Riktheje te parazgjedhjet"; - /* Accessibility label for the reset activity type button */ "Reset Activity Type filter" = "Zero filtër Lloj Veprimtarish"; @@ -6244,12 +6071,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ Button title, displayed when media has failed to upload. Allows the user to try the upload again. Button title. Retries uploading a post. If a user taps the button with this label, the action that evinced this error view will be retried. - Label for a button that attempts to re-upload a page that previously failed to upload. - Label for the retry post upload button. Tapping attempts to upload the post again. Opens the media library . Retries the upload of a user's post. Retry updating User's Role - Retry uploading the post. Retry. Action Retry. Verb – retry a failed media upload. The Jetpack view button title used when an error occurred @@ -6261,9 +6085,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Button title that triggers a scan */ "Retry Scan" = "Riprovo Skanim"; -/* User action to retry media upload. */ -"Retry Upload" = "Riprovo Ngarkimin"; - /* User action to retry all failed media uploads. */ "Retry all" = "Riprovoji krejt"; @@ -6361,9 +6182,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Accessibility label for the 'Save Post' button when a post has been saved. */ "Saved Post" = "Postimi u Ruajt"; -/* Text displayed in HUD when a media item's metadata (title, etc) is saved successfully. */ -"Saved!" = "U ruajt!"; - /* Accessibility hint for the 'Save Post' button. */ "Saves this post for later." = "E ruan këtë postim për më vonë."; @@ -6374,7 +6192,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Saving post…" = "Po ruhet postimi…"; /* Menus save button title while it is saving a Menu. - Text displayed in HUD while a media item's metadata (title, etc) is being saved. Text displayed in HUD while a post is being saved as a draft. */ "Saving..." = "Po ruhet…"; @@ -6465,21 +6282,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* No comment provided by engineer. */ "Search or type URL" = "Kërkoni ose shtypni URL"; -/* Text displayed when the search controller will be presented */ -"Search pages" = "Faqe kërkimesh"; - -/* Text displayed when the search controller will be presented */ -"Search posts" = "Kërkoni te postime"; - /* No comment provided by engineer. */ "Search settings" = "Rregullime kërkimi"; -/* Title for placeholder in Tenor picker */ -"Search to find GIFs to add to your Media Library!" = "Kërkoni për të gjetur GIF-e që t’i shtoni te Mediateka juaj!"; - -/* Title for placeholder in Free Photos */ -"Search to find free photos to add to your Media Library!" = "Kërkoni për të gjetur foto të lira që t’i shtoni te Mediateka juaj!"; - /* Menus search bar placeholder text. */ "Search..." = "Kërkoni…"; @@ -6550,9 +6355,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Register Domain - Domain contact information field placeholder for Country */ "Select Country" = "Përzgjidhni Vend"; -/* Title of button that allows the user to select more photos to access within the app */ -"Select More" = "Përzgjidhni Më Tepër"; - /* Blog Picker's Title */ "Select Site" = "Përzgjidhni Sajt"; @@ -6574,9 +6376,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Register domain - Title for the Choose domain button of Suggested domains screen */ "Select domain" = "Përzgjidhni një përkatësi"; -/* Accessibility hint for actions when displaying media items. */ -"Select media." = "Përzgjidhni media."; - /* Accessibility label for selecting paragraph style button on formatting toolbar. */ "Select paragraph style" = "Përzgjidhni stil paragrafi"; @@ -6680,19 +6479,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label for connected service in Publicize stat. */ "Service" = "Shërbim"; -/* Label for a button that opens the Set Parent options view controller - Navigation title displayed on the navigation bar */ +/* Navigation title displayed on the navigation bar */ "Set Parent" = "Caktojeni Mëmë"; /* No comment provided by engineer. */ "Set as Featured Image" = "Vëre si Figurë të Zgjedhur"; -/* Label for a button that sets the selected page as the site's Homepage */ -"Set as Homepage" = "Caktojeni si Faqen hyrëse"; - -/* Label for a button that sets the selected page as the site's Posts page */ -"Set as Posts Page" = "Caktojeni Faqe Postimesh"; - /* Notice confirming that an image has been set as the post's featured image. */ "Set as featured image" = "Vëre si figurë të zgjedhur"; @@ -6736,7 +6528,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Accessibility label for share buttons in nav bars Button label to share a post Button label to share a web page - Share the post. Shares the comment URL Spoken accessibility label Title for a button that allows the user to share their answer to the prompt. @@ -6756,7 +6547,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "Share comment" = "Ndajeni komentin me të tjerë"; /* Informational text for Collect Information setting */ -"Share information with our analytics tool about your use of services while logged in to your WordPress.com account." = "Ndani me mjetin tuaj të analizave të dhëna rreth përdorimit tuaj të shërbimeve, teksa jeni i futur në llogarinë tuaj WordPress.com."; +"Share information with our analytics tool about your use of services while logged in to your WordPress.com account." = "Ndani me mjetin tonë të analizave të dhëna rreth përdorimit tuaj të shërbimeve, teksa jeni i futur në llogarinë tuaj WordPress.com."; /* Title. A call to action to share an invite link. */ "Share invite link" = "Jepuni lidhje ftese"; @@ -7119,8 +6910,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Name of setting configured when a site uses a static page as its homepage */ "Static Homepage" = "Faqe Hyrëse Statike"; -/* Label for post stats option. Tapping displays statistics for a post. - Noun. Abbreviation of Statistics. Name of the Stats feature +/* Noun. Abbreviation of Statistics. Name of the Stats feature Noun. Abbv. of Statistics. Links to a blog's Stats screen. Stats 3D Touch Shortcut Stats window title @@ -7151,9 +6941,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label text that defines a post marked as sticky */ "Sticky" = "Ngjitës"; -/* Accessibility label for a sticky post in the post list. */ -"Sticky." = "Ngjitës."; - /* User action to stop upload. */ "Stop upload" = "Ndale ngarkimin"; @@ -7210,7 +6997,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ Theme Support action title */ "Support" = "Asistencë"; -/* Button used to switch site */ +/* Title for back button that leads to the site picker screen. */ "Switch Site" = "Këmbe Sajt"; /* Switches the Editor to HTML Mode */ @@ -7298,9 +7085,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label explaining why users might want to add tags. */ "Tags help tell readers what a post is about. Separate different tags with commas." = "Etiketat ndihmojnë t’u tregohet lexuesve se për çfarë është një postim. Etiketat e ndryshme ndajini me presje."; -/* Menu option for taking an image or video with the device's camera. */ -"Take Photo or Video" = "Bëni Foto ose Video"; - /* No comment provided by engineer. */ "Take a Photo" = "Bëni një Foto"; @@ -7371,12 +7155,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Accessibility hint */ "Tap to select the previous period" = "Prekeni që të përzgjidhni periudhën e mëparshme"; -/* Accessibility hint for button used to switch site */ -"Tap to switch to another site, or add a new site" = "Prekeni që të kalohet te një sajt tjetër, ose për të shtuar një sajt të ri"; - -/* Accessibility hint for media item preview for user's viewing an item in their media library */ -"Tap to view media in full screen" = "Prekni që të shihni median sa krejt ekrani"; - /* Accessibility hint for a button that opens a new view with more details. */ "Tap to view more details." = "Prekeni që të shihni më tepër hollësi."; @@ -7422,10 +7200,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* No comment provided by engineer. */ "Text formatting controls are located within the toolbar positioned above the keyboard while editing a text block" = "Kontrollet për formatim teksti gjenden brenda panelit të vendosur mbi tastierën, kur përpunohet një bllok teksti"; -/* Button title - The button's title text to send a 2FA code via SMS text message. */ +/* Button title */ "Text me a code instead" = "Më mirë më dërgoni kod me tekst"; +/* The button's title text to send a 2FA code via SMS text message. */ +"Text me a code via SMS" = "Dërgomëni një kod përmes SMS-je"; + /* Message of alert when theme activation succeeds */ "Thanks for choosing %@ by %@" = "Faleminderit që zgjodhët %1$@ nga %2$@"; @@ -7453,9 +7233,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Error message shown to a user who is trying to share to Facebook but does not have any available Facebook Pages. */ "The Facebook connection cannot find any Pages. Publicize cannot connect to Facebook Profiles, only published Pages." = "Lidhja Facebook s’gjen dot ndonjë Faqe. Publicize s’mund të lidhet me Profile Facebook, vetëm me Faqe të botuara."; -/* Message shown when a GIF failed to load while trying to add it to the Media library. */ -"The GIF could not be added to the Media Library." = "GIF-i s’u shtua dot te Mediateka."; - /* Description shown when a user logs in with Google but no matching WordPress.com account is found */ "The Google account \"%@\" doesn't match any account on WordPress.com" = "Llogaria Google \"%@\" s’ka përputhje me ndonjë llogari te WordPress.com"; @@ -7545,7 +7322,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "The server returned an empty response. This usually means you need to increase the memory limit for your site." = "Shërbyesi ktheu një përgjigje të zbrazët. Zakonisht kjo do të thotë që lypset të rritni kufirin e kujtesës për sajtin tuaj."; /* Message expliaining that the specified site will no longer appear in the user's reader. The '%@' characters are a placeholder for the title of the site. */ -"The site %@ will no longer appear in your reader. Tap to undo." = "Sajti %@ s’do të shfaqet më në lezuesin tuaj. Prekeni që të zhbëhet."; +"The site %@ will no longer appear in your reader. Tap to undo." = "Sajti %@ s’do të shfaqet më në lexuesin tuaj. Prekeni që të zhbëhet."; /* No comment provided by engineer. */ "The site address must be shorter than 64 characters." = "Adresa e sajtit duhet të jetë më e shkurtër se 64 shenja."; @@ -7583,7 +7360,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Error message shown when user attempts to remove the site owner. */ "The user you are trying to remove is the owner of this site. Please contact support for assistance." = "Përdoruesi që po provoni të hiqni është i zoti i këtij sajti. Ju lutemi, lidhuni me ata të asistencës."; -/* Error message informing a user about an invalid password. */ +/* No comment provided by engineer. */ "The username or password stored in the app may be out of date. Please re-enter your password in the settings and try again." = "Emri i përdoruesit ose fjalëkalimi i depozituar te aplikacioni mund të jenë të papërditësuar. Ju lutemi, jepeni fjalëkalimin tuaj te rregullimet dhe riprovoni."; /* Message shown when a video failed to load while trying to add it to the Media library. */ @@ -7651,11 +7428,8 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* A short error message letting the user know about a problem displaying a post. */ "There was a problem displaying this post." = "Pati një problem me shfaqjen e këtij postimi."; -/* Error message displayed when the Media Library is unable to load a full sized preview of an item. */ -"There was a problem loading the media item." = "Pati një problem me ngarkimin e objektit media."; - /* The loading view subtitle displayed when an error occurred */ -"There was a problem loading your data, refresh your page to try again." = "Pati një problem me ngarkimin e të dhënave tuaja, rifreskoni faqen tuaj që të riprovoni."; +"There was a problem loading your data, refresh your page to try again." = "Pati një problem me ngarkimin e të dhënave tuaja, rifreskoni faqen tuaj që të riprovoni."; /* Error message informing the user that there was a problem clearing the block on site preventing its posts from displaying in the reader. */ "There was a problem removing the block for specified site." = "Pati një problem me heqjen e bllokut për sajtin e treguar."; @@ -7666,9 +7440,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Explaining to the user there was an error trying to obtain the current location of the user. */ "There was a problem when trying to access your location. Please try again later." = "Pati një gabim teksa bëheshin përpjekje për të hyrë te vendi juaj. Ju lutemi, riprovoni më vonë."; -/* Explaining to the user there was an generic error accesing media. */ -"There was a problem when trying to access your media. Please try again later." = "Pati një problem kur po provohej të hyhej në median tuaj. Ju lutemi, riprovoni më vonë."; - /* Message for stories unknown error. */ "There was a problem with the Stories editor. If the problem persists you can contact us via the Me > Help & Support screen." = "Pati një problem me përpunuesin e Shkrimeve. Nëse problemi vazhdon, mund të lidheni me ne përmes skenës Unë > Ndihmë & Asistencë."; @@ -7739,9 +7510,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* A description informing the user in order to proceed with this feature we will need camera permissions, and how to enable it. */ "This app needs permission to access the Camera to scan login codes, tap on the Open Settings button to enable it." = "Këtij aplikacioni i duhen leje për të përdorur Kamerën, që të skanojë kode hyrjeje, prekni mbi butonin Hap Rregullimet për ta lejuar."; -/* Explaining to the user why the app needs access to the device media library. */ -"This app needs permission to access your device media library in order to add photos and\/or video to your posts. Please change the privacy settings if you wish to allow this." = "Që të mund të shtojë foto dhe\/ose video në postimet tuaja, ky aplikacion lyp leje hyrjeje në mediatekën e pajisjes tuaj. Nëse doni ta lejoni këtë, ju lutemi, ndryshoni rregullimet mbi privatësinë."; - /* No comment provided by engineer. */ "This color combination may be hard for people to read. Try using a brighter background color and\/or a darker text color." = "Kjo ndërthurje ngjyrash mund të jetë e vështirë për t’u lexuar nga njerëzit. Provoni të përdorni një ngjyrë sfondi më të ndritshme dhe\/ose një ngjyrë më të errët teksti."; @@ -7851,6 +7619,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Description of a Quick Start Tour */ "Time to finish setting up your site! Our checklist walks you through the next steps." = "Erdhi koha të përfundohet rregullimi i sajtit tuaj! Lista jonë e hapave ju udhëheq nëpër ata vijuesit."; +/* Error when the uses takes more than 1 minute to submit a security key. */ +"Time's up, but don't worry, your security is our priority. Please try again!" = "Po mbaron koha, por mos u bëni merak, siguria juaj është përparësia jonë. Ju lutemi, riprovoni!"; + /* WordPress.com Marketing Footer Text */ "Tips for getting the most out of WordPress.com." = "Këshilla për të përfituar maksimumin nga WordPress.com."; @@ -7974,24 +7745,20 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title for the traffic section in site settings screen */ "Traffic" = "Trafik"; +/* Describes a domain that was transferred from elsewhere to wordpress.com */ +"Transferred Domain" = "Përkatësi e Shpërngulur"; + /* translators: %s: block title e.g: \"Paragraph\". */ "Transform %s to" = "Shndërroje %s në"; /* No comment provided by engineer. */ "Transform block…" = "Shndërro bllokun…"; -/* Accessibility label for trash button to delete items from the user's media library - Accessibility label for trash buttons in nav bars +/* Accessibility label for trash buttons in nav bars Trashes a comment Trashes the comment */ "Trash" = "Në hedhurina"; -/* Accessibility hint for trash button to delete items from the user's media library */ -"Trash selected media" = "Shpjere te hedhurinat median e përzgjedhur"; - -/* Title of the trash page confirmation alert. */ -"Trash this page?" = "Të shpihet në hedhurina kjo faqe?"; - /* Title of the trash confirmation alert. */ "Trash this post?" = "Të shpihet ky postim te hedhurinat?"; @@ -8109,9 +7876,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Shown when a user logs in with Google but it subsequently fails to work as login to WordPress.com */ "Unable To Connect" = "S’arrihet të Lidhet"; -/* An error message. */ -"Unable to Connect" = "S’arrihet të Lidhet"; - /* Title for stories unknown error. */ "Unable to Create Stories Editor" = "S’arrihet të Krijohet Përpunues Shkrimesh"; @@ -8127,9 +7891,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* An error message shown when there is an issue creating new invite links. */ "Unable to create new invite links." = "S’arrihet të krijohen lidhje të reja ftese."; -/* Text displayed in HUD if there was an error attempting to delete a group of media items. */ -"Unable to delete all media items." = "S’arrihet të fshihen krejt objektet media."; - /* Text displayed in HUD if there was an error attempting to delete a media item. */ "Unable to delete media item." = "S’arrihet të fshihet objekt media."; @@ -8193,12 +7954,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Message displayed when sharing a link to the downloadable backup fails. */ "Unable to share link" = "S’arrihet t’u jepet lidhja të tjerëve"; -/* Message that appears when a user tries to trash a page while their device is offline. */ -"Unable to trash pages while offline. Please try again later." = "S’arrihet të shpihen faqe te hedhurina, ndërkohë që jeni i palidhur. Ju lutemi, riprovoni më vonë."; - -/* Message that appears when a user tries to trash a post while their device is offline. */ -"Unable to trash posts while offline. Please try again later." = "S’arrihet të shpihen postime te hedhurina ndërkohë që jeni i palidhur. Ju lutemi, riprovoni më vonë."; - /* Notice title when turning site notifications off fails. */ "Unable to turn off site notifications" = "S’arrihet të çaktivizohen njoftime sajti"; @@ -8271,8 +8026,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ Button title. Reverts the previous notification operation Revert an operation Revert enabling notification after successfully subcribing to the comments for the post. - The title of an 'undo' button. Tapping the button moves a trashed page out of the trash folder. - The title of an 'undo' button. Tapping the button moves a trashed post out of the trash folder. Undo action */ "Undo" = "Zhbëje"; @@ -8315,9 +8068,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title for Unknown HTML Editor */ "Unknown HTML" = "HTML e panjohur"; -/* Label to use when creation date from media asset is not know. */ -"Unknown creation date" = "Datë krijimi e panjohur"; - /* No comment provided by engineer. */ "Unknown error" = "Gabim i panjohur"; @@ -8352,7 +8102,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "Unsupported" = "I pambuluar"; /* Title for stories unsupported device error. */ -"Unsupported Device" = "Pajisje e Pammbuluar"; +"Unsupported Device" = "Pajisje e Pambuluar"; /* Label for an untitled post in the revision browser */ "Untitled" = "Pa titull"; @@ -8483,6 +8233,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of a row displayed on the debug screen used to configure the sandbox store use in the App. */ "Use Sandbox Store" = "Përdor Shitore Bankëprovë"; +/* The button's title text to use a security key. */ +"Use a security key" = "Përdorni një kyç sigurie"; + /* Option to enable the block editor for new posts */ "Use block editor" = "Përdorni përpunuesin me blloqe"; @@ -8558,15 +8311,10 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of an alert informing users that the video they are trying to select is not allowed. */ "Video not uploaded" = "S’u ngarkua video"; -/* Accessibility label for video thumbnails in the media collection view. The parameter is the creation date of the video. */ -"Video, %@" = "Video, %@"; - /* Period Stats 'Videos' header */ "Videos" = "Video"; /* Button title. Displays a summary / sharing screen for a specific post. - Label for a button that opens the page when tapped. - Label for the view post button. Tapping displays the post as it appears on the web. Opens the post epilogue screen to allow sharing / viewing of a post. Theme View action title Verb. The screen title shown when viewing a post inside the app. */ @@ -8681,6 +8429,10 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Message shown on screen while waiting for Google to finish its signup process. */ "Waiting for Google to complete…" = "Po pritet që të mbarojë punë Google…"; +/* Text while the webauthn signature is being verified + Text while waiting for a security key challenge */ +"Waiting for security key" = "Po pritet për kyç sigurie"; + /* View title during the Google auth process. */ "Waiting..." = "Po pritet…"; @@ -9055,6 +8807,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* An error message shown when a wpcom user provides the wrong password. */ "Whoops, something went wrong and we couldn't log you in. Please try again!" = "Hëm, diç shkoi ters dhe s’bëmë dot futjen tuaj. Ju lutemi, riprovoni!"; +/* Generic error on the 2FA screen */ +"Whoops, something went wrong. Please try again!" = "Hëm, diç shkoi ters. Ju lutemi, riprovoni!"; + +/* Error when the uses chooses an invalid security key on the 2FA screen. */ +"Whoops, that security key does not seem valid. Please try again with another one" = "Oh, ai kyç sigurie nuk duket i vlefshëm. Ju lutemi, riprovoni me një tjetër"; + /* Error message shown when an incorrect two factor code is provided. */ "Whoops, that's not a valid two-factor verification code. Double-check your code and try again!" = "Hëm, ky s’është kod i vlefshëm mirëfilltësimi dyfaktorësh. Kontrolloni sërish kodin tuaj dhe riprovoni!"; @@ -9082,9 +8840,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Siri Suggestion to open Support */ "WordPress Help" = "Ndihmë WordPress"; -/* Name for the WordPress Media Library */ -"WordPress Media" = "Media WordPress"; - /* Accessibility label for selecting an image or video from the user's WordPress media library on formatting toolbar. */ "WordPress Media Library" = "Mediatekë WordPress"; @@ -9265,7 +9020,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "You can update this any time via My Site > Site Settings" = "Këtë mund ta përditësoni në çfarëdo kohe që nga Sajti Im > Rregullime Sajti"; /* No comment provided by engineer. */ -"You cannot use that email address to signup. We are having problems with them blocking some of our email. Please use another email provider." = "S’mund të përdorni për regjistrim atë adresë email. Kemi probleme me ta, se bllokojnë disa nga email-et tanë. Ju lutemi, përdorni një shërbims tjetër email-esh."; +"You cannot use that email address to signup. We are having problems with them blocking some of our email. Please use another email provider." = "S’mund të përdorni për regjistrim atë adresë email. Kemi probleme me ta, se bllokojnë disa nga email-et tanë. Ju lutemi, përdorni një shërbim tjetër email-esh."; /* Displayed when the user views drafts in the pages list and there are no pages */ "You don't have any draft pages" = "S’keni ndonjë faqe skicë"; @@ -9339,7 +9094,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "You must be signed in to a WordPress.com account to perform this action." = "Që të mund të kryeni këtë veprim, duhet të jeni i futur në një llogari WordPress.com."; /* Body of alert prompting users to verify their accounts while attempting to publish */ -"You need to verify your account before you can publish a post.\nDon’t worry, your post is safe and will be saved as a draft." = "Lypset të verifikoni llogarinë tuaj përpara se të mund të botoni një postim.\nMos u shqetësoni, postimi juaj është i parrezikuar dhe do të ruhet si një skicë."; +"You need to verify your account before you can publish a post.\nDon’t worry, your post is safe and will be saved as a draft." = "Lypset të verifikoni llogarinë tuaj përpara se të mund të botoni një postim.\nMos u shqetësoni, postimi juaj është i parrezik dhe do të ruhet si një skicë."; /* Example notification content displayed on the Enable Notifications prompt that is personalized based on a users selection. Words marked between * characters will be displayed as bold text. */ "You received *50 likes* on your comment" = "Patët *50 pëlqime* të komentit tuaj"; @@ -9349,7 +9104,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Message displayed in popup when user has the option to load unsaved changes. is a placeholder for a new line, and the two %@ are placeholders for the date of last save on this device, and date of last autosave on another device, respectively. */ -"You recently made changes to this post but didn't save them. Choose a version to load:\n\nFrom this device\nSaved on %@\n\nFrom another device\nSaved on %@\n" = "Keni bërë së fundi ndryshime te ky postim, por s’i ruajtët. Zgjidhni një version për ngarkim:\n\nPrej kësaj pajisje\nRuajtur më %1$@\n\nNga pajisje tjetër\nRuajtur mën %2$@\n"; +"You recently made changes to this post but didn't save them. Choose a version to load:\n\nFrom this device\nSaved on %@\n\nFrom another device\nSaved on %@\n" = "Keni bërë së fundi ndryshime te ky postim, por s’i ruajtët. Zgjidhni një version për ngarkim:\n\nPrej kësaj pajisje\nRuajtur më %1$@\n\nNga pajisje tjetër\nRuajtur më %2$@\n"; /* Informs that the user has replied to this comment. Notification text - below a comment notification detail @@ -9399,9 +9154,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Share extension error dialog text. */ "Your account does not have permission to upload media to this site. The Site Administrator can change these permissions." = "Llogaria juaj s’ka leje të ngarkojë media në këtë sajt. Këto leje mund t’i ndryshojë Përgjegjësi i Sajtit."; -/* Explaining to the user why the app needs access to the device media library. */ -"Your app is not authorized to access media library due to active restrictions such as parental controls. Please check your parental control settings in this device." = "Aplikacioni juaj s’është i autorizuar të përdorë mediatekën, për shkak kufizimesh aktive, të tilla si kontrolli prindëror. Ju lutemi, kontrolloni rregullimet mbi kontrollin prindëror në këtë pajisje."; - /* Title for the Jetpack Backup Complete message. */ "Your backup is now available for download" = "Kopjeruajtja juaj tani është gati për shkarkim"; @@ -9420,9 +9172,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of the site address section in the Domains Dashboard. */ "Your free WordPress.com address is" = "Adresa juaj falas WordPress.com është"; -/* Error message when picked media cannot be imported into stories. */ -"Your media could not be exported. If the problem persists you can contact us via the Me > Help & Support screen." = "Media juaj s’u eksportua dot. Nëse problemi vazhdon, mund të lidheni me ne që nga skena Unë > Ndihmë & Asistencë."; - /* Details about recently acquired domain on domain credit redemption success screen */ "Your new domain %@ is being set up. It may take up to 30 minutes for your domain to start working." = "Përkatësia juaj e re %@ po ujdiset. Mund të duhen deri në 30 minuta që përkatësia juaj të fillojë të funksionojë."; @@ -9546,8 +9295,38 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* This is the string we display when prompting the user to review the WordPress app */ "appRatings.wordpress.prompt" = "Si ju duket WordPress-i?"; -/* Label displayed on audio media items. */ -"audio" = "audio"; +/* Option to enable the optimization of images when uploading. */ +"appSettings.media.imageOptimizationRow" = "Optimizo Figurat"; + +/* Indicates an image will use high quality when uploaded. */ +"appSettings.media.imageQuality.high" = "E lartë"; + +/* Indicates an image will use low quality when uploaded. */ +"appSettings.media.imageQuality.low" = "E ulët"; + +/* Indicates an image will use maximum quality when uploaded. */ +"appSettings.media.imageQuality.maximum" = "Maksimum"; + +/* Indicates an image will use medium quality when uploaded. */ +"appSettings.media.imageQuality.medium" = "Mesatare"; + +/* The quality of image used when uploading */ +"appSettings.media.imageQuality.title" = "Cilësi"; + +/* Title for the image quality settings option. */ +"appSettings.media.imageQualityRow" = "Cilësi Figurash"; + +/* Message of an alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.message" = "Optimizimi i figurave i tkurr këto, për ngarkim më të shpejtë.\n\nKjo mundësi, si parazgjedhje, është e aktivizuar, por mund ta ndryshoni që nga rregullimet e aplikacioni kurdo."; + +/* Title of an alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.title" = "Të vazhdohet të optimizohen figura?"; + +/* Title of button for turning off image optimization, displayed in the alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.turnOff" = "Jo, çaktivizoje"; + +/* Title of button for leaving on image optimization, displayed in the alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.turnOn" = "Po, lëre të aktivizuar"; /* translators: displays audio file extension. e.g. MP3 audio file */ "audio file" = "kartelë audio"; @@ -9661,7 +9440,44 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "blogHeader.actionCopyURL" = "Kopjoji URL-në"; /* Context menu button title */ -"blogHeader.actionOpenInBrowser" = "Hape në Shfletues"; +"blogHeader.actionVisitSite" = "Vizitoni sajtin"; + +/* Title for a button that, when tapped, shows more info about participating in Bloganuary. */ +"bloganuary.dashboard.card.button.learnMore" = "Mësoni më tepër"; + +/* Short description for the Bloganuary event, shown right below the title. */ +"bloganuary.dashboard.card.description" = "Për muajin janar, cytjet për blogim do të vijnë nga Bloganuary — sfida e bashkësisë tonë për të krijuar një zakon blogimi për vitin e ri."; + +/* Title for the Bloganuary dashboard card while Bloganuary is running. */ +"bloganuary.dashboard.card.runningTitle" = "Bloganuary erdhi!"; + +/* Title for the Bloganuary dashboard card. */ +"bloganuary.dashboard.card.title" = "Bloganuary po vjen!"; + +/* Title of a button that calls the user to enable the Blogging Prompts feature. */ +"bloganuary.learnMore.modal.button.promptsDisabled" = "Aktivizoni cytje blogimi"; + +/* Title of a button that will dismiss the Bloganuary modal when tapped. +Note that the word 'go' here should have a closer meaning to 'start' rather than 'move forward'. */ +"bloganuary.learnMore.modal.button.promptsEnabled" = "Shkojmë!"; + +/* The second line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.description.second" = "Botoni përgjigjen tuaj."; + +/* The third line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.description.third" = "Lexoni përgjigje të bloguesve të tjerë, për t’u frymëzuar dhe bërë lidhje të reja."; + +/* The first line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.descriptions.first" = "Merrni një cytje të re, për t’ju frymëzuar çdo ditë."; + +/* An additional piece of information shown in case the user has the Blogging Prompts feature disabled. */ +"bloganuary.learnMore.modal.footer.addition" = "Për të marrë pjesë në Bloganuary lypset të aktivizoni Cytje Blogimi."; + +/* An informative excerpt shown in a subtler tone. */ +"bloganuary.learnMore.modal.footer.text" = "Bloganuary do të përdorë Cytje të Përditshme Blogimi për t’ju dërguar tema për muajin janar."; + +/* The headline text of the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.headline" = "Merrni pjesë te sfida jonë, e gjatë një muaj, për shkrim"; /* Verb. Dismisses the blogging prompt notification. */ "bloggingPrompt.pushNotification.customActionDescription.dismiss" = "Hidhe tej"; @@ -9690,6 +9506,12 @@ Example: Comment on Example: Reply to Pamela Nguyen */ "comment.header.subText.reply" = "Përgjigje për %1$@"; +/* Error message informing a user about an invalid password. */ +"common.reEnterPasswordMessage" = "Emri i përdoruesit ose fjalëkalimi i depozituar te aplikacioni mund të jenë të papërditësuar. Ju lutemi, jepeni fjalëkalimin tuaj te rregullimet dhe riprovoni."; + +/* An error message. */ +"common.unableToConnect" = "S’arrihet të Lidhet"; + /* Footnote for the privacy compliance popover. */ "compliance.analytics.popover.footnote" = "Këto cookies<\/em> na lejojnë të bëjmë funksionimin optimal, duke mbledhur hollësi mbi se si ndërveprojnë përdoruesit me sajtet tona."; @@ -9831,44 +9653,98 @@ Example: Reply to Pamela Nguyen */ /* Remote Config debug menu title */ "debugMenu.remoteConfig.title" = "Formësim Së Largëti"; +/* Remove current quick start tour menu item */ +"debugMenu.removeQuickStart" = "Hiqe Turin e Tanishëm"; + /* Title for a menu action in the context menu on the Jetpack install card. */ "domain.dashboard.card.menu.hide" = "Fshihe këtë"; -/* Domain Purchase Completion footer */ -"domain.purchase.preview.footer" = "Mund të duhen deri në 30 minuta që përkatësia juaj vetjake të fillojë të funksionojë."; +/* Search domain - Title for the Suggested domains screen */ +"domain.management.addDomain.search.title" = "Kërkoni për një përkatësi"; -/* Domain Purchase Completion description (only for FREE domains). */ -"domain.purchase.preview.free.description" = "Më pas, do t’ju ndihmojmë të bëheni gati që t’ju shfletojnë."; +/* Domain management buy domain card button title */ +"domain.management.buy.card.button.title" = "Merrni Përkatësi"; -/* Domain Purchase Completion description (only for PAID domains). */ -"domain.purchase.preview.paid.description" = "Ju dërguan dëftesën tuaj me email. Më pas, do t’ju ndihmojmë të bëheni gati për këdo."; +/* Domain management buy domain card subtitle */ +"domain.management.buy.card.subtitle" = "Shtoni një sajt më vonë."; -/* Reflects that site is live when domain purchase feature flag is ON. */ -"domain.purchase.preview.title" = "Përgëzime, sajti tuaj u bë!"; +/* Domain management buy domain card title */ +"domain.management.buy.card.title" = "Thjesht blini një përkatësi"; + +/* The expired label of the domain card in All Domains screen. */ +"domain.management.card.expired.label" = "E skaduar"; + +/* The renews label of the domain card in All Domains screen. */ +"domain.management.card.renews.label" = "Rinovohet më"; + +/* The empty state button title in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.button.title" = "Gjeni një përkatësi"; + +/* The empty state description in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.description" = "Prekni më poshtë që të gjeni përkatësinë tuaj të përsosur."; + +/* The empty state title in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.title" = "S’keni ndonjë përkatësi"; + +/* The empty state description in All Domains screen when an error occurs */ +"domain.management.error.empty.state.description" = "Hasëm një gabim teksa ngarkonim përkatësitë tuaja. Nëse problemi vazhdon, ju lutemi, lidhuni me asistencën."; + +/* The empty state title in All Domains screen when an error occurs */ +"domain.management.error.empty.state.title" = "Diç shkoi ters"; -/* Status of a domain in `Action Required` state */ -"domain.status.action.required" = "Lypset Veprim"; +/* The empty state button title in All Domains screen when an error occurs */ +"domain.management.error.state.button.title" = "Riprovoni"; -/* Status of a domain in `Active` state */ -"domain.status.active" = "Aktive"; +/* The empty state description in All Domains screen when the user is offline */ +"domain.management.offline.empty.state.description" = "Ju lutemi, kontrolloni lidhjen tuaj në rrjet dhe riprovoni."; -/* Status of a domain in `Complete Setup` state */ -"domain.status.complete.setup" = "Plotësoni Ujdisjen"; +/* The empty state title in All Domains screen when the user is offline */ +"domain.management.offline.empty.state.title" = "S’ka Lidhje Internet"; -/* Status of a domain in `Error` state */ -"domain.status.error" = "Gabim"; +/* Domain management choose site card button title */ +"domain.management.purchase.footer" = "*Me krejt planet vjetorë me pagesë përfshihet një përkatësi falas për një vit"; -/* Status of a domain in `Expired` state */ -"domain.status.expired" = "E skaduar"; +/* Domain management purchase domain screen title */ +"domain.management.purchase.subtitle" = "Mos u merakosni, mund të shtoni lehtë një sajt më vonë."; -/* Status of a domain in `Expiring Soon` state */ -"domain.status.expiring.soon" = "Skadon Së Shpejti"; +/* Domain management purchase domain screen title. */ +"domain.management.purchase.title" = "Zgjidhni si të përdoret përkatësia juaj"; -/* Status of a domain in `Failed` state */ -"domain.status.failed" = "E dështuar"; +/* The search bar title in All Domains screen. */ +"domain.management.search-bar.title" = "Kërkoni te përkatësitë"; -/* Status of a domain in `In Progress` state */ -"domain.status.in.progress" = "Në Ecuri e Sipër"; +/* The empty state description in All Domains screen when the are no domains matching the search criteria */ +"domain.management.search.empty.state.description" = "S’gjetëm dot ndonjë përkatësi që të ketë përputhje me kërkimin tuaj për '%@'"; + +/* The empty state title in All Domains screen when the are no domains matching the search criteria */ +"domain.management.search.empty.state.title" = "S’u Gjetën Përkatësi Me Përputhje"; + +/* Domain management choose site card button title */ +"domain.management.site.card.button.title" = "Zgjidhni Sajt"; + +/* Domain management choose site card subtitle */ +"domain.management.site.card.footer" = "Përkatësi falas për vitin e parë*"; + +/* Domain management choose site card subtitle */ +"domain.management.site.card.subtitle" = "Përdoreni me një sajt që keni filluar tashmë."; + +/* Domain management choose site card title */ +"domain.management.site.card.title" = "Sajt WordPress.com ekzistues"; + +/* Domain Management Screen Title */ +"domain.management.title" = "Krejt Përkatësitë"; + +/* Domain Purchase Completion footer */ +"domain.purchase.preview.footer" = "Mund të duhen deri në 30 minuta që përkatësia juaj vetjake të fillojë të funksionojë."; + +/* Domain Purchase Completion description (only for FREE domains). */ +"domain.purchase.preview.free.description" = "Më pas, do t’ju ndihmojmë të bëheni gati që t’ju shfletojnë."; + +/* Domain Purchase Completion description (only for PAID domains). */ +"domain.purchase.preview.paid.description" = "Ju dërguan dëftesën tuaj me email. Më pas, do t’ju ndihmojmë të bëheni gati për këdo."; + +/* Reflects that site is live when domain purchase feature flag is ON. */ +"domain.purchase.preview.title" = "Përgëzime, sajti tuaj u bë!"; /* The 'Best Alternative' label under the domain name in 'Choose a domain' screen */ "domain.suggestions.row.best-alternative" = "Alternativa Më e Mirë"; @@ -9879,6 +9755,9 @@ Example: Reply to Pamela Nguyen */ /* The text to display for free domains in 'Site Creation > Choose a domain' screen */ "domain.suggestions.row.free" = "Falas"; +/* The text to display for paid domains that are free for the first year with the paid plan in 'Site Creation > Choose a domain' screen */ +"domain.suggestions.row.free-with-plan" = "Falas për vitin e parë, me plane vjetore të paguar"; + /* The 'Recommended' label under the domain name in 'Choose a domain' screen */ "domain.suggestions.row.recommended" = "E rekomanduar"; @@ -9888,12 +9767,24 @@ Example: Reply to Pamela Nguyen */ /* The text to display for paid domains in 'Site Creation > Choose a domain' screen */ "domain.suggestions.row.yearly" = "në vit"; +/* Title for the checkout screen. */ +"domains.checkout.title" = "Përfundim Blerjeje"; + /* Action shown in a bottom notice to dismiss it. */ "domains.failure.dismiss" = "Hidhe poshtë"; /* Content show when the domain selection action fails. */ "domains.failure.title" = "Na ndjeni, përkatësia që po rrekeni të shtoni s’mund të blihet që nga aplikacioni Jetpack në këtë kohë."; +/* Title for the screen where the user can choose how to use the domain they're end up purchasing. */ +"domains.purchase.choice.title" = "Blini Përkatësi"; + +/* Back button title that navigates back to the search domains screen. */ +"domains.search.backButton.title" = "Kërko"; + +/* Title of screen where user chooses a site to connect to their selected domain */ +"domains.sitePicker.title" = "Zgjidhni Sajt"; + /* No comment provided by engineer. */ "double-tap to change unit" = "që të ndryshoni njësinë, prekeni dy herë"; @@ -9911,6 +9802,15 @@ Example: Reply to Pamela Nguyen */ Site Address placeholder */ "example.com" = "example.com"; +/* Title for confirmation navigation bar button item */ +"externalMediaPicker.add" = "Shtoje"; + +/* Bottom toolbar title in the selection mode */ +"externalMediaPicker.toolbarSelectItemsPrompt" = "Përzgjidhni Figura"; + +/* Bottom toolbar title in the selection mode */ +"externalMediaPicker.toolbarViewSelected" = "Shihni të Përzgjedhurat (%@)"; + /* Title of screen the displays the details of an advertisement campaign. */ "feature.blaze.campaignDetails.title" = "Hollësi Fushate"; @@ -10007,9 +9907,6 @@ Example: Reply to Pamela Nguyen */ /* (placeholder) Help the user enter a URL into the field */ "http:\/\/my-site-address (URL)" = "http:\/\/adresa-e-sajtit-tim (URL)"; -/* Label displayed on image media items. */ -"image" = "figurë"; - /* Sentence to justify why the app is asking permission from the user to use their camera. */ "infoplist.NSCameraUsageDescription" = "Që të bëjë foto ose video për t’u përdorur te postimet tuaja."; @@ -10310,6 +10207,12 @@ Example: Reply to Pamela Nguyen */ /* Indicating that referrer was marked as spam */ "marked as spam" = "iu vu shenjë si i padëshiruar"; +/* Products header text in Me Screen. */ +"me.products.header" = "Produkte"; + +/* Title of error prompt shown when a sync fails. */ +"media.syncFailed" = "S’arrihet të njëkohësohet media"; + /* An error message the app shows if media import fails */ "mediaExporter.error.unknown" = "Objekti s’u shtua dot te Mediateka"; @@ -10319,18 +10222,24 @@ Example: Reply to Pamela Nguyen */ /* Message of an alert informing users that the video they are trying to select is not allowed. */ "mediaExporter.videoLimitExceededError" = "Ngarkimi i videove më të gjata se 5 minuta lyp një plan me pagesë."; -/* Verb. User action to dismiss error alert when failing to load media item. */ -"mediaItemTable.errorAlert.dismissButton" = "Hidhe tej"; - /* Accessibility hint for add button to add items to the user's media library */ "mediaLibrary.addButtonAccessibilityHint" = "Shtoni media të re"; /* Accessibility label for add button to add items to the user's media library */ "mediaLibrary.addButtonAccessibilityLabel" = "Shtoni"; +/* Button name in the more menu */ +"mediaLibrary.aspectRatioGrid" = "Rrjetë Përpjestimore"; + +/* Context menu button */ +"mediaLibrary.buttonDelete" = "Fshije"; + /* Media screen navigation bar button Select title */ "mediaLibrary.buttonSelect" = "Përzgjidhni"; +/* Context menu button */ +"mediaLibrary.buttonShare" = "Ndaje"; + /* Verb. Button title. Tapping cancels an action. */ "mediaLibrary.deleteConfirmationCancel" = "Anuloje"; @@ -10352,6 +10261,21 @@ Example: Reply to Pamela Nguyen */ /* Text displayed in HUD after successfully deleting a media item */ "mediaLibrary.deletionSuccessMessage" = "U fshi!"; +/* The name of the media filter */ +"mediaLibrary.filterAll" = "Krejt"; + +/* The name of the media filter */ +"mediaLibrary.filterAudio" = "Audio"; + +/* The name of the media filter */ +"mediaLibrary.filterDocuments" = "Dokumente"; + +/* The name of the media filter */ +"mediaLibrary.filterImages" = "Figura"; + +/* The name of the media filter */ +"mediaLibrary.filterVideos" = "Video"; + /* User action to delete un-uploaded media. */ "mediaLibrary.retryOptionsAlert.delete" = "Fshije"; @@ -10364,12 +10288,39 @@ Example: Reply to Pamela Nguyen */ /* Message displayed when no results are returned from a media library search. Should match Calypso. */ "mediaLibrary.searchResultsEmptyTitle" = "S’ka media që përputhet me kërkimin tuaj"; +/* Text displayed in HUD if there was an error attempting to share a group of media items. */ +"mediaLibrary.sharingFailureMessage" = "S’arrihet të ndahen me të tjerë objektet e përzgjedhur."; + +/* Button name in the more menu */ +"mediaLibrary.squareGrid" = "Rrjetë Katrore"; + /* Media screen navigation title */ "mediaLibrary.title" = "Media"; +/* Bottom toolbar title in the selection mode */ +"mediaLibrary.toolbarSelectImagesMany" = "%d Figura të Përzgjedhura"; + +/* Bottom toolbar title in the selection mode */ +"mediaLibrary.toolbarSelectImagesOne" = "1 Figurë e Përzgjedhur"; + +/* Bottom toolbar title in the selection mode */ +"mediaLibrary.toolbarSelectItemsMany" = "%d Objekte të Përzgjedhur"; + +/* Bottom toolbar title in the selection mode */ +"mediaLibrary.toolbarSelectItemsOne" = "1 Objekt i Përzgjedhur"; + +/* Bottom toolbar title in the selection mode */ +"mediaLibrary.toolbarSelectItemsPrompt" = "Përzgjidhni Objekte"; + /* The title of the button to dismiss the alert shown when the picked media cannot be imported into stories. */ "mediaPicker.failedMediaExportAlert.dismissButton" = "Hidhe tej"; +/* Error message when picked media cannot be imported into stories. */ +"mediaPicker.failedMediaExportAlert.message" = "Media juaj s’u eksportua dot. Nëse problemi vazhdon, mund të lidheni me ne që nga skena Unë > Ndihmë & Asistencë."; + +/* Error title when picked media cannot be imported into stories. */ +"mediaPicker.failedMediaExportAlert.title" = "Eksportimi i Medias Dështoi"; + /* Message for alert when access to camera is not granted */ "mediaPicker.noCameraAccessMessage" = "Ky aplikacion lyp leje përdorimi të Kamerës, që të krijojë media të re, nëse doni ta lejoni këtë, ju lutemi, ndryshoni rregullimet mbi privatësinë."; @@ -10403,6 +10354,12 @@ Example: Reply to Pamela Nguyen */ /* The name of the action in the context menu */ "mediaPicker.takeVideo" = "Bëni Video"; +/* Navigation title for media preview. Example: 1 of 3 */ +"mediaPreview.NofM" = "%1$@ nga %2$@"; + +/* Max image size in pixels (e.g. 300x300px) */ +"mediaSizeSlider.valueFormat" = "%1$d × %2$d px"; + /* The description in the Delete WordPress screen */ "migration.deleteWordpress.description" = "Duket sikur keni ende të instaluar aplikacionin WordPress."; @@ -10415,9 +10372,6 @@ Example: Reply to Pamela Nguyen */ /* The title in the Delete WordPress screen */ "migration.deleteWordpress.title" = "S’ju duhet më aplikacioni WordPress në pajisjen tuaj"; -/* Primary button title in the migration done screen. */ -"migration.done.actions.primary.title" = "Përfundoje"; - /* Footer for the migration done screen. */ "migration.done.footer" = "Rekomandojmë çinstalimin e aplikacionit WordPress në pajisjen tuaj, për të shmangur përplasje të dhënash."; @@ -10427,6 +10381,9 @@ Example: Reply to Pamela Nguyen */ /* Primary description in the migration done screen. */ "migration.done.primaryDescription" = "Kemi shpërngulur krejt të dhënat dhe rregullimet tuaja. Gjithçka është mu atje ku e latë."; +/* Secondary description (second paragraph) in the migration done screen. */ +"migration.done.secondaryDescription" = "Ka ardhur koha a vazhdoni udhëtimin tuaj WordPress te aplikacioni Jetpack!"; + /* Title of the migration done screen. */ "migration.done.title" = "Faleminderit që kaloni në Jetpack!"; @@ -10467,14 +10424,17 @@ Example: Reply to Pamela Nguyen */ "migration.welcome.primaryDescription" = "Duket sikur po bëni migrimin që nga aplikacioni WordPress."; /* The plural form of the secondary description in the migration welcome screen */ -"migration.welcome.secondaryDescription.plural" = "I gjetëm sajtet tuaj. Vazhdoni, që të shpërngulen krejt të dhënat tuaja dhe të bëhet automatikisht hyrja në Jetpack."; +"migration.welcome.secondaryDescription.plural" = "I gjetëm sajtet tuaj. Vazhdoni, që të shpërngulen krejt të dhënat tuaja dhe të bëhet automatikisht hyrja në Jetpack."; /* The singular form of the secondary description in the migration welcome screen */ -"migration.welcome.secondaryDescription.singular" = "E gjetëm sajtin tuaj. Vazhdoni, që të shpërngulen krejt të dhënat tuaja dhe të bëhet automatikisht hyrja në Jetpack."; +"migration.welcome.secondaryDescription.singular" = "E gjetëm sajtin tuaj. Vazhdoni, që të shpërngulen krejt të dhënat tuaja dhe të bëhet automatikisht hyrja në Jetpack."; /* The title in the migration welcome screen */ "migration.welcome.title" = "Mirë se vini te Jetpack-u!"; +/* Primary button title in the migration done screen. */ +"migrationDone.actions.primaryTitle" = "Shkojmë"; + /* Description for the static screen displayed prompting users to switch the Jetpack app. */ "movedToJetpack.description" = "Aplikacioni Jetpack ka krejt veçoritë e aplikacionit WordPress dhe tanimë edhe hyrje përjashtimore te Statistika, Lexues, Njoftime, etj."; @@ -10550,6 +10510,30 @@ Example: Reply to Pamela Nguyen */ /* Message title for when a user has no sites. */ "mySite.noSites.title" = "S’keni ndonjë sajt"; +/* Menu title for the add site option */ +"mySite.siteActions.addSite" = "Shtoni sajt"; + +/* Button that reveals more site actions */ +"mySite.siteActions.button" = "Veprime Sajti"; + +/* Accessibility hint for button used to show more site actions */ +"mySite.siteActions.hint" = "Prekeni, që të shfaqen më tepër veprime sajti"; + +/* Menu title for the personalize home option */ +"mySite.siteActions.personalizeHome" = "Personalizoni kreun"; + +/* Menu title for the change site icon option */ +"mySite.siteActions.siteIcon" = "Ndryshoni ikonë sajti"; + +/* Menu title for the change site title option */ +"mySite.siteActions.siteTitle" = "Ndryshoni titull sajti"; + +/* Menu title for the switch site option */ +"mySite.siteActions.switchSite" = "Ndërroni sajt"; + +/* Menu title for the visit site option */ +"mySite.siteActions.visitSite" = "Vizitoni sajtin"; + /* Dismiss button title. */ "noResultsViewController.dismissButton" = "Hidhe tej"; @@ -10565,14 +10549,20 @@ Example: Reply to Pamela Nguyen */ /* This is one of the buttons we display when prompting the user for a review */ "notifications.appRatings.sendFeedback.yes.buttonTitle" = "Dërgoni përshtypje"; -/* Word separating the current index from the total amount. I.e.: 7 of 9 */ -"of" = "nga"; +/* Badge for page cells */ +"pageList.badgeHomepage" = "Faqe hyrëse"; -/* Label displayed on media items that are not video, image, or audio. */ -"other" = "tjetër"; +/* Badge for page cells */ +"pageList.badgeLocalChanges" = "Ndryshime vendore"; -/* Promote the page with Blaze. */ -"pages.blaze.actionTitle" = "Promovojeni me Blaze"; +/* Badge for page cells */ +"pageList.badgePendingReview" = "Në pritje të shqyrtimit"; + +/* Badge for page cells */ +"pageList.badgePosts" = "Faqe postimesh"; + +/* Badge for page cells */ +"pageList.badgePrivate" = "Private"; /* Subtitle of the theme template homepage cell */ "pages.template.subtitle" = "Faqja juaj hyrëse po përdor një gjedhe Teme dhe do të hapet në përpunuesin në internet."; @@ -10580,6 +10570,36 @@ Example: Reply to Pamela Nguyen */ /* Title of the theme template homepage cell */ "pages.template.title" = "Faqe hyrëse"; +/* Message informing the user that their static homepage page was set successfully */ +"pages.updatePage.successTitle" = "Faqja u përditësua me sukses"; + +/* Delete option in the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.actionTitle" = "Fshije Përgjithmonë"; + +/* Message of the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.alertMessage" = "Jeni i sigurt se doni të fshihet përgjithmonë kjo faqe?"; + +/* Title of the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.alertTitle" = "Të Fshihet Përgjithmonë?"; + +/* Menu option for filtering posts by everyone */ +"pagesList.pagesByEveryone" = "Faqe nga cilido"; + +/* Menu option for filtering posts by me */ +"pagesList.pagesByMe" = "Faqe nga unë"; + +/* Trash option in the trash page confirmation alert. */ +"pagesList.trash.actionTitle" = "Shpjere te Hedhurinat"; + +/* Message of the trash page confirmation alert. */ +"pagesList.trash.alertMessage" = "Jeni i sigurt se doni të shpihet në hedhurina kjo faqe?"; + +/* Title of the trash page confirmation alert. */ +"pagesList.trash.alertTitle" = "Të shpihet në hedhurina kjo faqe?"; + +/* Cancels an Action */ +"pagesList.trash.cancel" = "Anuloje"; + /* No comment provided by engineer. */ "password" = "fjalëkalim"; @@ -10607,12 +10627,60 @@ Example: Reply to Pamela Nguyen */ /* Card title for the pesonalization menu */ "personalizeHome.dashboardCard.todaysStats" = "Statistika për sot"; +/* Section header for shortcuts */ +"personalizeHome.shortcutsSectionHeader" = "Shfaqni ose fshihni shkurtore"; + /* Page title */ "personalizeHome.title" = "Personalizoni Skedën Krye"; /* Register Domain - Domain contact information field Phone */ "phone number" = "numër telefoni"; +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.createdTimeAgo" = "Krijuar %@"; + +/* Status mesasge for post cells */ +"post.deletingPostPermanentlyStatusMessage" = "Po fshihet postim…"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.editedTimeAgo" = "Përpunuar %@"; + +/* Status mesasge for post cells */ +"post.movingToTrashStatusMessage" = "Po shpihet postim në hedhurina…"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.publishedTimeAgo" = "Botuar %@"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.scheduledForDate" = "Planifikuar për më %@"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.trashedTimeAgo" = "Shpënë te Hedhurinat %@"; + +/* Accessibility label for the post author in the post list. The parameter is the author name. For example, \"By Elsa.\" */ +"postList.a11y.authorChunkFormat" = "Nga %@."; + +/* Accessibility label for a post's excerpt in the post list. The parameter is the post excerpt. For example, \"Excerpt. This is the first paragraph.\" */ +"postList.a11y.exerptChunkFormat" = "Copëz. %@."; + +/* Accessibility label for a sticky post in the post list. */ +"postList.a11y.sticky" = "Ngjitës."; + +/* Accessibility label for a post in the post list. The first placeholder is the post title. The second placeholder is the date. */ +"postList.a11y.titleAndDateChunkFormat" = "%1$@, %2$@."; + +/* Title for the 'Trash' post list row swipe action */ +"postList.swipeActionDelete" = "Shpjere te hedhurinat"; + +/* Title for the 'Delete' post list row swipe action */ +"postList.swipeActionDeletePermanently" = "Fshije"; + +/* Title for the 'Share' post list row swipe action */ +"postList.swipeActionShare" = "Ndajeni"; + +/* Title for the 'View' post list row swipe action */ +"postList.swipeActionView" = "Shiheni"; + /* User action to dismiss featured media options. */ "postSettings.featuredImageUploadActionSheet.dismiss" = "Hidhe tej"; @@ -10631,9 +10699,84 @@ Example: Reply to Pamela Nguyen */ /* Button in Post Settings */ "postSettings.setFeaturedImageButton" = "Caktoni Figurë të Zgjedhur"; +/* Error message on post/page settings screen */ +"postSettings.updateFailedMessage" = "S’u arrit të përditësohen rregullime postimi"; + /* Promote the post with Blaze. */ "posts.blaze.actionTitle" = "Promovojeni me Blaze"; +/* Label for the Post List option that cancels automatic uploading of a post. */ +"posts.cancelUpload.actionTitle" = "Anuloje ngarkimin"; + +/* Label for post comments option. Tapping displays comments for a post. */ +"posts.comments.actionTitle" = "Komente"; + +/* Label for the delete post option. Tapping permanently deletes a post. */ +"posts.delete.actionTitle" = "Fshije përgjithmonë"; + +/* Label for an option that moves a post to the draft folder */ +"posts.draft.actionTitle" = "Shpjere te skica"; + +/* Label for post duplicate option. Tapping creates a copy of the post. */ +"posts.duplicate.actionTitle" = "Përsëdyte"; + +/* Opens a submenu for page attributes. */ +"posts.pageAttributes.actionTitle" = "Atribute faqeje"; + +/* Label for the preview post button. Tapping displays the post as it appears on the web. */ +"posts.preview.actionTitle" = "Paraparje"; + +/* Label for an option that moves a publishes a post immediately */ +"posts.publish.actionTitle" = "Botoje që tani"; + +/* Retry uploading the post. */ +"posts.retry.actionTitle" = "Riprovoni"; + +/* Set the selected page as the homepage. */ +"posts.setHomepage.actionTitle" = "Caktojeni si faqen hyrëse"; + +/* Set the parent page for the selected page. */ +"posts.setParent.actionTitle" = "Caktoje si mëmë"; + +/* Set the selected page as a posts page. */ +"posts.setPostsPage.actionTitle" = "Caktojeni si faqe postimesh"; + +/* Set the selected page as a regular page. */ +"posts.setRegularPage.actionTitle" = "Vëreni si faqe të rregullt"; + +/* Label for post settings option. Tapping displays settings for a post. */ +"posts.settings.actionTitle" = "Rregullime"; + +/* Share the post. */ +"posts.share.actionTitle" = "Ndajeni"; + +/* Label for post stats option. Tapping displays statistics for a post. */ +"posts.stats.actionTitle" = "Statistika"; + +/* Label for a option that moves a post to the trash folder */ +"posts.trash.actionTitle" = "Shpjere te hedhurinat"; + +/* Label for the view post button. Tapping displays the post as it appears on the web. */ +"posts.view.actionTitle" = "Shiheni"; + +/* A short message explaining that a page was deleted permanently. */ +"postsList.deletePage.message" = "Faqja u fshi përgjithmonë"; + +/* A short message explaining that a post was deleted permanently. */ +"postsList.deletePost.message" = "Postimi u fshi përgjithmonë"; + +/* A short message explaining that a page was moved to the trash bin. */ +"postsList.movePageToTrash.message" = "Faqja u shpu te hedhurinat"; + +/* A short message explaining that a post was moved to the trash bin. */ +"postsList.movePostToTrash.message" = "Postimi u shpu te hedhurinat"; + +/* Menu option for filtering posts by everyone */ +"postsList.postsByEveryone" = "Postime nga kushdo"; + +/* Menu option for filtering posts by me */ +"postsList.postsByMe" = "Postime nga unë"; + /* Title for the button to subscribe to Jetpack Social on the remaining shares view */ "postsettings.social.remainingshares.subscribe" = "Pajtohuni që tani, që të ndani më tepër me të tjerët"; @@ -10719,16 +10862,40 @@ Tapping on this row allows the user to edit the sharing message. */ /* The quick tour actions item to select during a guided tour. */ "quickStart.moreMenu" = "Më tepër"; +/* Accessibility hint to inform that the author section can be tapped to see posts from the site. */ +"reader.detail.header.authorInfo.a11y.hint" = "Shihni postime nga sajti"; + /* Title for the Comment button on the Reader Detail toolbar. Note: Since the display space is limited, a short or concise translation is preferred. */ "reader.detail.toolbar.comment.button" = "Komentoni"; +/* Title for the Like button in the Reader Detail toolbar. +This is shown when the user has not liked the post yet. +Note: Since the display space is limited, a short or concise translation is preferred. */ +"reader.detail.toolbar.like.button" = "Pëlqejeni"; + +/* Accessibility hint for the Like button state. The button shows that the user has not liked the post, +but tapping on this button will add a Like to the post. */ +"reader.detail.toolbar.like.button.a11y.hint" = "E pëlqen këtë postim."; + +/* Title for the Like button in the Reader Detail toolbar. +This is shown when the user has already liked the post. +Note: Since the display space is limited, a short or concise translation is preferred. */ +"reader.detail.toolbar.liked.button" = "U pëlqye"; + +/* Accessibility hint for the Liked button state. The button shows that the user has liked the post, +but tapping on this button will remove their like from the post. */ +"reader.detail.toolbar.liked.button.a11y.hint" = "Heq pëlqimin për këtë postim."; + /* Accessibility hint for the 'Save Post' button. */ "reader.detail.toolbar.save.button.a11y.hint" = "E ruan këtë postim për më vonë."; /* Accessibility label for the 'Save Post' button. */ "reader.detail.toolbar.save.button.a11y.label" = "Ruaje postimin"; +/* Accessibility hint for the 'Save Post' button when a post is already saved. */ +"reader.detail.toolbar.saved.button.a11y.hint" = "Heq ruajtjen e këtij postimi."; + /* Accessibility label for the 'Save Post' button when a post has been saved. */ "reader.detail.toolbar.saved.button.a11y.label" = "Postimi u Ruajt"; @@ -10742,24 +10909,57 @@ Note: Since the display space is limited, a short or concise translation is pref Example: given a notice format "Following %@" and empty site name, this will be "Following this site". */ "reader.notice.follow.site.unknown" = "këtë sajt"; +/* Notice title when blocking a user fails. */ +"reader.notice.user.blocked" = "reader.notice.user.block.failed"; + /* Text for the 'Comment' button on the reader post card cell. */ "reader.post.button.comment" = "Komentoni"; +/* Accessibility hint for the comment button on the reader post card cell */ +"reader.post.button.comment.accessibility.hint" = "Hap komentet për postimin."; + +/* Text for the 'Like' button on the reader post card cell. */ +"reader.post.button.like" = "Pëlqejeni"; + +/* Accessibility hint for the like button on the reader post card cell */ +"reader.post.button.like.accessibility.hint" = "E pëlqen postimin."; + +/* Text for the 'Liked' button on the reader post card cell. */ +"reader.post.button.liked" = "U pëlqye"; + +/* Accessibility hint for the liked button on the reader post card cell */ +"reader.post.button.liked.accessibility.hint" = "Heq pëlqimin për këtë postim."; + +/* Accessibility hint for the site header on the reader post card cell */ +"reader.post.button.menu.accessibility.hint" = "Hap një menu me më tepër veprime."; + /* Accessibility label for the more menu button on the reader post card cell */ "reader.post.button.menu.accessibility.label" = "Më tepër"; /* Text for the 'Reblog' button on the reader post card cell. */ "reader.post.button.reblog" = "Riblogojeni"; +/* Accessibility hint for the reblog button on the reader post card cell */ +"reader.post.button.reblog.accessibility.hint" = "Riblogon postimin."; + +/* Accessibility hint for the site header on the reader post card cell */ +"reader.post.header.accessibility.hint" = "Hap hollësi sajti për postimin."; + /* The title of a button that triggers blocking a user from the user's reader. */ "reader.post.menu.block.user" = "Bllokoje këtë përdorues"; +/* The title of a button that removes a saved post. */ +"reader.post.menu.remove.post" = "Heq Postimin e Ruajtur"; + /* The title of a button that triggers the reporting of a post's author. */ "reader.post.menu.report.user" = "Raportojeni këtë përdorues"; /* The title of a button that saves a post. */ "reader.post.menu.save.post" = "Ruaje"; +/* The formatted number of posts and followers for a site. '%1$@' is a placeholder for the site post count. '%2$@' is a placeholder for the site follower count. Example: `5,000 posts • 10M followers` */ +"reader.site.header.counts" = "%1$@ postime • %2$@ ndjekës"; + /* Spoken accessibility label */ "readerDetail.backButton.accessibilityLabel" = "Mbrapsht"; @@ -10770,7 +10970,7 @@ Example: given a notice format "Following %@" and empty site name, this will be "readerDetail.followConversationTooltipButton.accessibilityLabel" = "E kuptova"; /* Message for the follow conversations tooltip. */ -"readerDetail.followConversationTooltipMessage.accessibilityLabel" = "Njoftuhuni, kur te ky postim shtohen komente të reja."; +"readerDetail.followConversationTooltipMessage.accessibilityLabel" = "Njoftohuni, kur te ky postim shtohen komente të reja."; /* Title of follow conversations tooltip. */ "readerDetail.followConversationTooltipTitle.accessibilityLabel" = "Ndiqni bisedën"; @@ -10793,6 +10993,12 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Title for the tooltip anchor. */ "readerDetail.tooltipAnchorTitle.accessibilityLabel" = "I ri"; +/* The button title for the transfer footer view in Register Domain screen */ +"register.domain.transfer.button.title" = "Shpërngule përkatësinë"; + +/* The title for the transfer footer view in Register Domain screen */ +"register.domain.transfer.title" = "Po shihni si të shpërngulni një përkatësi që e zotëroni tashmë?"; + /* Information of what related post are and how they are presented */ "relatedPostsSettings.optionsFooter" = "Postimet e Afërta shfaqin nën postimet tuaja lëndë të afërt prej sajtit tuaj."; @@ -10892,6 +11098,21 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Accessibility hint for actions when displaying media items. */ "siteMedia.cellAccessibilityHint" = "Përzgjidhni media."; +/* Accessibility hint for media item preview for user's viewing an item in their media library */ +"siteMediaItem.contentViewAccessibilityHint" = "Prekni që të shihni median sa krejt ekrani"; + +/* Accessibility label for media item preview for user's viewing an item in their media library */ +"siteMediaItem.contentViewAccessibilityLabel" = "Bëni paraparje të medias"; + +/* Title for confirmation navigation bar button item */ +"siteMediaPicker.add" = "Shtoje"; + +/* Button selection media in media picker */ +"siteMediaPicker.deselect" = "Shpërzgjidhe"; + +/* Button selection media in media picker */ +"siteMediaPicker.select" = "Përzgjidhe"; + /* Media screen navigation title */ "siteMediaPicker.title" = "Media"; @@ -10899,7 +11120,7 @@ Example: given a notice format "Following %@" and empty site name, this will be "siteSettings.privacy.title" = "Privatësi"; /* Hint for users when hidden privacy setting is set */ -"siteVisibility.hidden.hint" = "Sajti juaj është i dukshëm për këdo, por u kërkon motorëve të kërkimeve të mos e indeksojnë."; +"siteVisibility.hidden.hint" = "Sajti juaj u është fshehur vizitorëve pas një shënimi “S’Afërmi”, deri sa të jetë gati për t’u parë."; /* Text for privacy settings: Hidden */ "siteVisibility.hidden.title" = "I fshehur"; @@ -11060,6 +11281,12 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Dismiss the AlertView */ "stockPhotos.strings.dismiss" = "Hidhe tej"; +/* Subtitle for placeholder in Free Photos. The company name 'Pexels' should always be written as it is. */ +"stockPhotos.subtitle" = "Foto të furnizuara nga Pexels"; + +/* Title for placeholder in Free Photos */ +"stockPhotos.title" = "Kërkoni për të gjetur foto të lira që t’i shtoni te Mediateka juaj!"; + /* Section title for prominent suggestions */ "suggestions.section.prominent" = "Në këtë bisedë"; @@ -11093,6 +11320,9 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Button for users to contact the support team directly. */ "support.chatBot.contactSupport" = "Lidhuni me asistencën"; +/* Initial message shown to the user when the chat starts. */ +"support.chatBot.firstMessage" = "Njatjeta, jam Asistenti IA Jetpack.\\n\\nMe se mund t’ju ndihmoj?\\n\\nNëse s’i përgjigjem dot pyetjes tuaj, do t’ju ndihmoj të hapni një çështje asistence nga ekipi ynë!"; + /* Placeholder text for the chat input field. */ "support.chatBot.inputPlaceholder" = "Dërgoni një mesazh…"; @@ -11141,11 +11371,14 @@ Example: given a notice format "Following %@" and empty site name, this will be /* A title for a text that displays a transcript of user's question in a support chat */ "support.chatBot.zendesk.question" = "Pyetje"; +/* A title for a text that displays a transcript from a conversation between Jetpack Mobile Bot (chat bot) and a user */ +"support.chatBot.zendesk.transcript" = "Transkriptim Nga Roboti Jetpack Mobile"; + /* Suggestion in Support view to visit the Forums. */ "support.row.communityForum.title" = "Bëni një pyetje te forumi i bashkësisë dhe merrni ndihmë nga grupi ynë i vullnetarëve."; /* Accessibility hint describing what happens if the Contact Email button is tapped. */ -"support.row.contactEmail.accessibilityHint" = "Shfaq një dialog për ndryshimin e Email-it të Kontaktit"; +"support.row.contactEmail.accessibilityHint" = "Shfaq një dialog për ndryshimin e Email-it të Kontaktit."; /* Display value for Support email field if there is no user email address. */ "support.row.contactEmail.emailNoteSet.detail" = "E paujdisur"; @@ -11184,7 +11417,7 @@ Example: given a notice format "Following %@" and empty site name, this will be "support.row.version.title" = "Version"; /* Support screen footer text explaining the benefits of enabling the Debug feature. */ -"support.sectionFooter.advanced.title" = "Aktivizoni Diagnostikimin, që të përfshini të dhëna shtesë te regjistrat tuaj, të cilat mund të ndihmojë për të diagnostikuar probleme me aplikacionin."; +"support.sectionFooter.advanced.title" = "Aktivizoni Diagnostikimin, që të përfshini të dhëna shtesë te regjistrat tuaj, të cilat mund të ndihmojë për të diagnostikuar probleme me aplikacionin."; /* WordPress.com sign-out section header title */ "support.sectionHeader.account.title" = "Llogari WordPress.com"; @@ -11201,6 +11434,9 @@ Example: given a notice format "Following %@" and empty site name, this will be /* View title for Help & Support page. */ "support.title" = "Ndihmë"; +/* Title for placeholder in Tenor picker */ +"tenor.welcomeMessage" = "Kërkoni për të gjetur GIF-e që t’i shtoni te Mediateka juaj!"; + /* Header of delete screen section listing things that will be deleted. */ "these items will be deleted:" = "këto objekte do të fshihen:"; @@ -11216,18 +11452,27 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Displayed in the confirmation alert when marking unread notifications as read. */ "unread" = "të palexuar"; -/* Label displayed on video media items. */ -"video" = "video"; - /* Portion of a message for Jetpack users that have multisite WP installation, thus Restore is not available. This part is a link, colored with a different color. */ "visit our documentation page" = "vizitoni faqen tonë të dokumentimit"; /* Verb. Dismiss the web view screen. */ "webKit.button.dismiss" = "Hidhe tej"; +/* Preview title of all-time posts and most views widget */ +"widget.allTimePostViews.previewTitle" = "Postime & Më të Parët e Krejt Kohës"; + +/* Preview title of all-time views widget */ +"widget.allTimeViews.previewTitle" = "Parje Gjatë Krejt Kohës"; + +/* Preview title of all-time views and visitors widget */ +"widget.allTimeViewsVisitors.previewTitle" = "Parje & Vizitorë Gjatë Gjithë Kohës"; + /* Title of best views ever label in all time widget */ "widget.alltime.bestviews.label" = "Parjet më të mira ndonjëherë"; +/* Title of the label which displays the number of the most daily views the site has ever had. Keep the translation as short as possible. */ +"widget.alltime.bestviewsshort.label" = "Shumica e Parjeve"; + /* Title of the no data view in all time widget */ "widget.alltime.nodata.view.title" = "S’arrihet të ngarkohen statistika të krejt kohës."; @@ -11312,11 +11557,20 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Title of visitors label in today widget */ "widget.today.visitors.label" = "Vizitorë"; +/* Preview title of today's likes and commnets widget */ +"widget.todayLikesComments.previewTitle" = "Pëlqime & Komente Sot"; + +/* Preview title of today's views widget */ +"widget.todayViews.previewTitle" = "Parje Sot"; + +/* Preview title of today's views and visitors widget */ +"widget.todayViewsVisitors.previewTitle" = "Parje & Vizitorë Sot"; + /* Second part of delete screen title stating [the site] will be unavailable in the future. */ "will be unavailable in the future." = "s’do të jetë më i passhëm në të ardhmen."; /* This is a comma separated list of keywords used for spotlight indexing of the 'Help & Support' screen within the 'Me' tab */ -"wordpress, help, support, faq, questions, debug, logs, help center, contact" = "wordpress, ndihmë, asistencë, faq, pyetje, diagnostikim, regjistra, qendër ndihme, kontakt"; +"wordpress, help, support, faq, questions, debug, logs, help center, contact" = "wordpress, ndihmë, asistencë, PBR, pyetje, diagnostikim, regjistra, qendër ndihme, kontakt"; /* This is a comma separated list of keywords used for spotlight indexing of the 'Me' tab. */ "wordpress, me, app settings, settings, cache, media, about, upload, usage, statistics" = "wordpress, mua, rregullime aplikacioni, rregullime, fshehtinë, media, mbi, ngarkim, përdorim, statistika"; diff --git a/WordPress/Resources/sv.lproj/Localizable.strings b/WordPress/Resources/sv.lproj/Localizable.strings index 7499c6c03a65..34b66ea1c16f 100644 --- a/WordPress/Resources/sv.lproj/Localizable.strings +++ b/WordPress/Resources/sv.lproj/Localizable.strings @@ -1,4 +1,4 @@ -/* Translation-Revision-Date: 2023-10-18 12:54:08+0000 */ +/* Translation-Revision-Date: 2024-01-03 12:51:39+0000 */ /* Plural-Forms: nplurals=2; plural=n != 1; */ /* Generator: GlotPress/4.0.0-alpha.11 */ /* Language: sv_SE */ @@ -152,9 +152,6 @@ /* Difference label for Period Overview stat, indicating change from previous period. Ex: +99.9K (5%) */ "%@%@ (%@%%)" = "%1$@%2$@ (%3$@ %%)"; -/* Accessibility label for a post in the post list. The parameters are the title, and date respectively. For example, \"Let it Go, 1 hour ago.\" */ -"%@, %@." = "%1$@, %2$@."; - /* Accessibility value for a Stats' Posting Activity Month if the user selected a day with posts. The first parameter is day (e.g. November 2019). The second parameter is the number of posts. */ "%@. %d posts." = "%1$@. %2$d inlägg."; @@ -210,9 +207,6 @@ /* Age between dates over one year. */ "%d years" = "%d år"; -/* Max image size in pixels (e.g. 300x300px) */ -"%dx%dpx" = "%1$dx%2$dpx"; - /* One menu area available in the theme */ "%i menu area in this theme" = "%i menyområde i detta tema"; @@ -271,6 +265,9 @@ translators: Block name. %s: The localized block name */ /* translators: %s: social link name e.g: \"Instagram\". */ "%s social icon" = "Social ikon för %s"; +/* translators: displayed right after the classic block is converted to blocks. %s: The localized classic block name */ +"'%s' block converted to blocks" = "'%s' block konverterade till block"; + /* translators: Missing block alert title. %s: The localized block name */ "'%s' is not fully-supported" = "”%s” stöds inte fullt ut"; @@ -478,13 +475,6 @@ translators: Block name. %s: The localized block name */ /* Label for the Activity Type filter button when there are more than 1 activity type selected */ "Activity Type (%1$d)" = "Aktivitetstyp (%1$d)"; -/* Accessibility label for add button to add items to the user's media library - Remove asset from media picker list */ -"Add" = "Lägg till"; - -/* Action for Media Picker to indicate selection of media. The argument in the string represents the number of elements (as numeric digits) selected */ -"Add %@" = "Lägg till %@"; - /* No comment provided by engineer. */ "Add Block After" = "Lägg till block efter"; @@ -581,9 +571,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Add menu item to children" = "Lägg till menyobjekt till underordnad"; -/* Accessibility hint for add button to add items to the user's media library */ -"Add new media" = "Lägg till ny mediefil"; - /* Screen reader text for Menus button that adds a new menu to a site. */ "Add new menu" = "Lägg till meny"; @@ -649,9 +636,6 @@ translators: Block name. %s: The localized block name */ /* Option to select the Airmail app when logging in with magic links */ "Airmail" = "Airmail"; -/* Description of albums in the photo libraries */ -"Albums" = "Album"; - /* Image alignment option title. Title of the screen for choosing an image's alignment. */ "Alignment" = "Justering"; @@ -665,6 +649,9 @@ translators: Block name. %s: The localized block name */ Title of the drafts filter. This filter shows a list of draft posts. */ "All" = "Alla"; +/* Information about redeeming domain credit on site dashboard. */ +"All WordPress.com annual plans include a custom domain name. Register your free domain now." = "Alla årliga WordPress.com-paket inkluderar ett anpassat domännamn. Registrera din gratis domän nu."; + /* Footer of the free domain registration section for a paid plan. Information about redeeming domain credit on site dashboard. */ "All WordPress.com plans include a custom domain name. Register your free premium domain now." = "Nu innehåller alla prispaket hos WordPress.com ett anpassat domännamn. Registrera ditt fria domännamn nu."; @@ -730,10 +717,13 @@ translators: Block name. %s: The localized block name */ "Alt Text" = "Alt-text"; /* No comment provided by engineer. */ -"Alternatively, you can detach and edit these blocks separately by tapping “Detach patterns”." = "Du kan även frikoppla och redigera dessa block separat genom att trycka på Frikoppla mönster."; +"Alternatively, you can convert the content to blocks." = "Alternativt kan du konvertera innehållet till block."; /* No comment provided by engineer. */ -"Alternatively, you can detach and edit this block separately by tapping “Detach pattern”." = "Du kan även frikoppla och redigera det här blocket separat genom att trycka på Frikoppla mönster."; +"Alternatively, you can detach and edit this block separately by tapping “Detach”." = "Alternativt kan du ta bort bifogning och redigera detta block separat genom att trycka på ”Ta bort bifogning”."; + +/* translators: Alternative option included in a warning related to having blocks deeply nested. */ +"Alternatively, you can flatten the content by ungrouping the block." = "Alternativt kan du platta ut innehållet genom att avgruppera blocket."; /* Instruction text to explain to help users type their password instead of using magic link login option. */ "Alternatively, you may enter the password for this account." = "Alternativt kan du ange lösenordet för detta konto."; @@ -809,7 +799,7 @@ translators: Block name. %s: The localized block name */ "An unknown error occurred." = "Ett okänt fel uppstod."; /* Error message shown when a media upload fails for unknown reason and the user should try again. */ -"An unknown error occurred. Please try again." = "Ett okänt fel har inträffatt. Försök igen."; +"An unknown error occurred. Please try again." = "Ett okänt fel uppstod. Försök igen."; /* Insights 'This Year' details view header */ "Annual Site Stats" = "Årstatistik för webbplatsen"; @@ -855,7 +845,7 @@ translators: Block name. %s: The localized block name */ "Approve" = "Godkänn"; /* Approves a Comment */ -"Approve Comment" = "Godkänn"; +"Approve Comment" = "Godkänn kommentar"; /* Button title for Approved comment state. Title of approved Comments filter. */ @@ -874,7 +864,7 @@ translators: Block name. %s: The localized block name */ "Are you sure you want to continue?\n All site data will be removed from your %@." = "Är du säker på att du vill fortsätta?\n Alla webbplatsdata tas bort från din %@."; /* Menus confirmation text for confirming if a user wants to delete a menu. */ -"Are you sure you want to delete the menu?" = "Är det säkert att du vill radera menyn?"; +"Are you sure you want to delete the menu?" = "Är det säkert att du vill ta bort menyn?"; /* Message asking for confirmation on tag deletion */ "Are you sure you want to delete this tag?" = "Är du säker på att du vill ta bort etiketten?"; @@ -882,14 +872,8 @@ translators: Block name. %s: The localized block name */ /* Message prompting the user to confirm that they want to disconnect Jetpack from the site. */ "Are you sure you want to disconnect Jetpack from the site?" = "Är du säker på att du vill koppla bort Jetpack från webbplatsen?"; -/* Message prompting the user to confirm that they want to permanently delete a group of media items. */ -"Are you sure you want to permanently delete these items?" = "Är du säker på att du vill ta bort dessa objekt permanent?"; - /* Message prompting the user to confirm that they want to permanently delete a media item. Should match Calypso. */ -"Are you sure you want to permanently delete this item?" = "Är du säker på att du vill radera detta objekt permanent?"; - -/* Message of the confirmation alert when deleting a page from the trash. */ -"Are you sure you want to permanently delete this page?" = "Är du säker på att du vill radera denna sida permanent?"; +"Are you sure you want to permanently delete this item?" = "Är du säker på att du vill ta bort detta objekt permanent?"; /* Message of the confirmation alert when deleting a post from the trash. */ "Are you sure you want to permanently delete this post?" = "Är du säker på att du vill radera detta inlägg permanent?"; @@ -922,11 +906,8 @@ translators: Block name. %s: The localized block name */ /* Title of message shown when user taps submit for review. */ "Are you sure you want to submit for review?" = "Är du säker på att du vill sända för granskning?"; -/* Message of the trash page confirmation alert. */ -"Are you sure you want to trash this page?" = "Är det säkert att du vill lägga denna sida i papperskorgen?"; - /* Message of the trash confirmation alert. */ -"Are you sure you want to trash this post?" = "Är du säker på att du vill radera detta inlägg?"; +"Are you sure you want to trash this post?" = "Är du säker på att du vill slänga detta inlägg?"; /* Title of message shown when user taps continue during homepage editing in site creation. */ "Are you sure you want to update your homepage?" = "Är du säker på att du vill uppdatera startsidan?"; @@ -965,9 +946,6 @@ translators: Block name. %s: The localized block name */ /* translators: accessibility text. Empty Audio caption. */ "Audio caption. Empty" = "Beskrivning för ljudfil. Tom"; -/* Accessibility label for audio items in the media collection view. The parameter is the creation date of the audio. */ -"Audio, %@" = "Ljudfil, %@"; - /* No comment provided by engineer. */ "Authenticating" = "Autentiserar"; @@ -1178,10 +1156,7 @@ translators: Block name. %s: The localized block name */ "Blocks menu" = "Block-meny"; /* translators: Warning related to having blocks deeply nested. %d: The deepest nesting level. */ -"Blocks nested deeper than %d levels may not render properly in the mobile editor. For this reason, we recommend flattening the content by ungrouping the block or editing the block using the web editor." = "Block som är inbäddade djupare än %d kanske inte renderas korrekt i den mobila redigeraren. Av den här anledningen rekommenderar vi att du plattar ut innehållet genom att avgruppera blocket eller att du redigerar blocket med webbredigeraren."; - -/* translators: Warning related to having blocks deeply nested. %d: The deepest nesting level. */ -"Blocks nested deeper than %d levels may not render properly in the mobile editor. For this reason, we recommend flattening the content by ungrouping the block or editing the block using your web browser." = "Block som är inbäddade djupare än %d kanske inte renderas korrekt i den mobila redigeraren. Av den här anledningen rekommenderar vi att du plattar ut innehållet genom att avgruppera blocket eller att du redigerar blocket i din webbläsare."; +"Blocks nested deeper than %d levels may not render properly in the mobile editor." = "Block som är inbäddade djupare än %d kanske inte renderas korrekt i den mobila redigeraren."; /* Title of a button that displays the WordPress.com blog */ "Blog" = "Blogg"; @@ -1209,7 +1184,7 @@ translators: Block name. %s: The localized block name */ /* Accessibility label for bold button on formatting toolbar. Discoverability title for bold formatting keyboard shortcut. */ -"Bold" = "Fetstil"; +"Bold" = "Fet"; /* Books site intent topic */ "Books" = "Böcker"; @@ -1259,9 +1234,6 @@ translators: Block name. %s: The localized block name */ /* Label for the post author in the post detail. */ "By " = "av"; -/* Accessibility label for the post author in the post list. The parameter is the author name. For example, \"By Elsa.\" */ -"By %@." = "Av %@."; - /* Legal disclaimer for logging in. The underscores _..._ denote underline. */ "By continuing, you agree to our _Terms of Service_." = "Genom att fortsätta samtycker du till våra _användarvillkor_."; @@ -1279,10 +1251,9 @@ translators: Block name. %s: The localized block name */ "CUSTOMIZE" = "ANPASSA"; /* Label for size of media while it's being calculated. */ -"Calculating..." = "Beräknar..."; +"Calculating..." = "Beräknar …"; -/* Accessibility label for taking an image or video with the camera on formatting toolbar. - Accessibility label for the camera tile in the collection view */ +/* Accessibility label for taking an image or video with the camera on formatting toolbar. */ "Camera" = "Kamera"; /* Title of an alert informing the user the camera permission for the app is disabled and its needed to proceed */ @@ -1333,10 +1304,7 @@ translators: Block name. %s: The localized block name */ Cancels an Action Cancels an alert. Cancels the mark all as read action. - Dismiss the post action sheet Editing GIF alert cancel action button. - Label for a cancel button - Label for the auto-upload cancelation button in the post list. Tapping will prevent the app from auto-uploading the post. Menus cancel button for deleting a menu. Menus cancel button within text bar while editing items. Menus: Cancel button title for canceling an edited menu item. @@ -1353,10 +1321,6 @@ translators: Block name. %s: The localized block name */ Verb. Title for Jetpack Restore cancel button. */ "Cancel" = "Avbryt"; -/* Label for the Post List option that cancels automatic uploading of a post. - Media Library option to cancel an in-progress or failed upload. */ -"Cancel Upload" = "Avbryt uppladdning"; - /* Menus alert button title to cancel discarding changes and not createa a new menu. Menus alert button title to cancel discarding changes and not select a new menu Menus alert button title to cancel discarding changes and not select a new menu location */ @@ -1375,7 +1339,7 @@ translators: Block name. %s: The localized block name */ "Canceled" = "Avbruten"; /* A short message that informs the user the share extension is being canceled. */ -"Canceling..." = "Avbryter..."; +"Canceling..." = "Avbryter …"; /* Error message shown when the input URL does not point to an existing site. */ "Cannot access the site at this address. Please double-check and try again." = "Det går inte komma åt webbplatsen på den här adressen. Dubbelkolla och försök igen."; @@ -1406,7 +1370,7 @@ translators: Block name. %s: The localized block name */ "Category title missing." = "Kategorirubrik saknas."; /* Center alignment for an image. Should be the same as in core WP. */ -"Center" = "Mitten"; +"Center" = "Centrerat"; /* Popup title for wrong SSL certificate. */ "Certificate error" = "Certifikat-fel"; @@ -1415,9 +1379,6 @@ translators: Block name. %s: The localized block name */ Main title */ "Change Password" = "Ändra lösenord"; -/* Title of button that takes user to the system Settings section for the app */ -"Change Settings" = "Ändra inställningar"; - /* Change Username title. */ "Change Username" = "Ändra användarnamn"; @@ -1428,7 +1389,7 @@ translators: Block name. %s: The localized block name */ "Change block position" = "Ändra blockets position"; /* Message to show when Publicize globally shared setting failed */ -"Change failed" = "Förändringen misslyckades"; +"Change failed" = "Ändring misslyckades"; /* Accessibility hint for web preview device switching button */ "Change the device type used for preview" = "Ändra typen av enhet som används för förhandsgranskning"; @@ -1499,7 +1460,7 @@ translators: Block name. %s: The localized block name */ "Check your site title" = "Kontrollera din webbplatsrubrik"; /* Overlay message displayed while checking if site has premium purchases */ -"Checking purchases…" = "Kontrollerar inköp..."; +"Checking purchases…" = "Kontrollerar köp …"; /* Title of button that asks the users if they'd like to focus on checking their sites stats */ "Checking stats" = "Kolla statistik"; @@ -1557,9 +1518,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Choose file" = "Välj fil"; -/* Menu option for selecting media from the device's photo library. */ -"Choose from My Device" = "Välj från min enhet"; - /* Explanatory text for Homepage Settings homepage type selection. */ "Choose from a homepage that displays your latest posts (classic blog) or a fixed \/ static page." = "Välj mellan en startsida som visar dina senaste inlägg (klassisk blogg) och en fast\/statisk sida."; @@ -1621,7 +1579,7 @@ translators: Block name. %s: The localized block name */ "Clear search history" = "Rensa sökhistorik"; /* Label for size of media while it's being cleared. */ -"Clearing..." = "Rensar..."; +"Clearing..." = "Rensar …"; /* Label for number of clicks. Period Stats 'Clicks' header */ @@ -1760,9 +1718,6 @@ translators: Block name. %s: The localized block name */ /* Community & Non-Profit site intent topic */ "Community & Non-Profit" = "Gemenskaper och ideella organisationer"; -/* Accessibility indication that the current Post List style is currently Compact. */ -"Compact" = "Kompakt"; - /* The action is completed */ "Completed" = "Färdig"; @@ -1809,7 +1764,7 @@ translators: Block name. %s: The localized block name */ "Completed: View your site" = "Slutförd: Visa din webbplats"; /* Section title for the configure table section in the blog details screen */ -"Configure" = "Ställ in"; +"Configure" = "Konfigurera"; /* Verb. Title for Jetpack Restore confirm button. */ "Confirm" = "Bekräfta"; @@ -1818,7 +1773,7 @@ translators: Block name. %s: The localized block name */ "Confirm Close Account" = "Bekräfta kontoavslut"; /* Title of Delete Site confirmation alert */ -"Confirm Delete Site" = "Bekräfta radering av webbplats"; +"Confirm Delete Site" = "Bekräfta borttagning av webbplats"; /* Instructional text about the Sharing feature. */ "Confirm this is the account you would like to authorize. Note that your posts will be automatically shared to this account." = "Bekräfta att det är detta konto som du vill auktorisera. Observera att inläggen automatiskt delas med detta konto."; @@ -1948,10 +1903,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Copied block" = "Blocket kopierat"; -/* Copy the post url and paste anywhere in phone - Label for page copy link. Tapping copy the url of page */ -"Copy Link" = "Kopiera länk"; - /* Copy link to oomment button title */ "Copy Link to Comment" = "Kopiera länk till kommentaren"; @@ -2043,7 +1994,7 @@ translators: Block name. %s: The localized block name */ "Couldn't load tags." = "Det gick inte att ladda etiketterna."; /* Error message when tag loading failed */ -"Couldn't load tags. Tap to retry." = "Det gick inte att läsa in etiketterna. Knacka för att försöka igen."; +"Couldn't load tags. Tap to retry." = "Det gick inte att läsa in etiketter. Tryck för att försöka igen."; /* Indicating that referrer couldn't be marked as spam */ "Couldn't mark as spam" = "Kunde inte markera som skräppost"; @@ -2060,9 +2011,6 @@ translators: Block name. %s: The localized block name */ /* Error title displayed when unable to close user account. */ "Couldn’t close account automatically" = "Det gick inte att avsluta kontot automatiskt"; -/* Message to show while media data source is finding the number of items available. */ -"Counting media items..." = "Räknar medieobjekt..."; - /* Period Stats 'Countries' header */ "Countries" = "Länder"; @@ -2083,13 +2031,13 @@ translators: Block name. %s: The localized block name */ "Create" = "Skapa"; /* The button title text for creating a new account. */ -"Create Account" = "Skapa Konto"; +"Create Account" = "Skapa konto"; /* Title for button to make a blank page */ "Create Blank Page" = "Skapa en tom sida"; /* Create New header text */ -"Create New" = "Skapa nytt"; +"Create New" = "Skapa ny"; /* Title for the site creation flow. */ "Create New Site" = "Skapa ny webbplats"; @@ -2313,7 +2261,6 @@ translators: Block name. %s: The localized block name */ /* Delete Delete button title for the warning shown to the user when he refuses to re-login when the authToken is missing. Title for button that permanently deletes a media item (photo / video) - Title for button that permanently deletes one or more media items (photos / videos) Title of a delete button Title of the trash confirmation alert. */ "Delete" = "Kasta i papperskorgen"; @@ -2321,24 +2268,20 @@ translators: Block name. %s: The localized block name */ /* Menus confirmation button for deleting a menu. */ "Delete Menu" = "Radera meny"; -/* Delete option in the confirmation alert when deleting a page from the trash. - Delete option in the confirmation alert when deleting a post from the trash. - Label for a button permanently deletes a page. - Label for the delete post option. Tapping permanently deletes a post. +/* Delete option in the confirmation alert when deleting a post from the trash. Title for button on the comment details page that deletes the comment when tapped. */ -"Delete Permanently" = "Radera permanent"; +"Delete Permanently" = "Ta bort permanent"; -/* Title of the confirmation alert when deleting a page from the trash. - Title of the confirmation alert when deleting a post from the trash. */ +/* Title of the confirmation alert when deleting a post from the trash. */ "Delete Permanently?" = "Radera permanent?"; /* Button label for deleting the current site Label for selecting the Delete Site Settings item Title of settings page for deleting a site */ -"Delete Site" = "Radera webbplats"; +"Delete Site" = "Ta bort webbplats"; /* Title of alert when site deletion fails */ -"Delete Site Error" = "Radera webbplats-fel"; +"Delete Site Error" = "Ta bort webbplats-fel"; /* Screen Reader: Button that deletes a menu. */ "Delete menu" = "Ta bort meny"; @@ -2350,10 +2293,10 @@ translators: Block name. %s: The localized block name */ "Deleted!" = "Raderat!"; /* Overlay message displayed while deleting site */ -"Deleting site…" = "Raderar webbplats…"; +"Deleting site…" = "Tar bort webbplats …"; /* Text displayed in HUD while a media item is being deleted. */ -"Deleting..." = "Tar bort..."; +"Deleting..." = "Tar bort …"; /* Verb. Denies a 2fa authentication challenge. */ "Deny" = "Neka"; @@ -2393,15 +2336,15 @@ translators: Block name. %s: The localized block name */ "Disable invite link" = "Inaktivera inbjudningslänk"; /* Adjective. Comment threading is disabled. */ -"Disabled" = "Inaktiverad (originalstorlek)"; +"Disabled" = "Inaktiverad"; /* Button shown if there are unsaved changes and the author cancelled editing a Comment. Button shown if there are unsaved changes and the author is trying to move away from the post. Menus button title for canceling/discarding changes made. */ -"Discard" = "Spara inte"; +"Discard" = "Förkasta"; /* Menus alert button title to discard changes. */ -"Discard Changes" = "Kasta bort ändringar"; +"Discard Changes" = "Kasta ändringar"; /* Menus alert button title to continue creating a menu and discarding current changes. */ "Discard and Create New Menu" = "Släng och skapa ny meny"; @@ -2448,7 +2391,6 @@ translators: Block name. %s: The localized block name */ /* Accessibility label for button to dismiss a bottom sheet Accessibility label for the transparent space above the login dialog which acts as a button to dismiss the dialog. Accessibility label for the transparent space above the signup dialog which acts as a button to dismiss the dialog. - Action to show on alert when view asset fails. Dismiss a view. Verb */ "Dismiss" = "Avfärda"; @@ -2466,12 +2408,6 @@ translators: Block name. %s: The localized block name */ User's Display Name */ "Display Name" = "Visningsnamn"; -/* Accessibility label for other media items in the media collection view. The parameter is the creation date of the document. */ -"Document, %@" = "Dokument, %@"; - -/* Accessibility label for other media items in the media collection view. The parameter is the filename file. */ -"Document: %@" = "Dokument: %@"; - /* Message shown when all tours have been completed */ "Doesn't it feel good to cross things off a list?" = "Visst är det skönt att kunna bocka av saker i en lista?"; @@ -2527,7 +2463,7 @@ translators: Block name. %s: The localized block name */ "Double tap to add a link." = "Dubbeltryck för att lägga till en länk."; /* Voiceover accessibility hint informing the user they can double tap a modal alert to dismiss it */ -"Double tap to dismiss" = "Dubbelknacka för att stänga"; +"Double tap to dismiss" = "Dubbeltryck för att avfärda"; /* No comment provided by engineer. */ "Double tap to edit button text" = "Dubbeltryck för att redigera knapptexten"; @@ -2632,8 +2568,7 @@ translators: Block name. %s: The localized block name */ /* Description of a Quick Start Tour */ "Draft and publish a post." = "Gör ett utkast och publicera ett inlägg."; -/* Title of the drafts filter. This filter shows a list of draft posts. - Title of the drafts header in search list. */ +/* Title of the drafts filter. This filter shows a list of draft posts. */ "Drafts" = "Utkast"; /* No comment provided by engineer. */ @@ -2645,10 +2580,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Drag to adjust focal point" = "Dra för att flytta bildfokus"; -/* Label for page duplicate option. Tapping creates a copy of the page. - Label for post duplicate option. Tapping creates a copy of the post. */ -"Duplicate" = "Duplicera"; - /* No comment provided by engineer. */ "Duplicate block" = "Duplicera block"; @@ -2661,13 +2592,9 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Each block has its own settings. To find them, tap on a block. Its settings will appear on the toolbar at the bottom of the screen." = "Varje block har sina egna inställningar. För att se dem trycker du på blocket i fråga. Inställningarna visas i verktygsfältet längst ned på skärmen."; -/* Accessibility label for edit button to enable multi selection mode in the user's media library - Edit the post. - Editing GIF alert default action button. +/* Editing GIF alert default action button. Edits a Comment Edits the comment - Label for a button that opens the Edit Page view controller - Label for the edit post button. Tapping displays the editor. User action to edit media details. Verb, edit a comment */ "Edit" = "Redigera"; @@ -2675,9 +2602,6 @@ translators: Block name. %s: The localized block name */ /* Title for the edit more button section */ "Edit \"More\" button" = "Redigera knapenn ”Mer”"; -/* Button that displays the media editor to the user */ -"Edit %@" = "Redigera %@"; - /* Blocklist Keyword Edition Title */ "Edit Blocklist Word" = "Redigera ord i blockeringslistan"; @@ -2696,7 +2620,7 @@ translators: Block name. %s: The localized block name */ "Edit Post" = "Redigera inlägg"; /* Accessibility label for button to edit a comment from a notification */ -"Edit comment" = "Ändra kommentar"; +"Edit comment" = "Redigera kommentar"; /* No comment provided by engineer. */ "Edit cover media" = "Redigera omslagsmedia"; @@ -2735,7 +2659,7 @@ translators: Block name. %s: The localized block name */ "Editing this GIF will remove its animation." = "Om du redigerar den här GIF-bilden tas animeringen bort."; /* Title for the editor settings section */ -"Editor" = "Redigeringsverktyg"; +"Editor" = "Redigerare"; /* Edit Action Spoken hint. */ "Edits a comment" = "Redigerar kommentaren"; @@ -2744,7 +2668,7 @@ translators: Block name. %s: The localized block name */ "Edits the comment." = "Redigerar kommentaren."; /* Screen reader hint for button to edit a menu item */ -"Edits this menu item" = "Lägg till detta menyval"; +"Edits this menu item" = "Redigerar detta menyval"; /* Education site intent topic */ "Education" = "Utbildning"; @@ -2812,7 +2736,7 @@ translators: Block name. %s: The localized block name */ "Empty" = "Tomt"; /* Message to show to user when he tries to add a self-hosted site that is an empty URL. */ -"Empty URL" = "URL ej angiven"; +"Empty URL" = "Tom URL"; /* Button title for the enable site notifications action. Button title to enable notifications for new comments @@ -2862,7 +2786,7 @@ translators: Block name. %s: The localized block name */ "Enter a custom value" = "Mata in ett anpassat värde"; /* No comment provided by engineer. */ -"Enter a password" = "Lösenord"; +"Enter a password" = "Ange ett lösenord"; /* Message explaining why the user might enter a password. */ "Enter a password to protect this post" = "Ange ett lösenord för att skydda detta inlägg"; @@ -2870,12 +2794,9 @@ translators: Block name. %s: The localized block name */ /* Secondary message shown when there are no domains that match the user entered text. */ "Enter different words above and we'll look for an address that matches it." = "Skriv in olika ord ovan så letar vi efter en adress som matchar dem."; -/* Accessibility hint for edit button to enable multi selection mode in the user's media library */ -"Enter edit mode to enable multi select to delete" = "Gå till redigeringsläget för att kunna markera flera objekt för borttagning"; - /* (placeholder) Help enter WordPress password Placeholder of a field to type a password to protect the post. */ -"Enter password" = "Ditt lösenord"; +"Enter password" = "Ange lösenord"; /* Placeholder text prompting the user to type the name of the URL they would like to follow. */ "Enter the URL of a site to follow" = "Ange URL för en webbsida för att följa"; @@ -2888,7 +2809,7 @@ translators: Block name. %s: The localized block name */ "Enter the password for your WordPress.com account." = "Ange lösenordet för ditt konto på WordPress.com."; /* (placeholder) Help enter WordPress username */ -"Enter username" = "Ditt användarnamn"; +"Enter username" = "Ange användarnamn"; /* Enter your account information for {site url}. Asks the user to enter a username and password for their self-hosted site. */ "Enter your account information for %@." = "Ange din kontoinformation för %@."; @@ -2960,7 +2881,7 @@ translators: Block name. %s: The localized block name */ "Error loading plugins" = "Fel vid laddning av tilläggen"; /* Text displayed when there is a failure loading a blogging prompt. */ -"Error loading prompt" = "Kunde inte ladda uppmaningen"; +"Error loading prompt" = "Kunde inte hämta förslag"; /* Message displayed when spamming a comment fails. */ "Error marking comment as spam." = "Fel när kommentar markerades som skräppost."; @@ -3028,9 +2949,6 @@ translators: Block name. %s: The localized block name */ /* Short title telling the user they will receive a blogging reminder every day of the week. */ "Every day at %@" = "Varje dag kl. %@"; -/* Label for the post author filter. This filter shows posts for all users on the blog. */ -"Everyone" = "Alla"; - /* Example story title description */ "Example story title" = "Exempelrubrik för berättelse"; @@ -3040,9 +2958,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Excerpt length (words)" = "Utdragets längd (antal ord)"; -/* Accessibility label for a post's excerpt in the post list. The parameter is the post excerpt. For example, \"Excerpt. This is the first paragraph.\" */ -"Excerpt. %@." = "Utdrag. %@."; - /* Should be the same as the text displayed if the user clicks the (i) in Calypso. */ "Excerpts are optional hand-crafted summaries of your content." = "Utdrag är valfria summeringar av innehåll som du skriver själv."; @@ -3052,8 +2967,7 @@ translators: Block name. %s: The localized block name */ /* Accessibility Label for the exit full screen button on the full screen comment reply mode */ "Exit Full Screen" = "Avsluta fullskärmsläge"; -/* Accessibility indication that the current Post List style is currently Expanded. - Screen reader text to represent the expanded state of a UI control */ +/* Screen reader text to represent the expanded state of a UI control */ "Expanded" = "Expanderad"; /* Accessibility hint */ @@ -3094,7 +3008,7 @@ translators: Block name. %s: The localized block name */ "Export Your Content" = "Exportera ditt innehåll"; /* Overlay message displayed while starting content export */ -"Exporting content…" = "Exporterar innehåll..."; +"Exporting content…" = "Exporterar innehåll …"; /* Section title for the external table section in the blog details screen */ "External" = "Externt"; @@ -3103,9 +3017,6 @@ translators: Block name. %s: The localized block name */ The action failed */ "Failed" = "Misslyckades"; -/* Error title when picked media cannot be imported into stories. */ -"Failed Media Export" = "Export av mediafil misslyckades"; - /* Message for mark all as read success notice */ "Failed marking Notifications as read" = "Lyckades inte markera aviseringarna som lästa"; @@ -3113,7 +3024,7 @@ translators: Block name. %s: The localized block name */ "Failed to insert audio file. Please tap for options." = "Kunde inte lägga in ljudfil. Tryck för att visa alternativ."; /* Error message to show to use when media insertion on a post fails */ -"Failed to insert media.\n Please tap for options." = "Det gick inte att placera in media.\nKnacka här för alternativ."; +"Failed to insert media.\n Please tap for options." = "Kunde inte infoga media.\nTryck för alternativ."; /* No comment provided by engineer. */ "Failed to insert media.\nTap for more info." = "Misslyckades att infoga media.\nTryck för mer information."; @@ -3147,24 +3058,24 @@ translators: Block name. %s: The localized block name */ "Featured Image did not load" = "Utvald bild kunde inte laddas in"; /* Text displayed while fetching themes */ -"Fetching Themes..." = "Hämtar teman..."; +"Fetching Themes..." = "Hämtar teman …"; /* A brief prompt shown when the comment list is empty, letting the user know the app is currently fetching new comments. */ -"Fetching comments..." = "Kommentarer hämtas ..."; +"Fetching comments..." = "Hämtar kommentarer …"; /* Title displayed whilst fetching media from the user's media library */ -"Fetching media..." = "Hämtar media…"; +"Fetching media..." = "Hämtar media …"; /* A brief prompt shown when the reader is empty, letting the user know the app is currently fetching new pages. */ -"Fetching pages..." = "Hämtar sidor ..."; +"Fetching pages..." = "Hämtar sidor …"; /* A brief prompt shown when the reader is empty, letting the user know the app is currently fetching new posts. */ -"Fetching posts..." = "Inlägg hämtas ..."; +"Fetching posts..." = "Hämtar inlägg …"; /* A brief prompt when the user is searching for sites in the Reader. A short message to inform the user data for their followed sites is being fetched.. A short message to inform the user data for their sites are being fetched. */ -"Fetching sites..." = "Hämtar webbplatser ..."; +"Fetching sites..." = "Hämtar webbplatser …"; /* Label for the posting activity legend. */ "Fewer Posts" = "Färre inlägg"; @@ -3203,7 +3114,7 @@ translators: Block name. %s: The localized block name */ "Find your site address" = "Hitta adressen till din webbplats"; /* Label displayed to the user while loading their selected interests */ -"Finding blogs and stories you’ll love..." = "Letar efter bloggar och historier du kommer att gilla ..."; +"Finding blogs and stories you’ll love..." = "Letar efter bloggar och historier du kommer att gilla …"; /* My Profile first name label Register Domain - Domain contact information field First name @@ -3307,11 +3218,17 @@ translators: Block name. %s: The localized block name */ /* An example tag used in the login prologue screens. */ "Football" = "Fotboll"; +/* translators: Recommendation included in a warning related to having blocks deeply nested. */ +"For this reason, we recommend editing the block using the web editor." = "Av den här anledningen rekommenderar vi att du redigerar blocket med webbredigeraren."; + +/* translators: Recommendation included in a warning related to having blocks deeply nested. */ +"For this reason, we recommend editing the block using your web browser." = "Av denna anledning rekommenderar vi att du redigerar blocket med din webbläsare."; + /* Register Domain - Domain contact information section header description */ "For your convenience, we have pre-filled your WordPress.com contact information. Please review to be sure it’s the correct information you want to use for this domain." = "För din bekvämlighet har vi redan fyllt i dina kontaktuppgifter från WordPress.com. Granska dem för att kontrollera att det är den korrekta informationen som du vill använda för denna domän."; /* Next web page */ -"Forward" = "Vidarebefodra"; +"Forward" = "Framåt"; /* No comment provided by engineer. */ "Four" = "Fyra"; @@ -3598,7 +3515,7 @@ translators: Block name. %s: The localized block name */ "Hide All Sites" = "Dölj alla webbplatser"; /* No comment provided by engineer. */ -"Hide keyboard" = "Göm tangentbord"; +"Hide keyboard" = "Dölj tangentbord"; /* No comment provided by engineer. */ "Hide search heading" = "Dölj sökrubrik"; @@ -3624,8 +3541,7 @@ translators: Block name. %s: The localized block name */ "Home" = "Hem"; /* Title for setting which shows the current page assigned as a site's homepage - Title for the homepage section in site settings screen - Title of the Homepage Badge */ + Title for the homepage section in site settings screen */ "Homepage" = "Startsida"; /* Label for Homepage Settings site settings section @@ -3722,11 +3638,8 @@ translators: Block name. %s: The localized block name */ /* Hint for image title on image settings. */ "Image title" = "Bildrubrik"; -/* Accessibility label for image thumbnails in the media collection view. The parameter is the creation date of the image. */ -"Image, %@" = "Bild, %@"; - /* Undated post time label */ -"Immediately" = "Direkt"; +"Immediately" = "Genast"; /* Footer for the Serve images from our servers setting */ "Improve your site's speed by only loading images visible on the screen. New images will load just before they scroll into view. This prevents viewers from having to download all the images on a page all at once, even ones they can't see." = "Snabba upp din webbplats genom att bara ladda ned bilder som syns på skärmen. Ytterligare bilder kommer att laddas ned precis innan deras position rullas fram. Detta gör så att besökare inte tvingas ladda hem alla bilder direkt, inklusive bilder som de ändå inte kan se."; @@ -3756,7 +3669,7 @@ translators: Block name. %s: The localized block name */ "Include a Blogging Prompt" = "Inkludera ett bloggningsförslag"; /* Describes a standard *.wordpress.com site domain */ -"Included with Site" = "Ingår i webbplatsen"; +"Included with Site" = "Inkluderad med webbplatsen"; /* Downloadable/Restorable items: general section footer text */ "Includes wp-config.php and any non WordPress files" = "Innehåller wp-config.php och eventuella filer som inte hör till WordPress"; @@ -3844,7 +3757,7 @@ translators: Block name. %s: The localized block name */ "Installed" = "Installerat"; /* Title of a progress view displayed while the first plugin for a site is being installed. */ -"Installing %@…" = "Installerar %@…"; +"Installing %@…" = "Installerar %@ …"; /* The Jetpack view title used while the is installing */ "Installing Jetpack" = "Installerar Jetpack"; @@ -3880,7 +3793,7 @@ translators: Block name. %s: The localized block name */ "Invalid URL scheme inserted, only HTTP and HTTPS are supported." = "Ogiltigt URL-schema infogat, endast HTTP och HTTPS stöds."; /* Message to show to user when he tries to add a self-hosted site that isn't a valid URL. */ -"Invalid URL, please check if you wrote a valid site address." = "Ogiltig URL, kolla om du skrev en giltig webbplatsadress."; +"Invalid URL, please check if you wrote a valid site address." = "Ogiltig URL, kontrollera att du skrev en giltig webbplatsadress."; /* No comment provided by engineer. */ "Invalid URL." = "Ogiltig URL."; @@ -4078,7 +3991,7 @@ translators: Block name. %s: The localized block name */ "Latest Post Summary" = "Sammanfattning av senaste inlägg"; /* Title of a button. Tapping allows the user to learn more about the specific error. */ -"Learn More" = "Läs mer"; +"Learn More" = "Lär dig mer"; /* Body text of the first alert preparing users to grant permission for us to send them push notifications. */ "Learn about new comments, likes, and follows in seconds." = "Få information om nya kommentarer, gilla-märkningar och följare på några sekunder."; @@ -4090,7 +4003,7 @@ translators: Block name. %s: The localized block name */ Link to cookie policy Menu title to show the prompts feature introduction modal. Read more button title shown in alert announcing new Create Button feature. */ -"Learn more" = "Läs mer"; +"Learn more" = "Lär dig mer"; /* Writing, Date and Time Settings: Learn more about date and time settings footer text */ "Learn more about date and time formatting." = "Läs mer om formatering av datum och tid."; @@ -4160,7 +4073,7 @@ translators: Block name. %s: The localized block name */ "Likes the Comment." = "Gillar kommentaren."; /* Message to show when a request for a WP.com API endpoint is throttled */ -"Limit reached. You can try again in 1 minute. Trying again before that will only increase the time you have to wait before the ban is lifted. If you think this is in error, contact support." = "Gräns nådd. Du kan försöka igen om 1 minut. Att försöka igen innan kommer bara öka tiden du måste vänta innan förbudet är upphävt. Vänligen kontakta support om du tror detta är ett fel."; +"Limit reached. You can try again in 1 minute. Trying again before that will only increase the time you have to wait before the ban is lifted. If you think this is in error, contact support." = "Gränsen är nådd. Du kan försöka igen om 1 minut. Att försöka igen innan dess kommer bara att öka den tid du måste vänta innan förbudet upphävs. Om du tror att detta är felaktigt, kontakta supporten."; /* This description is used to set the accessibility label for the Insights chart, with Views selected. */ "Line Chart depicting Views for insights." = "Linjediagram som visar antalet visningar för Insikter."; @@ -4188,7 +4101,7 @@ translators: Block name. %s: The localized block name */ "Link Rel" = "Länk-rel"; /* Noun. Title for screen in editor that allows to configure link options */ -"Link Settings" = "Länkalternativ"; +"Link Settings" = "Länkinställningar"; /* Noun. Label for the text of a link in the editor */ "Link Text" = "Länktext"; @@ -4210,9 +4123,6 @@ translators: Block name. %s: The localized block name */ Settings: Comments Approval settings */ "Links in comments" = "Länkar i kommentarer"; -/* The accessibility label for the list style button in the Post List. */ -"List style" = "Stil på listan"; - /* Title of the screen that load selected the revisions. */ "Load" = "Ladda"; @@ -4223,28 +4133,22 @@ translators: Block name. %s: The localized block name */ "Loading" = "Laddar in"; /* Text displayed while loading the activity feed for a site */ -"Loading Activities..." = "Läser in aktiviteter..."; +"Loading Activities..." = "Laddar in aktiviteter …"; /* Text displayed while loading the activity feed for a site */ "Loading Backups..." = "Läser in säkerhetskopior …"; -/* Phrase to show when the user has searched for GIFs and they are being loaded. */ -"Loading GIFs..." = "Läser in GIF-bilder..."; - /* Menus label text displayed while menus are loading */ -"Loading Menus..." = "Laddar menyer..."; +"Loading Menus..." = "Laddar in menyer …"; /* Text displayed while loading site People. */ -"Loading People..." = "Läser in personer..."; - -/* Phrase to show when the user has searched for images and they are being loaded. */ -"Loading Photos..." = "Laddar foton…"; +"Loading People..." = "Laddar in personer …"; /* Text displayed while loading plans details */ -"Loading Plan..." = "Laddar paket..."; +"Loading Plan..." = "Laddar in paket …"; /* Text displayed while loading plans details */ -"Loading Plans..." = "Laddar paket..."; +"Loading Plans..." = "Laddar in paket …"; /* Text displayed while loading an specific plugin */ "Loading Plugin..." = "Laddar tillägg…"; @@ -4253,7 +4157,7 @@ translators: Block name. %s: The localized block name */ "Loading Plugins..." = "Läser in tillägg…"; /* Text displayed while loading the scan history for a site */ -"Loading Scan History..." = "Laddar in genomsökningshistoriken ..."; +"Loading Scan History..." = "Läser in genomsökningshistoriken …"; /* Text displayed while loading the scan section for a site */ "Loading Scan..." = "Laddar in genomsökningen …"; @@ -4265,19 +4169,19 @@ translators: Block name. %s: The localized block name */ "Loading domains" = "Laddar in domäner"; /* Displayed while a call is loading the history. */ -"Loading history..." = "Laddar historiken ..."; +"Loading history..." = "Laddar in historik …"; /* Menus label text displayed when a menu is loading. */ -"Loading menu..." = "Laddar meny..."; +"Loading menu..." = "Laddar in meny …"; /* Messaged displayed when fetching plugins. */ "Loading plugins..." = "Laddar tillägg…"; /* Displayed while blogging prompts are being loaded. */ -"Loading prompts..." = "Laddar uppmaningar..."; +"Loading prompts..." = "Laddar förslag …"; /* A short message to inform the user the requested stream is being loaded. */ -"Loading stream..." = "Laddar ström..."; +"Loading stream..." = "Laddar in ström …"; /* Loading message shown while the Unsupported Block Editor is loading. */ "Loading the block editor." = "Blockredigeraren laddas."; @@ -4295,13 +4199,12 @@ translators: Block name. %s: The localized block name */ "Loading..." = "Laddar in …"; /* Status for Media object that is only exists locally. */ -"Local" = "Lokala utkast"; +"Local" = "Lokalt"; /* Local Services site intent topic */ "Local Services" = "Lokala tjänster"; -/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. - Title of the Local Changes Badge */ +/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. */ "Local changes" = "Lokala ändringar"; /* Title for alert when a generic error happened when trying to find the location of the device */ @@ -4377,7 +4280,7 @@ translators: Block name. %s: The localized block name */ "Mail (Default)" = "E-post (standardval)"; /* No comment provided by engineer. */ -"Main Navigation" = "Huvudmeny"; +"Main Navigation" = "Huvudnavigering"; /* No comment provided by engineer. */ "Make your content stand out by adding images, gifs, videos, and embedded media to your pages." = "Förstärk ditt innehåll genom att lägga till bilder, gif-illustrationer, videoklipp och inbäddade media i ditt sidinnehåll."; @@ -4465,7 +4368,6 @@ translators: Block name. %s: The localized block name */ "Max Video Upload Size" = "Maximal uppladdningsstorlek för videoklipp"; /* Accessibility label for the Me button in My Site. - Label for the post author filter. This filter shows posts only authored by the current user. Me page title Noun. Title. Links to the Me screen. The accessibility value of the me tab. @@ -4473,9 +4375,7 @@ translators: Block name. %s: The localized block name */ "Me" = "Jag"; /* Noun. Title. Links to the blog's Media library. - Tab bar title for the Media tab in Media Picker The menu item to select during a guided tour. - Title for Media Library section of the app. Title for the media section in site settings screen Title label for the media settings section in the app settings */ "Media" = "Media"; @@ -4487,13 +4387,6 @@ translators: Block name. %s: The localized block name */ /* Label for size of media cache in the app. */ "Media Cache Size" = "Mediacachens storlek"; -/* Title for alert when access to media capture is not granted */ -"Media Capture" = "Mediainspelning"; - -/* Title for alert when a generic error happened when loading media - Title for alert when access to the media library is not granted by the user */ -"Media Library" = "Mediebibliotek"; - /* Title for action sheet with media options. */ "Media Options" = "Mediaalternativ"; @@ -4516,9 +4409,6 @@ translators: Block name. %s: The localized block name */ /* translators: %s: block title e.g: \"Paragraph\". */ "Media options" = "Mediaalternativ"; -/* Alert title when there is issues loading an asset to preview. */ -"Media preview failed." = "Det gick inte att förhandsgranska media."; - /* Alert displayed to the user when multiple media items have uploaded successfully. */ "Media uploaded (%ld files)" = "Media uppladdat (%ld filer)"; @@ -4556,9 +4446,6 @@ translators: Block name. %s: The localized block name */ Label for the share message field on the post settings. */ "Message" = "Meddelande"; -/* Title of section containing image / video metadata such as size and file type */ -"Metadata" = "Meta-data"; - /* Option to select the Microsft Outlook app when logging in with magic links */ "Microsoft Outlook" = "Microsoft Outlook"; @@ -4578,13 +4465,11 @@ translators: Block name. %s: The localized block name */ "Months and Years" = "Månader och år"; /* Accessibility label for more button in dashboard quick start card. - Accessibility label for the More button in Page List. Accessibility label for the More button in Post List (compact view). Accessibility label for the More button on formatting toolbar. Accessibility label for the More button on Reader Cell Accessibility label for the More button on Reader's post details - Action button to display more available options - Label for the more post button. Tapping displays an action sheet with post options. */ + Action button to display more available options */ "More" = "Mer"; /* Action button to display more available options @@ -4642,15 +4527,8 @@ translators: Block name. %s: The localized block name */ /* Screen reader text for button that will move the menu item */ "Move menu item" = "Flytta menyval"; -/* Label for a button that moves a page to the draft folder - Label for an option that moves a post to the draft folder */ -"Move to Draft" = "Flytta till utkast"; - -/* Label for a button that moves a page to the trash folder - Label for a option that moves a post to the trash folder - Title for button on the comment details page that moves the comment to trash when tapped. +/* Title for button on the comment details page that moves the comment to trash when tapped. Trash option in the trash confirmation alert. - Trash option in the trash page confirmation alert. Trashes the comment */ "Move to Trash" = "Flytta till papperskorgen"; @@ -4682,7 +4560,8 @@ translators: Block name. %s: The localized block name */ Title of My Site tab */ "My Site" = "Min webbplats"; -/* Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ +/* Title for site picker screen. + Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ "My Sites" = "Mina webbplatser"; /* Siri Suggestion to open My Sites */ @@ -4725,7 +4604,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Navigates to the previous content sheet" = "Går till föregående innehållsblad"; /* 'Need help?' button label, links off to the WP for iOS FAQ. */ -"Need Help?" = "Hjälp"; +"Need Help?" = "Behöver du hjälp?"; /* A button title. */ "Need help finding your site address?" = "Behöver du hjälp att hitta adressen till din webbplats?"; @@ -4771,7 +4650,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "New posts" = "Nya inlägg"; /* Screen title, where users can see the newest plugins */ -"Newest" = "Senaste"; +"Newest" = "Nyaste"; /* Sort Order */ "Newest first" = "Nyast först"; @@ -4812,7 +4691,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "No Category" = "Ingen kategori"; /* Title of error prompt when no internet connection is available. */ -"No Connection" = "Ingen nätverksanslutning"; +"No Connection" = "Ingen anslutning"; /* List Editor Empty State Message */ "No Items" = "Inga poster"; @@ -4932,9 +4811,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title for the view when there aren't any Activities to display in the Activity Log for a given filter. */ "No matching events found." = "Inga matchande händelser hittades."; -/* Message displayed when no results are returned from a media library search. Should match Calypso. - Phrase to show when the user search for images but there are no result to show. - Phrase to show when the user searches for GIFs but there are no result to show. */ +/* Message displayed when no results are returned from a media library search. Should match Calypso. */ "No media matching your search" = "Ingen mediafil motsvarar din sökning"; /* Menus text shown when no menus were available for loading the Menus editor. */ @@ -4950,10 +4827,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ "No new topics to follow" = "Inga nya ämnen att följa"; /* Displayed in the Notifications Tab as a title, when there are no notifications */ -"No notifications yet" = "Inga notiser än"; +"No notifications yet" = "Inga aviseringar än"; -/* Displayed when the user is searching the pages list and there are no matching pages - Text displayed when there's no matching with the text search */ +/* Text displayed when there's no matching with the text search */ "No pages matching your search" = "Inga sidor matchar din sökning"; /* Text displayed when search for plugins returns no results */ @@ -4974,9 +4850,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Message shown whent the reader finds no posts for the chosen tag */ "No posts have been made recently with this tag." = "Inga inlägg har nyligen gjorts med denna etikett."; -/* Displayed when the user is searching the posts list and there are no matching posts */ -"No posts matching your search" = "Inga inlägg matchar din sökning"; - /* Accessibility value for a Stats' Posting Activity Month if there are no posts. */ "No posts." = "Inga inlägg."; @@ -4987,7 +4860,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "No primary site address found" = "Ingen primär webbplatsadress hittades"; /* Title displayed when there are no blogging prompts to display. */ -"No prompts yet" = "Inga uppmaningar än"; +"No prompts yet" = "Inga förslag än"; /* A message title */ "No recent posts" = "Inga nya inlägg"; @@ -5077,9 +4950,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* A message title */ "Nothing liked yet" = "Ingenting gillat än"; -/* Default message for empty media picker */ -"Nothing to show" = "Inget att visa"; - /* Notifications Details Accessibility Identifier */ "Notification Details Table" = "Notisinformationstabell"; @@ -5139,7 +5009,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Alert dismissal title Button title. Acknowledges a prompt. Button title. An acknowledgement of the message displayed in a prompt. - Confirmation of action Default action Dismisses the alert Marks all notifications as read. @@ -5154,7 +5023,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title of a button that dismisses a prompt Title of an OK button. Pressing the button acknowledges and dismisses a prompt. Title of primary button on alert prompting verify their accounts while attempting to publish */ -"OK" = "Fortsätt"; +"OK" = "OK"; /* No comment provided by engineer. */ "OPEN" = "ÖPPNA"; @@ -5201,9 +5070,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* No comment provided by engineer. */ "Only show excerpt" = "Visa endast utdrag"; -/* Message telling the user that they've only enabled limited photo library permissions for the app. */ -"Only the selected photos you've given access to are available." = "Endast de markerade fotografier som du har gett åtkomst till är tillgängliga."; - /* An informal exclaimation that means `something went wrong`. Title for the view when there's an error loading a comment. Title for the view when there's an error loading Activity Log @@ -5212,7 +5078,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title for the view when there's an error loading the history Title for the view when there's an error loading the plugin Title for the view when there's an error loading time zones */ -"Oops" = "Aj då"; +"Oops" = "Hoppsan"; /* An informal exclaimation meaning `something went wrong`. Title for the warning shown to the user when the app realizes there should be an auth token but there isn't one. */ @@ -5238,11 +5104,8 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title of a button that opens the apps settings in the system Settings.app */ "Open Settings" = "Öppna inställningar"; -/* Editor button to swich the media picker from quick mode to full picker */ -"Open full media picker" = "Öppna hela mediaväljaren"; - /* No comment provided by engineer. */ -"Open in Safari" = "Öppna länk i Safari"; +"Open in Safari" = "Öppna i Safari"; /* Label for the description of openening a link using a new window */ "Open in a new Window\/Tab" = "Öppna i nytt fönster\/ny flik"; @@ -5269,7 +5132,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Opportunities to participate in WordPress.com research & surveys." = "Möjligheter att delta i WordPress.coms studier och undersökningar."; /* Placeholder to indicate that filling out the field is optional. */ -"Optional" = "Ingen"; +"Optional" = "Valfritt"; /* Invite: Message Hint. %1$d is the maximum number of characters allowed. */ "Optional message up to %1$d characters to be included in the invitation." = "Valfritt meddelande på högst %1$d tecken som skickas tillsammans med inbjudan."; @@ -5280,6 +5143,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Divider on initial auth view separating auth options. */ "Or" = "eller"; +/* Instruction text for other forms of two-factor auth methods. */ +"Or choose another form of authentication." = "Eller välj en annan form av autentisering."; + /* Label for button to log in using site address. Underscores _..._ denote underline. */ "Or log in by _entering your site address_." = "Eller logga in genom att _skriva in adressen till din webbplats_."; @@ -5297,7 +5163,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Indicates a video will use its original size when uploaded. Indicates an image will use its original size when uploaded. */ -"Original" = "Full storlek"; +"Original" = "Original"; /* Used to attribute a post back to its original author. The '%@' characters are a placholder for the author's name. */ "Originally posted by %@" = "Först publicerat av %@"; @@ -5338,15 +5204,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title shown when selecting a post type of Page from the Share Extension. */ "Page" = "Sida"; -/* Prompts the user that a restored page was moved to the drafts list. */ -"Page Restored to Drafts" = "Sidan återställd till Utkast"; - -/* Prompts the user that a restored page was moved to the published list. */ -"Page Restored to Published" = "Sidan återställd till Publicerat"; - -/* Prompts the user that a restored page was moved to the scheduled list. */ -"Page Restored to Scheduled" = "Sidan återställd till Tidsinställd"; - /* Name of the button to open the page settings The title of the Page Settings screen. */ "Page Settings" = "Sidinställningar"; @@ -5363,9 +5220,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of notification displayed when a page has failed to upload. */ "Page failed to upload" = "Det gick inte att ladda upp sidan"; -/* A short message explaining that a page was moved to the trash bin. */ -"Page moved to trash." = "Sida flyttad till papperskorgen."; - /* Title of notification displayed when a page has been successfully saved as a draft. */ "Page pending review" = "Sidan inväntar granskning"; @@ -5397,7 +5251,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Placeholder to set a parent category for a new category. Title for selecting parent category of a category */ -"Parent Category" = "Förälder"; +"Parent Category" = "Överordnad kategori"; /* Message informing the user that their pages parent has been set successfully */ "Parent page successfully updated." = "Överordnad sida uppdaterades."; @@ -5437,8 +5291,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title of pending Comments filter. */ "Pending" = "Inväntar granskning"; -/* Name for the status of a post pending review. - Title of the Pending Review Badge */ +/* Name for the status of a post pending review. */ "Pending review" = "Inväntar granskning"; /* Noun. Title of the people management feature. @@ -5459,7 +5312,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Personal" = "Personlig"; /* Section title for the personalize table section in the blog details screen. */ -"Personalize" = "Anpassa"; +"Personalize" = "Personifiera"; /* Accessibility label for selecting an image or video from the device's photo library on formatting toolbar. */ "Photo Library" = "Fotobibliotek"; @@ -5467,12 +5320,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Photography site intent topic */ "Photography" = "Fotografering"; -/* Tab bar title for the Photos tab in Media Picker */ -"Photos" = "Foton"; - -/* Subtitle for placeholder in Free Photos. The company name 'Pexels' should always be written as it is. */ -"Photos provided by Pexels" = "Foton från Pexels"; - /* Title for selecting a new username in the site creation flow. */ "Pick username" = "Välj användarnamn"; @@ -5517,7 +5364,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Please enter a complete website address, like example.com." = "Ange den fullständiga adressen till en webbplats, t.ex. example.com."; /* No comment provided by engineer. */ -"Please enter a site address." = "Ange en webbplatsenadress."; +"Please enter a site address." = "Ange en webbplatsadress."; /* No comment provided by engineer. */ "Please enter a username." = "Ange ett användarnamn."; @@ -5565,10 +5412,10 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Please enter the password for your WordPress.com account to log in with your Apple ID." = "Ange lösenordet för ditt konto hos WordPress.com eller logga in med ditt Apple-ID."; /* Instruction text on the two-factor screen. */ -"Please enter the verification code from your authenticator app, or tap the link below to receive a code via SMS." = "Mata in bekräftelsekoden från din autentiseringsapp eller tryck på länken nedan för att erhålla en kod via SMS."; +"Please enter the verification code from your authenticator app." = "Ange verifieringskoden från din Authenticator-app."; /* Popup message to ask for user credentials (fields shown below). */ -"Please enter your credentials" = "Fyll i dina användaruppgifter"; +"Please enter your credentials" = "Ange dina användaruppgifter"; /* Instructions for alert asking for email. */ "Please enter your email address." = "Skriv in din e-postadress."; @@ -5643,7 +5490,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Header of section in Plugin Directory showing popular plugins Screen title, where users can see the most popular plugins */ -"Popular" = "Populära"; +"Popular" = "Populär"; /* Section title for Popular Languages */ "Popular languages" = "Populära språk"; @@ -5658,16 +5505,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* For setting the format of a post. The post formats available for the post. Should be the same as in core WP. */ -"Post Format" = "Format"; - -/* Prompts the user that a restored post was moved to the drafts list. */ -"Post Restored to Drafts" = "Inlägg återställt till Utkast"; - -/* Prompts the user that a restored post was moved to the published list. */ -"Post Restored to Published" = "Inlägg återställt till Publicerat"; - -/* Prompts the user that a restored post was moved to the scheduled list. */ -"Post Restored to Scheduled" = "Inlägg återställt till Tidsinställt"; +"Post Format" = "Inläggsformat"; /* Name of the button to open the post settings The title of the Post Settings screen. */ @@ -5688,9 +5526,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of notification displayed when a post has failed to upload. */ "Post failed to upload" = "Det gick inte att ladda upp Inlägget"; -/* A short message explaining that a post was moved to the trash bin. */ -"Post moved to trash." = "Inlägg flyttat till papperskorgen."; - /* Title of notification displayed when a post has been successfully saved as a draft. */ "Post pending review" = "Inlägg inväntar granskning"; @@ -5749,9 +5584,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Period Stats 'Posts and Pages' header */ "Posts and Pages" = "Inlägg och sidor"; -/* Title of the Posts Page Badge */ -"Posts page" = "Inläggssida"; - /* Message informing the user that their static homepage for posts was set successfully */ "Posts page successfully updated" = "Inläggssidan uppdaterades"; @@ -5764,9 +5596,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* A message explaining the Posts I Like feature in the reader */ "Posts that you like will appear here." = "Inlägg som du gillar visas här."; -/* Subtitle for placeholder in Tenor picker. `The company name 'Tenor' should always be written as it is. */ -"Powered by Tenor" = "Drivs med Tenor"; - /* Browse premium themes selection title */ "Premium" = "Premium"; @@ -5777,7 +5606,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Preparing to scan" = "Förbereder för att skanna"; /* Label to show while converting and/or resizing media to send to server */ -"Preparing..." = "Förbereder..."; +"Preparing..." = "Förbereder …"; /* Displays the Post Preview Interface Title for button to preview a selected layout @@ -5785,23 +5614,17 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title for screen to preview a static content. */ "Preview" = "Visa"; -/* Action for Media Picker to preview the selected media items. The argument in the string represents the number of elements (as numeric digits) selected */ -"Preview %@" = "Förhandsgranska %@"; - /* Title for web preview device switching button */ "Preview Device" = "Enhet för förhandsgranskning"; /* Title on display preview error */ "Preview Unavailable" = "Förhandsvisning inte tillgänglig"; -/* Accessibility label for media item preview for user's viewing an item in their media library */ -"Preview media" = "Förhandsvisa media"; - /* No comment provided by engineer. */ "Preview page" = "Förhandsgranska sidan"; /* No comment provided by engineer. */ -"Preview post" = "Förhandsgranska"; +"Preview post" = "Förhandsgranska inlägg"; /* Description of a Quick Start Tour */ "Preview your site to see what your visitors will see." = "Förhandsgranska din webbplats för att se vad dina besökare kommer att se."; @@ -5827,7 +5650,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Privacy" = "Integritet"; /* Title of button that displays the App's privacy policy */ -"Privacy Policy" = "Privacy Policy"; +"Privacy Policy" = "Integritetspolicy"; /* Register Domain - Privacy Protection section header title */ "Privacy Protection" = "Integritetsskydd"; @@ -5843,8 +5666,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Privacy notice for California users" = "Integritetsnotis för användare i Kalifornien"; /* Name for the status of a post that is marked private. - Privacy setting for posts set to 'Private'. Should be the same as in core WP. - Title of the Private Badge */ + Privacy setting for posts set to 'Private'. Should be the same as in core WP. */ "Private" = "Privat"; /* No comment provided by engineer. */ @@ -5872,7 +5694,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Projects" = "Projekt"; /* Title of the notification presented when a prompt is skipped */ -"Prompt skipped" = "Påminnelsen hoppades över"; +"Prompt skipped" = "Förslaget hoppades över"; /* Title label for blogging prompts in the create new bottom action sheet. Title label for the Prompts card in My Sites tab. @@ -5880,7 +5702,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Prompts" = "Förslag"; /* Privacy setting for posts set to 'Public' (default). Should be the same as in core WP. */ -"Public" = "Publikt"; +"Public" = "Offentligt"; /* Button shown when the author is asked for publishing confirmation. Button title. Publishes a post. @@ -5894,12 +5716,10 @@ translators: %s: Select control button label e.g. \"Button width\" */ Label for the publish date button. */ "Publish Date" = "Publiceringsdatum"; -/* A short phrase indicating a post is due to be immedately published. - Label for a button that moves a page to the published folder, publishing with the current date/time. */ +/* A short phrase indicating a post is due to be immedately published. */ "Publish Immediately" = "Publicera direkt"; /* Label for a button that publishes the post - Label for an option that moves a publishes a post immediately Title of button allowing the user to immediately publish the post they are editing. */ "Publish Now" = "Publicera nu"; @@ -5917,15 +5737,14 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Name for the status of a published post. Period Stats 'Published' header - Title of the published filter. This filter shows a list of posts that the user has published. - Title of the published header in search list. */ + Title of the published filter. This filter shows a list of posts that the user has published. */ "Published" = "Publicerat"; /* Precedes the name of the blog just posted on */ "Published just now on" = "Har just publicerat på"; /* Published on [date] */ -"Published on" = "Publicerat"; +"Published on" = "Publicerad den"; /* Label that describes in which blog the user is publishing to */ "Publishing To" = "Publicerar till"; @@ -5934,13 +5753,13 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Publishing page..." = "Publicerar sida …"; /* A short message that informs the user a post is being published to the server from the share extension. */ -"Publishing post..." = "Publicearr inlägget ..."; +"Publishing post..." = "Publicerar inlägg …"; /* Text displayed in HUD while a post is being published. */ -"Publishing..." = "Publicerar ..."; +"Publishing..." = "Publicerar …"; /* Title of screen showing site purchases */ -"Purchases" = "Inköp"; +"Purchases" = "Köp"; /* Mobile Push Notifications */ "Push Notifications" = "Push-notiser"; @@ -6022,14 +5841,14 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Redo" = "Gör om"; /* Label for link title in Referrers stat. */ -"Referrer" = "Hänvisning"; +"Referrer" = "Hänvisande webbplats"; /* Period Stats 'Referrers' header */ "Referrers" = "Hänvisningar"; /* Button label to refres a web page The loading view button title displayed when an error occurred */ -"Refresh" = "Ladda om"; +"Refresh" = "Uppdatera"; /* Action to redeem domain credit. */ "Register Domain" = "Registrera domän"; @@ -6060,8 +5879,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of the completion screen of the Blogging Reminders Settings screen when the reminders are removed. */ "Reminders removed" = "Påminnelserna har tagits bort"; -/* Add asset to media picker list - Alert button to confirm a plugin to be removed +/* Alert button to confirm a plugin to be removed Button label when removing a blog Label for the Remove Feature Image icon. Tapping will show a confirmation screen for removing the feature image from the post. Remove Action @@ -6113,7 +5931,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Remove image" = "Ta bort bild"; /* Prompt when removing a featured image from a post */ -"Remove this Featured Image?" = "Ta bort utvald bild?"; +"Remove this Featured Image?" = "Ta bort denna utvalda bild?"; /* Accessibility hint for the 'Save Post' button when a post is already saved. */ "Remove this post from my saved posts." = "Ta bort detta inlägg från mina sparade inlägg."; @@ -6174,7 +5992,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ Reply to a comment (verb) Reply to a comment. Verb. Button title. Reply to a comment. */ -"Reply" = "Svar"; +"Reply" = "Svara"; /* The app successfully sent a comment */ "Reply Sent!" = "Svar skickat!"; @@ -6214,9 +6032,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of secondary button on alert prompting verify their accounts while attempting to publish */ "Resend" = "Skicka en gång till"; -/* Title of the reset button */ -"Reset" = "Återställ"; - /* Accessibility label for the reset activity type button */ "Reset Activity Type filter" = "Återställ aktivitetstypsfilter"; @@ -6271,12 +6086,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ Button title, displayed when media has failed to upload. Allows the user to try the upload again. Button title. Retries uploading a post. If a user taps the button with this label, the action that evinced this error view will be retried. - Label for a button that attempts to re-upload a page that previously failed to upload. - Label for the retry post upload button. Tapping attempts to upload the post again. Opens the media library . Retries the upload of a user's post. Retry updating User's Role - Retry uploading the post. Retry. Action Retry. Verb – retry a failed media upload. The Jetpack view button title used when an error occurred @@ -6288,9 +6100,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Button title that triggers a scan */ "Retry Scan" = "Försök skanna igen"; -/* User action to retry media upload. */ -"Retry Upload" = "Försök ladda upp igen"; - /* User action to retry all failed media uploads. */ "Retry all" = "Försök igen med alla"; @@ -6388,9 +6197,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Accessibility label for the 'Save Post' button when a post has been saved. */ "Saved Post" = "Sparat inlägg"; -/* Text displayed in HUD when a media item's metadata (title, etc) is saved successfully. */ -"Saved!" = "Sparat!"; - /* Accessibility hint for the 'Save Post' button. */ "Saves this post for later." = "Sparar detta inlägg till senare."; @@ -6401,9 +6207,8 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Saving post…" = "Sparar inlägg …"; /* Menus save button title while it is saving a Menu. - Text displayed in HUD while a media item's metadata (title, etc) is being saved. Text displayed in HUD while a post is being saved as a draft. */ -"Saving..." = "Sparar..."; +"Saving..." = "Sparar …"; /* Noun. Links to a blog's Jetpack Scan screen. Noun. Name of the Scan feature @@ -6430,23 +6235,23 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Scanning files" = "Skannar filer"; /* Schedule button, this is what the Publish button changes to in the Post Editor if the post has been scheduled for posting later. */ -"Schedule" = "Tidsinställ för"; +"Schedule" = "Schemalägg"; /* Label for the button that schedules the post */ "Schedule Now" = "Schemalägg nu"; /* Name for the status of a scheduled post Title of the scheduled filter. This filter shows a list of posts that are scheduled to be published at a future date. */ -"Scheduled" = "Tidsinställd"; +"Scheduled" = "Schemalagd"; /* Scheduled for [date] */ -"Scheduled for" = "Tidsinställd för"; +"Scheduled for" = "Schemalägg till"; /* Precedes the name of the blog a post was just scheduled on. Variable is the date post was scheduled for. */ "Scheduled for %@ on" = "Schemalagt för %@ den"; /* Text displayed in HUD while a post is being scheduled to be published. */ -"Scheduling..." = "Tidsinställer ..."; +"Scheduling..." = "Schemalägger …"; /* No comment provided by engineer. */ "Scrollable block menu closed." = "Den rullningsbara blockmenyn har stängts."; @@ -6492,23 +6297,11 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* No comment provided by engineer. */ "Search or type URL" = "Sök eller skriv URL"; -/* Text displayed when the search controller will be presented */ -"Search pages" = "Söksidor"; - -/* Text displayed when the search controller will be presented */ -"Search posts" = "Sök inlägg"; - /* No comment provided by engineer. */ "Search settings" = "Sökinställningar"; -/* Title for placeholder in Tenor picker */ -"Search to find GIFs to add to your Media Library!" = "Sök för att hitta GIF som du vill lägga till i ditt mediebibliotek!"; - -/* Title for placeholder in Free Photos */ -"Search to find free photos to add to your Media Library!" = "Sök efter gratis bilder att lägga till i ditt mediebibliotek!"; - /* Menus search bar placeholder text. */ -"Search..." = "Sök..."; +"Search..." = "Sök …"; /* Accessibility hint for the domains search field in Site Creation. */ "Searches for available domains to use for your site." = "Söker efter domäner som är tillgängliga för användning på din webbplats."; @@ -6577,9 +6370,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Register Domain - Domain contact information field placeholder for Country */ "Select Country" = "Välj land"; -/* Title of button that allows the user to select more photos to access within the app */ -"Select More" = "Välj fler"; - /* Blog Picker's Title */ "Select Site" = "Välj webbplats"; @@ -6601,9 +6391,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Register domain - Title for the Choose domain button of Suggested domains screen */ "Select domain" = "Välj domän"; -/* Accessibility hint for actions when displaying media items. */ -"Select media." = "Välj media."; - /* Accessibility label for selecting paragraph style button on formatting toolbar. */ "Select paragraph style" = "Välj styckeformatering"; @@ -6707,19 +6494,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label for connected service in Publicize stat. */ "Service" = "Tjänst"; -/* Label for a button that opens the Set Parent options view controller - Navigation title displayed on the navigation bar */ +/* Navigation title displayed on the navigation bar */ "Set Parent" = "Ange överordnad"; /* No comment provided by engineer. */ "Set as Featured Image" = "Ställ in som utvald bild"; -/* Label for a button that sets the selected page as the site's Homepage */ -"Set as Homepage" = "Ställ in som startsida"; - -/* Label for a button that sets the selected page as the site's Posts page */ -"Set as Posts Page" = "Ställ in som inläggssida"; - /* Notice confirming that an image has been set as the post's featured image. */ "Set as featured image" = "Ställ in som utvald bild"; @@ -6763,7 +6543,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Accessibility label for share buttons in nav bars Button label to share a post Button label to share a web page - Share the post. Shares the comment URL Spoken accessibility label Title for a button that allows the user to share their answer to the prompt. @@ -6799,7 +6578,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Aztec's Text Placeholder Share Extension Content Body Text Placeholder */ -"Share your story here..." = "Berätta din historia här ..."; +"Share your story here..." = "Dela din historia här …"; /* Noun. Name of the Social Sharing feature Noun. Title. Links to a blog's sharing options. @@ -6820,7 +6599,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "Show Like button" = "Visa Gilla-knappen"; /* Show site purchases action title */ -"Show Purchases" = "Visa inköp"; +"Show Purchases" = "Visa köp"; /* Title for the `show reblog button` setting */ "Show Reblog button" = "Visa Reblogga-knappen"; @@ -6939,7 +6718,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* The accessibility label for the followed sites search field The item to select during a guided tour. */ -"Site URL" = "Adress till webbplats"; +"Site URL" = "Webbplatsens URL"; /* Setting: indicates if Achievements will be notified */ "Site achievements" = "Webbplats-troféer"; @@ -6954,7 +6733,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "Site database" = "Webbplatsens databas"; /* Overlay message displayed when site successfully deleted */ -"Site deleted" = "Webbplatsen raderad"; +"Site deleted" = "Webbplats borttagen"; /* Setting: indicates if New Follows will be notified */ "Site follows" = "Webbplatsföljare"; @@ -7043,10 +6822,10 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "Sorry!" = "Tyvärr!"; /* This error message occurs when a user tries to create a username that contains an invalid phrase for WordPress.com. The %@ may include the phrase in question if it was sent down by the API */ -"Sorry, but your username contains an invalid phrase%@." = "Tyvärr innehåller ditt användarnamn en ogiltig fras %@."; +"Sorry, but your username contains an invalid phrase%@." = "Ditt användarnamn innehåller en ogiltig fras %@."; /* Error title when updating the account password fails */ -"Sorry, can't log in" = "Kunde inte logga in. Försök igen."; +"Sorry, can't log in" = "Kan inte logga in"; /* No comment provided by engineer. */ "Sorry, site addresses can only contain lowercase letters (a-z) and numbers." = "Adressen till webbplatsen kan endast innehålla gemener (a-z) och siffror."; @@ -7076,7 +6855,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "Sorry, that username already exists!" = "Det användarnamnet finns redan!"; /* No comment provided by engineer. */ -"Sorry, that username is unavailable." = "Tyvärr är inte det användarnamnet ledigt."; +"Sorry, that username is unavailable." = "Det användarnamnet är inte tillgängligt."; /* No comment provided by engineer. */ "Sorry, usernames can only contain lowercase letters (a-z) and numbers." = "Användarnamn kan endast innehålla gemener (a-z) och siffror."; @@ -7088,7 +6867,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "Sorry, usernames must have letters (a-z) too!" = "Användarnamn måste innehålla bokstäver (a-z) också!"; /* No comment provided by engineer. */ -"Sorry, you cannot access this feature. Please check your User Role on this site." = "Du kan inte använda denna funktion. Vänligen kontrollera dina användarprivilegier för denna webbplats."; +"Sorry, you cannot access this feature. Please check your User Role on this site." = "Du kan inte komma åt denna funktion. Kontrollera din användarroll på denna webbplats."; /* No comment provided by engineer. */ "Sorry, you may not use that site address." = "Du kan inte använda den adressen till webbplatsen."; @@ -7115,7 +6894,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Button title for Spam comment state. Marks comment as spam. Title of spam Comments filter. */ -"Spam" = "Skräp"; +"Spam" = "Skräppost"; /* Option to select the Spark email app when logging in with magic links */ "Spark" = "Spark"; @@ -7149,8 +6928,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Name of setting configured when a site uses a static page as its homepage */ "Static Homepage" = "Statisk startsida"; -/* Label for post stats option. Tapping displays statistics for a post. - Noun. Abbreviation of Statistics. Name of the Stats feature +/* Noun. Abbreviation of Statistics. Name of the Stats feature Noun. Abbv. of Statistics. Links to a blog's Stats screen. Stats 3D Touch Shortcut Stats window title @@ -7181,9 +6959,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label text that defines a post marked as sticky */ "Sticky" = "Klistrat"; -/* Accessibility label for a sticky post in the post list. */ -"Sticky." = "Klistrat."; - /* User action to stop upload. */ "Stop upload" = "Avbryt uppladdning"; @@ -7209,7 +6984,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "Submit for Review" = "Spara som väntande"; /* Text displayed in HUD while a post is being submitted for review. */ -"Submitting for Review..." = "Skickar till granskning..."; +"Submitting for Review..." = "Skickar till granskning …"; /* Notice displayed to the user after clearing the spotlight index in app settings. */ "Successfully cleared spotlight index" = "Spotlight-indexet har tömts"; @@ -7240,7 +7015,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ Theme Support action title */ "Support" = "Hjälp"; -/* Button used to switch site */ +/* Title for back button that leads to the site picker screen. */ "Switch Site" = "Byt webbplats"; /* Switches the Editor to HTML Mode */ @@ -7302,14 +7077,14 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Menu item label for linking a specific tag. Section header for tag name in Tag Details View. */ -"Tag" = "Tagga"; +"Tag" = "Etikett"; /* Title of the alert indicating that a tag with that name already exists. */ "Tag already exists" = "Etiketten finns redan"; /* Label for tagline blog setting Title for screen that show tagline editor */ -"Tagline" = "Tagline"; +"Tagline" = "Slogan"; /* Label for selecting the blogs tags Label for Tags @@ -7328,9 +7103,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label explaining why users might want to add tags. */ "Tags help tell readers what a post is about. Separate different tags with commas." = "Etiketter hjälper till att berätta för läsarna vad ett inlägg handlar om. Dela upp med kommatecken om du anger flera etiketter."; -/* Menu option for taking an image or video with the device's camera. */ -"Take Photo or Video" = "Ta ett foto eller spela in video"; - /* No comment provided by engineer. */ "Take a Photo" = "Ta en bild"; @@ -7341,7 +7113,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "Tap \"View more\" to see your top commenters." = "Tryck på ”Visa mer” för att se vilka som kommenterat dig mest."; /* A hint displayed in the Saved Posts section of the Reader. The '[bookmark-outline]' placeholder will be replaced by an icon at runtime – please leave that string intact. */ -"Tap [bookmark-outline] to save a post to your list." = "Knacka på [bookmark-outline] för att spara ett inlägg i din lista."; +"Tap [bookmark-outline] to save a post to your list." = "Tryck på [bookmark-outline] för att spara ett inlägg i din lista."; /* Accessibility hint */ "Tap for more detail." = "Tryck för detaljerad information."; @@ -7375,13 +7147,13 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "Tap to edit" = "Tryck för att ändra"; /* Message in a row indicating to tap to enter a custom value */ -"Tap to enter a custom value" = "Knacka för att mata in ett anpassat värde"; +"Tap to enter a custom value" = "Tryck för att ange ett anpassat värde"; /* No comment provided by engineer. */ "Tap to hide the keyboard" = "Tryck för att dölja tangentbordet"; /* Title for a push notification with fixed content that invites the user to load today's blogging prompt. */ -"Tap to load today's prompt..." = "Tryck för att hämta dagens tips …"; +"Tap to load today's prompt..." = "Tryck för att hämta dagens förslag …"; /* Accessibility hint for referrer action row. */ "Tap to mark referrer as not spam." = "Tryck för att märka den länkande sidan som ej skräp."; @@ -7401,12 +7173,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Accessibility hint */ "Tap to select the previous period" = "Tryck för att välja föregående period"; -/* Accessibility hint for button used to switch site */ -"Tap to switch to another site, or add a new site" = "Tryck för att växla till någon annan webbplats eller lägga till en ny."; - -/* Accessibility hint for media item preview for user's viewing an item in their media library */ -"Tap to view media in full screen" = "Tryck för att visa media i fullskärmsläge"; - /* Accessibility hint for a button that opens a new view with more details. */ "Tap to view more details." = "Tryck för att visa mer information."; @@ -7452,10 +7218,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* No comment provided by engineer. */ "Text formatting controls are located within the toolbar positioned above the keyboard while editing a text block" = "Medan man redigerar ett textblock finns verktygen för textformatering finns i verktygsfältet ovanför tangentbordet "; -/* Button title - The button's title text to send a 2FA code via SMS text message. */ +/* Button title */ "Text me a code instead" = "Skicka mig en kod via SMS i stället"; +/* The button's title text to send a 2FA code via SMS text message. */ +"Text me a code via SMS" = "Skicka mig en kod via SMS"; + /* Message of alert when theme activation succeeds */ "Thanks for choosing %@ by %@" = "Tack för att du väljer %1$@ från %2$@"; @@ -7466,7 +7234,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "That doesn't look like a WordPress site." = "Det där ser inte ut som en WordPress-webbplats."; /* No comment provided by engineer. */ -"That email address has already been used. Please check your inbox for an activation email. If you don't activate you can try again in a few days." = "Den e-postadressen har redan använts. Kolla om det finns ett aktiverings-email i din inbox. Om du inte aktiverar kan du försöka igen om några dagar."; +"That email address has already been used. Please check your inbox for an activation email. If you don't activate you can try again in a few days." = "Den e-postadressen har redan använts. Kontrollera din inkorg efter en e-postaktivering. Om du inte aktiverar kan du försöka igen om några dagar."; /* No comment provided by engineer. */ "That site address is not allowed." = "Den adressen till webbplatsen är inte tillåten."; @@ -7481,10 +7249,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "That username is not allowed." = "Det användarnamnet är inte tillåtet."; /* Error message shown to a user who is trying to share to Facebook but does not have any available Facebook Pages. */ -"The Facebook connection cannot find any Pages. Publicize cannot connect to Facebook Profiles, only published Pages." = "Facebook-kopplingen hittar inga sidor. Publicize kan inte kopplas till Facebook-profiler, utan endast till publicerade sidor."; - -/* Message shown when a GIF failed to load while trying to add it to the Media library. */ -"The GIF could not be added to the Media Library." = "GIF-filen kunde inte läggas till i mediabiblioteket."; +"The Facebook connection cannot find any Pages. Publicize cannot connect to Facebook Profiles, only published Pages." = "Facebook-anslutningen kan inte hitta några sidor. Offentliggör kan inte ansluta till Facebook-profiler, endast publicerade sidor."; /* Description shown when a user logs in with Google but no matching WordPress.com account is found */ "The Google account \"%@\" doesn't match any account on WordPress.com" = "Google-kontot ”%@” matchar inte något konto på WordPress.com"; @@ -7509,7 +7274,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "The World's Best Fans" = "Världens bästa fans"; /* No comment provided by engineer. */ -"The app can't recognize the server response. Please, check the configuration of your site." = "Appen förstår inte svaret från servern. Vänligen kontrollera din webbplats inställningar."; +"The app can't recognize the server response. Please, check the configuration of your site." = "Appen kan inte känna igen serversvaret. Kontrollera konfigurationen av din webbplats."; /* No comment provided by engineer. */ "The basics" = "Grunderna"; @@ -7518,7 +7283,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "The best way to become a better writer is to build a writing habit and share with others - that’s where Prompts come in!" = "Det bästa sättet att bli en bättre skribent är att börja skriva regelbundet och visa för andra – och det är här bloggningspåminnelserna kommer in i bilden!"; /* No comment provided by engineer. */ -"The certificate for this server is invalid. You might be connecting to a server that is pretending to be “%@” which could put your confidential information at risk.\n\nWould you like to trust the certificate anyway?" = "Certifikatet för den här servern är inte giltigt. Det kan hända att du ansluter till en server som påstår sig vara \"%@\", vilket kan försätta din sekretessbelagda information i fara.\n\nVill du lita på certifikatet i alla fall?"; +"The certificate for this server is invalid. You might be connecting to a server that is pretending to be “%@” which could put your confidential information at risk.\n\nWould you like to trust the certificate anyway?" = "Certifikatet för denna server är ogiltigt. Du kanske ansluter till en server som utger sig för att vara ”%@”, vilket kan försätta din konfidentiella information i fara.\n\nVill du lita på certifikatet ändå?"; /* Message informing the user that posts page cannot be edited */ "The content of your latest posts page is automatically generated and cannot be edited." = "Innehållet på sidan med dina senaste inlägg genereras automatiskt och kan inte redigeras."; @@ -7613,8 +7378,8 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Error message shown when user attempts to remove the site owner. */ "The user you are trying to remove is the owner of this site. Please contact support for assistance." = "Användaren du försöker ta bort är ägare till denna webbplats. Kontakta supporten för hjälp."; -/* Error message informing a user about an invalid password. */ -"The username or password stored in the app may be out of date. Please re-enter your password in the settings and try again." = "Det sparade användarnamnet eller lösenordet kan vara föråldrat. Vänligen fyll i ditt lösenord på nytt i inställningarna och försök igen. "; +/* No comment provided by engineer. */ +"The username or password stored in the app may be out of date. Please re-enter your password in the settings and try again." = "Användarnamnet eller lösenordet lagrat i appen kan vara inaktuellt. Ange ditt lösenord igen i inställningarna och försök igen."; /* Message shown when a video failed to load while trying to add it to the Media library. */ "The video could not be added to the Media Library." = "Videoklippet kunde inte läggas till i mediabiblioteket."; @@ -7681,9 +7446,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* A short error message letting the user know about a problem displaying a post. */ "There was a problem displaying this post." = "Kunde inte visa detta inlägg."; -/* Error message displayed when the Media Library is unable to load a full sized preview of an item. */ -"There was a problem loading the media item." = "Det gick inte att läsa in mediaobjektet."; - /* The loading view subtitle displayed when an error occurred */ "There was a problem loading your data, refresh your page to try again." = "Det gick inte att hämta in dina uppgifter. Försök igen genom att ladda om sidan."; @@ -7696,9 +7458,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Explaining to the user there was an error trying to obtain the current location of the user. */ "There was a problem when trying to access your location. Please try again later." = "Ett problem uppstod med åtkomsten till din plats. Försök igen senare."; -/* Explaining to the user there was an generic error accesing media. */ -"There was a problem when trying to access your media. Please try again later." = "Ett problem uppstod när du försökte få åtkomst till din media. Försök igen senare."; - /* Message for stories unknown error. */ "There was a problem with the Stories editor. If the problem persists you can contact us via the Me > Help & Support screen." = "Ett problem inträffade med berättelseredigeraren. Om problemet kvarstår kan du kontakta oss via skärmen Jag > Hjälp och support."; @@ -7716,7 +7475,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "There was an error loading plugins" = "Ett fel inträffade när tilläggen lästes in"; /* Text displayed when there is a failure loading blogging prompts. */ -"There was an error loading prompts." = "Ett fel uppstod vid laddningen av uppmaningarna."; +"There was an error loading prompts." = "Ett fel uppstod när förslagen skulle hämtas."; /* Text displayed when there is a failure loading a comment. */ "There was an error loading the comment." = "Det gick inte att läsa in kommentaren."; @@ -7769,9 +7528,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* A description informing the user in order to proceed with this feature we will need camera permissions, and how to enable it. */ "This app needs permission to access the Camera to scan login codes, tap on the Open Settings button to enable it." = "Denna app behöver tillstånd för att använda kameran till att skanna inloggningskoder. Tryck på knappen ”öppna inställningar” för att aktivera."; -/* Explaining to the user why the app needs access to the device media library. */ -"This app needs permission to access your device media library in order to add photos and\/or video to your posts. Please change the privacy settings if you wish to allow this." = "Denna app behöver åtkomst till mediabiblioteket i din enhet för att kunna lägga till bilder eller videoklipp i dina inlägg. Ändra integritetsinställningarna om du vill tillåta detta."; - /* No comment provided by engineer. */ "This color combination may be hard for people to read. Try using a brighter background color and\/or a darker text color." = "Denna färgkombination kan vara svår för människor att läsa. Försök använda en ljusare bakgrundsfärg och\/eller en mörkare textfärg."; @@ -7881,6 +7637,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Description of a Quick Start Tour */ "Time to finish setting up your site! Our checklist walks you through the next steps." = "Nu är det dags för webbplatsens avslutande inställningar! Vår checklista visar vad du behöver göra."; +/* Error when the uses takes more than 1 minute to submit a security key. */ +"Time's up, but don't worry, your security is our priority. Please try again!" = "Tiden är ute, men oroa dig inte, din säkerhet är vår prioritet. Försök igen!"; + /* WordPress.com Marketing Footer Text */ "Tips for getting the most out of WordPress.com." = "Tips för att få ut så mycket som möjligt av WordPress.com."; @@ -7935,10 +7694,10 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Comments Today Section Header Insights 'Today' header Notifications Today Section Header */ -"Today" = "I dag"; +"Today" = "Idag"; /* Title for a push notification showing today's blogging prompt. */ -"Today's Prompt 💡" = "Dagens tips 💡"; +"Today's Prompt 💡" = "Dagens förslag 💡"; /* Insights Management 'Today's Stats' title */ "Today's Stats" = "Dagens statistik"; @@ -8004,23 +7763,19 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title for the traffic section in site settings screen */ "Traffic" = "Trafik"; +/* Describes a domain that was transferred from elsewhere to wordpress.com */ +"Transferred Domain" = "Överförd domän"; + /* translators: %s: block title e.g: \"Paragraph\". */ "Transform %s to" = "Omvandla %s till"; /* No comment provided by engineer. */ "Transform block…" = "Omvandla block …"; -/* Accessibility label for trash button to delete items from the user's media library - Accessibility label for trash buttons in nav bars +/* Accessibility label for trash buttons in nav bars Trashes a comment Trashes the comment */ -"Trash" = "Kastas"; - -/* Accessibility hint for trash button to delete items from the user's media library */ -"Trash selected media" = "Släng valda media"; - -/* Title of the trash page confirmation alert. */ -"Trash this page?" = "Vill du lägga denna sida i papperskorgen?"; +"Trash" = "Släng"; /* Title of the trash confirmation alert. */ "Trash this post?" = "Ska detta inlägg kastas i papperskorgen?"; @@ -8037,7 +7792,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "Trust" = "Godkänn"; /* Theme Try & Customize action title */ -"Try & Customize" = "Testa & anpassa"; +"Try & Customize" = "Testa och anpassa"; /* Retries an Action Retry @@ -8131,7 +7886,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "Type to get more suggestions" = "Fortsätt skriva för att få fler förslag."; /* URL text field placeholder */ -"URL" = "URL:"; +"URL" = "URL"; /* Menus label for describing which menu the location uses in the header. */ "USES" = "ANVÄNDER"; @@ -8139,9 +7894,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Shown when a user logs in with Google but it subsequently fails to work as login to WordPress.com */ "Unable To Connect" = "Kan inte ansluta"; -/* An error message. */ -"Unable to Connect" = "Det gick inte att ansluta"; - /* Title for stories unknown error. */ "Unable to Create Stories Editor" = "Det gick inte att skapa en berättelseredigerare"; @@ -8157,9 +7909,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* An error message shown when there is an issue creating new invite links. */ "Unable to create new invite links." = "Det gick inte att skapa nya inbjudningslänkar."; -/* Text displayed in HUD if there was an error attempting to delete a group of media items. */ -"Unable to delete all media items." = "Kan inte radera alla medieobjekt."; - /* Text displayed in HUD if there was an error attempting to delete a media item. */ "Unable to delete media item." = "Kan inte radera medieobjekt."; @@ -8223,12 +7972,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Message displayed when sharing a link to the downloadable backup fails. */ "Unable to share link" = "Kan inte dela länken"; -/* Message that appears when a user tries to trash a page while their device is offline. */ -"Unable to trash pages while offline. Please try again later." = "Det går inte att lägga sidor i papperskorgen när du saknar internetanslutning. Försök igen senare."; - -/* Message that appears when a user tries to trash a post while their device is offline. */ -"Unable to trash posts while offline. Please try again later." = "Det går inte att kasta inlägg i papperskorgen utan internet-anslutning. Försök igen senare."; - /* Notice title when turning site notifications off fails. */ "Unable to turn off site notifications" = "Kan inte stänga av webbplatsaviseringar"; @@ -8276,10 +8019,10 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Unapproves a Comment Unapproves a comment */ -"Unapprove" = "Förkasta"; +"Unapprove" = "Godkänn ej"; /* Unapproves a Comment */ -"Unapprove Comment" = "Förkasta kommentar"; +"Unapprove Comment" = "Godkänn ej kommentar"; /* VoiceOver accessibility hint, informing the user the button can be used to unapprove a comment */ "Unapproves the Comment." = "Godkänner ej kommentaren."; @@ -8301,8 +8044,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ Button title. Reverts the previous notification operation Revert an operation Revert enabling notification after successfully subcribing to the comments for the post. - The title of an 'undo' button. Tapping the button moves a trashed page out of the trash folder. - The title of an 'undo' button. Tapping the button moves a trashed post out of the trash folder. Undo action */ "Undo" = "Ångra"; @@ -8345,9 +8086,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title for Unknown HTML Editor */ "Unknown HTML" = "Okänd HTML-kodning"; -/* Label to use when creation date from media asset is not know. */ -"Unknown creation date" = "Tillkomstdatum okänt"; - /* No comment provided by engineer. */ "Unknown error" = "Okänt fel"; @@ -8436,7 +8174,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "Updating" = "Uppdaterar"; /* Text displayed in HUD while a draft or scheduled post is being updated. */ -"Updating..." = "Uppdaterar..."; +"Updating..." = "Uppdaterar …"; /* No comment provided by engineer. */ "Upgrade your plan to upload audio" = "Uppgradera ditt paket för att ladda upp ljud"; @@ -8449,10 +8187,10 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Message displayed on a post's card when the post has failed to upload System notification displayed to the user when media files have failed to upload. */ -"Upload failed" = "Uppladdningsfel"; +"Upload failed" = "Uppladdning misslyckades"; /* Description to show on post setting for a featured image that failed to upload. */ -"Upload failed. Tap for options." = "Uppladdning misslyckades. Knacka för alternativ."; +"Upload failed. Tap for options." = "Uppladdning misslyckades. Tryck för alternativ."; /* Title of a Quick Start Tour */ "Upload photos or videos" = "Ladda upp foton eller videoklipp"; @@ -8484,13 +8222,13 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "Uploading media" = "Laddar upp media"; /* Message displayed on a post's card while the post is uploading media */ -"Uploading media..." = "Ladda upp media..."; +"Uploading media..." = "Laddar upp media …"; /* Title for alert when trying to preview a post before the uploading process is complete. */ "Uploading post" = "Laddar upp inlägg"; /* Message displayed on a post's card when the post has failed to upload */ -"Uploading post..." = "Laddar upp inlägg..."; +"Uploading post..." = "Laddar upp inlägg …"; /* Message of an alert informing users that the video they are trying to select is not allowed. */ "Uploading videos longer than 5 minutes requires a paid plan." = "Att ladda upp videoklipp som är längre än 5 minuter kräver ett betalt paket."; @@ -8502,7 +8240,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "Uploading…" = "Laddar upp …"; /* Title for alert when trying to save post with failed media items */ -"Uploads failed" = "Filer kunde inte laddas upp"; +"Uploads failed" = "Uppladdning misslyckades"; /* Use the current image */ "Use" = "Använd"; @@ -8513,6 +8251,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of a row displayed on the debug screen used to configure the sandbox store use in the App. */ "Use Sandbox Store" = "Använd sandlåde-lagring"; +/* The button's title text to use a security key. */ +"Use a security key" = "Använd en säkerhetsnyckel"; + /* Option to enable the block editor for new posts */ "Use block editor" = "Använd blockredigeraren"; @@ -8534,7 +8275,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ The header and main title Username label text. Username placeholder */ -"Username" = "Användarnamn:"; +"Username" = "Användarnamn"; /* Message displayed in a Notice when the username has changed successfully. The placeholder is the new username. */ "Username changed to %@" = "Användarnamnet har bytts till %@"; @@ -8588,15 +8329,10 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of an alert informing users that the video they are trying to select is not allowed. */ "Video not uploaded" = "Videon laddades inte upp"; -/* Accessibility label for video thumbnails in the media collection view. The parameter is the creation date of the video. */ -"Video, %@" = "Video, %@"; - /* Period Stats 'Videos' header */ "Videos" = "Videoklipp"; /* Button title. Displays a summary / sharing screen for a specific post. - Label for a button that opens the page when tapped. - Label for the view post button. Tapping displays the post as it appears on the web. Opens the post epilogue screen to allow sharing / viewing of a post. Theme View action title Verb. The screen title shown when viewing a post inside the app. */ @@ -8609,16 +8345,16 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "View Post" = "Se inlägget"; /* Action title. Opens the user's site in an in-app browser */ -"View Site" = "Besök sida"; +"View Site" = "Visa webbplats"; /* Title for button on the post details page to show all comments when tapped. */ "View all comments" = "Visa alla kommentarer"; /* Displays More Rows */ -"View all…" = "Visa alla..."; +"View all…" = "Visa alla …"; /* Displays Less Rows */ -"View less…" = "Visa färre..."; +"View less…" = "Visa färre …"; /* Accessibility label for the View more button in Stats' Post Summary. Accessibility label for View more button in Stats. @@ -8711,6 +8447,10 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Message shown on screen while waiting for Google to finish its signup process. */ "Waiting for Google to complete…" = "Väntar på att Google ska bli klara …"; +/* Text while the webauthn signature is being verified + Text while waiting for a security key challenge */ +"Waiting for security key" = "Väntar på säkerhetsnyckel"; + /* View title during the Google auth process. */ "Waiting..." = "Väntar…"; @@ -8930,7 +8670,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "We're creating a downloadable backup of your site from %1$@." = "Vi håller på att skapa en nedladdningsbar säkerhetskopia av din webbplats från %1$@."; /* Title of progress label displayed when a first plugin on a site is almost done installing. */ -"We're doing the final setup—almost done…" = "Vi konfigurerar det sista – nästan klart nu..."; +"We're doing the final setup—almost done…" = "Vi konfigurerar det sista – nästan klart nu …"; /* Detail text display informing the user that we're fixing threats */ "We're hard at work fixing these threats in the background. In the meantime feel free to continue to use your site as normal, you can check back on progress at any time." = "Vi arbetar intensivt med att rätta dessa hot i bakgrunden. Under tiden kan du fortsätta att använda din webbplats som vanligt. Du kan när som helst kolla hur arbetet fortskrider."; @@ -9085,6 +8825,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* An error message shown when a wpcom user provides the wrong password. */ "Whoops, something went wrong and we couldn't log you in. Please try again!" = "Ojdå. Något blev fel. Vi lyckades inte logga in dig. Försök igen!"; +/* Generic error on the 2FA screen */ +"Whoops, something went wrong. Please try again!" = "Hoppsan, något gick fel. Försök igen!"; + +/* Error when the uses chooses an invalid security key on the 2FA screen. */ +"Whoops, that security key does not seem valid. Please try again with another one" = "Hoppsan, den säkerhetsnyckeln verkar inte vara giltig. Försök igen med en annan"; + /* Error message shown when an incorrect two factor code is provided. */ "Whoops, that's not a valid two-factor verification code. Double-check your code and try again!" = "Ojdå. Den där var ingen giltig bekräftelsekod för tvåfaktorautentisering. Dubbelkolla koden och försök en gång till!"; @@ -9112,9 +8858,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Siri Suggestion to open Support */ "WordPress Help" = "WordPress-hjälp"; -/* Name for the WordPress Media Library */ -"WordPress Media" = "WordPress Media"; - /* Accessibility label for selecting an image or video from the user's WordPress media library on formatting toolbar. */ "WordPress Media Library" = "WordPress mediabibliotek"; @@ -9241,7 +8984,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Comments Yesterday Section Header Notifications Yesterday Section Header */ -"Yesterday" = "I går"; +"Yesterday" = "Igår"; /* Main message on dialog that prompts user to confirm or cancel the replacement of a featured image. */ "You already have a featured image set. Do you want to replace it with the new image?" = "En utvald bild finns redan angiven. Vill du ersätta med den nya bilden?"; @@ -9250,7 +8993,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "You are about to change your username, which is currently %@. %@" = "Du är på väg att ändra ditt användarnamn, som för närvarande är %1$@. %2$@"; /* Error message informing the user that they are already following a site in their reader. */ -"You are already following this site." = "Du följer redan den här webbplatsen."; +"You are already following this site." = "Du följer redan denna webbplats."; /* Alert message. */ "You are changing your username to %@. Changing your username will also affect your Gravatar profile and IntenseDebate profile addresses. \nConfirm your new username to continue." = "Du är på väg att ändra ditt användarnamn till %@. När du byter användarnamn kommer detta även att påverka adresserna till profilerna för Gravatar och IntenseDebate.\nBekräfta ditt nya användarnamn för att fortsätta."; @@ -9295,7 +9038,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "You can update this any time via My Site > Site Settings" = "Du kan uppdatera detta när som helst via Min webbplats > Webbplatsinställningar"; /* No comment provided by engineer. */ -"You cannot use that email address to signup. We are having problems with them blocking some of our email. Please use another email provider." = "Du kan inte använda den e-postadressen för att skapa ett konto. Vi har problem med att den e-posttjänsten ibland blockerar vår e-post. Vänligen använd en annan e-posttjänst."; +"You cannot use that email address to signup. We are having problems with them blocking some of our email. Please use another email provider." = "Du kan inte använda den e-postadressen för att registrera dig. Vi har problem med att de blockerar en del av vår e-post. Använd en annan e-postleverantör."; /* Displayed when the user views drafts in the pages list and there are no pages */ "You don't have any draft pages" = "Duhar inga utkast till sidor"; @@ -9345,7 +9088,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Show when clicking cancel on a comment text box and you didn't save your changes. Title of message with options that shown when there are unsaved changes and the author cancelled editing a Comment. Title of message with options that shown when there are unsaved changes and the author is trying to move away from the post. */ -"You have unsaved changes." = "Inlägget har osparade ändringar."; +"You have unsaved changes." = "Du har osparade ändringar."; /* Displayed when the user views published pages in the pages list and there are no pages */ "You haven't published any pages yet" = "Du har inte publicerat några sidor än"; @@ -9429,9 +9172,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Share extension error dialog text. */ "Your account does not have permission to upload media to this site. The Site Administrator can change these permissions." = "Ditt konto saknar behörighet att ladda upp media till denna webbplats. Webbplatsens adninistratör kan ändra denna inställning."; -/* Explaining to the user why the app needs access to the device media library. */ -"Your app is not authorized to access media library due to active restrictions such as parental controls. Please check your parental control settings in this device." = "På grund av engällande begränsning, t.ex. föräldrakontroll, har din app inte behörighet att använda mediabiblioteket. Kontrollera inställningarna för föräldrakontroll i denna enhet."; - /* Title for the Jetpack Backup Complete message. */ "Your backup is now available for download" = "Din säkerhetskopia är nu tillgänglig för nedladdning"; @@ -9450,9 +9190,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of the site address section in the Domains Dashboard. */ "Your free WordPress.com address is" = "Din gratisadress hos WordPress.com är"; -/* Error message when picked media cannot be imported into stories. */ -"Your media could not be exported. If the problem persists you can contact us via the Me > Help & Support screen." = "Dina mediafiler kunde inte exporteras. Om problemet kvarstår kan du kontakta oss via skärmen Jag > Hjälp och support."; - /* Details about recently acquired domain on domain credit redemption success screen */ "Your new domain %@ is being set up. It may take up to 30 minutes for your domain to start working." = "Din nya domän %@ håller på att konfigureras. Det kan dröja upp till 30 minuter innan din domän börjar fungera."; @@ -9576,8 +9313,38 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* This is the string we display when prompting the user to review the WordPress app */ "appRatings.wordpress.prompt" = "Vad tycker du om WordPress?"; -/* Label displayed on audio media items. */ -"audio" = "ljud"; +/* Option to enable the optimization of images when uploading. */ +"appSettings.media.imageOptimizationRow" = "Optimera bilder"; + +/* Indicates an image will use high quality when uploaded. */ +"appSettings.media.imageQuality.high" = "Hög"; + +/* Indicates an image will use low quality when uploaded. */ +"appSettings.media.imageQuality.low" = "Låg"; + +/* Indicates an image will use maximum quality when uploaded. */ +"appSettings.media.imageQuality.maximum" = "Maximal"; + +/* Indicates an image will use medium quality when uploaded. */ +"appSettings.media.imageQuality.medium" = "Medium"; + +/* The quality of image used when uploading */ +"appSettings.media.imageQuality.title" = "Kvalitet"; + +/* Title for the image quality settings option. */ +"appSettings.media.imageQualityRow" = "Bildkvalitet"; + +/* Message of an alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.message" = "Bildoptimering minskar storleken på bilder för snabbare uppladdning.\n\nDet här alternativet är aktiverat som standard, men du kan ändra det i appinställningarna när som helst."; + +/* Title of an alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.title" = "Fortsätt optimera bilder?"; + +/* Title of button for turning off image optimization, displayed in the alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.turnOff" = "Nej, stäng av"; + +/* Title of button for leaving on image optimization, displayed in the alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.turnOn" = "Ja, lämna på"; /* translators: displays audio file extension. e.g. MP3 audio file */ "audio file" = "ljudfil"; @@ -9691,7 +9458,44 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "blogHeader.actionCopyURL" = "Kopiera URL"; /* Context menu button title */ -"blogHeader.actionOpenInBrowser" = "Öppna i webbläsare"; +"blogHeader.actionVisitSite" = "Besök webbplats"; + +/* Title for a button that, when tapped, shows more info about participating in Bloganuary. */ +"bloganuary.dashboard.card.button.learnMore" = "Läs mer"; + +/* Short description for the Bloganuary event, shown right below the title. */ +"bloganuary.dashboard.card.description" = "För januari månad kommer bloggningsförslagen från Bloganuary – vår community-utmaning att bygga en bloggvana för det nya året."; + +/* Title for the Bloganuary dashboard card while Bloganuary is running. */ +"bloganuary.dashboard.card.runningTitle" = "Bloganuary är här!"; + +/* Title for the Bloganuary dashboard card. */ +"bloganuary.dashboard.card.title" = "Bloganuary är på väg!"; + +/* Title of a button that calls the user to enable the Blogging Prompts feature. */ +"bloganuary.learnMore.modal.button.promptsDisabled" = "Aktivera bloggningsförslag"; + +/* Title of a button that will dismiss the Bloganuary modal when tapped. +Note that the word 'go' here should have a closer meaning to 'start' rather than 'move forward'. */ +"bloganuary.learnMore.modal.button.promptsEnabled" = "Nu kör vi!"; + +/* The second line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.description.second" = "Publicera ditt svar."; + +/* The third line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.description.third" = "Läs andra bloggares svar för att få inspiration och skapa nya kontakter."; + +/* The first line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.descriptions.first" = "Få ett nytt förslag som inspirerar dig varje dag."; + +/* An additional piece of information shown in case the user has the Blogging Prompts feature disabled. */ +"bloganuary.learnMore.modal.footer.addition" = "För att gå med i Bloganuary behöver du aktivera bloggningsförslag."; + +/* An informative excerpt shown in a subtler tone. */ +"bloganuary.learnMore.modal.footer.text" = "Bloganuary använder dagliga bloggningsförslag för att skicka dig ämnena för januari månad."; + +/* The headline text of the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.headline" = "Gå med i vår månadslånga skrivutmaning"; /* Verb. Dismisses the blogging prompt notification. */ "bloggingPrompt.pushNotification.customActionDescription.dismiss" = "Avfärda"; @@ -9720,6 +9524,12 @@ Example: Comment on Example: Reply to Pamela Nguyen */ "comment.header.subText.reply" = "Svar till %1$@"; +/* Error message informing a user about an invalid password. */ +"common.reEnterPasswordMessage" = "Användarnamnet eller lösenordet som är lagrat i appen kan vara inaktuellt. Ange ditt lösenord i inställningarna och försök igen."; + +/* An error message. */ +"common.unableToConnect" = "Kan inte ansluta"; + /* Footnote for the privacy compliance popover. */ "compliance.analytics.popover.footnote" = "Dessa cookies tillåter oss att optimera prestandan genom att samla in information om hur användare interagerar med våra webbplatser."; @@ -9870,50 +9680,92 @@ Example: Reply to Pamela Nguyen */ /* Title for a menu action in the context menu on the Jetpack install card. */ "domain.dashboard.card.menu.hide" = "Dölj detta"; -/* Domain Purchase Completion footer */ -"domain.purchase.preview.footer" = "Det kan ta upp till 30 minuter för din anpassade domän att börja fungera."; +/* Search domain - Title for the Suggested domains screen */ +"domain.management.addDomain.search.title" = "Sök efter en domän"; -/* Domain Purchase Completion description (only for FREE domains). */ -"domain.purchase.preview.free.description" = "Nu ska vi hjälpa dig att göra den redo att visas i webbläsare."; +/* Domain management buy domain card button title */ +"domain.management.buy.card.button.title" = "Skaffa domän"; -/* Domain Purchase Completion description (only for PAID domains). */ -"domain.purchase.preview.paid.description" = "Vi har skickat dig ett kvitto på din e-post. Nu ska vi hjälpa dig att få den redo för alla."; +/* Domain management buy domain card subtitle */ +"domain.management.buy.card.subtitle" = "Lägg till en webbplats senare."; -/* Reflects that site is live when domain purchase feature flag is ON. */ -"domain.purchase.preview.title" = "Grattis, din webbplats är live!"; +/* Domain management buy domain card title */ +"domain.management.buy.card.title" = "Köp bara en domän"; + +/* The expired label of the domain card in All Domains screen. */ +"domain.management.card.expired.label" = "Löpt ut"; + +/* The renews label of the domain card in All Domains screen. */ +"domain.management.card.renews.label" = "Förnyas"; + +/* The empty state button title in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.button.title" = "Hitta en domän"; + +/* The empty state description in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.description" = "Tryck nedan för att hitta din perfekta domän."; + +/* The empty state title in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.title" = "Du har inga domäner"; + +/* The empty state description in All Domains screen when an error occurs */ +"domain.management.error.empty.state.description" = "Vi stötte på ett fel vid inläsningen av dina domäner. Kontakta supporten om problemet kvarstår."; + +/* The empty state title in All Domains screen when an error occurs */ +"domain.management.error.empty.state.title" = "Något gick fel"; + +/* The empty state button title in All Domains screen when an error occurs */ +"domain.management.error.state.button.title" = "Försök igen"; + +/* The empty state description in All Domains screen when the user is offline */ +"domain.management.offline.empty.state.description" = "Kontrollera din nätverksanslutning och försök igen."; + +/* The empty state title in All Domains screen when the user is offline */ +"domain.management.offline.empty.state.title" = "Ingen internetanslutning"; -/* Status of a domain in `Action Required` state */ -"domain.status.action.required" = "Åtgärd krävs"; +/* Domain management choose site card button title */ +"domain.management.purchase.footer" = "*En gratis domän i ett år är inkluderat med alla betalda årliga paket"; -/* Status of a domain in `Active` state */ -"domain.status.active" = "Aktiv"; +/* Domain management purchase domain screen title */ +"domain.management.purchase.subtitle" = "Oroa dig inte, du kan enkelt lägga till en webbplats senare."; -/* Status of a domain in `Complete Setup` state */ -"domain.status.complete.setup" = "Slutför konfiguration"; +/* Domain management purchase domain screen title. */ +"domain.management.purchase.title" = "Välj hur du vill använda din domän"; -/* Status of a domain in `Error` state */ -"domain.status.error" = "Fel"; +/* The search bar title in All Domains screen. */ +"domain.management.search-bar.title" = "Sök domäner"; -/* Status of a domain in `Expired` state */ -"domain.status.expired" = "Har löpt ut"; +/* The empty state description in All Domains screen when the are no domains matching the search criteria */ +"domain.management.search.empty.state.description" = "Vi kunde inte hitta några domäner som matchar din sökning på \"%@\""; -/* Status of a domain in `Expiring Soon` state */ -"domain.status.expiring.soon" = "Löper ut snart"; +/* The empty state title in All Domains screen when the are no domains matching the search criteria */ +"domain.management.search.empty.state.title" = "Inga matchande domäner hittades"; -/* Status of a domain in `Failed` state */ -"domain.status.failed" = "Misslyckades"; +/* Domain management choose site card button title */ +"domain.management.site.card.button.title" = "Välj webbplats"; -/* Status of a domain in `In Progress` state */ -"domain.status.in.progress" = "Pågår"; +/* Domain management choose site card subtitle */ +"domain.management.site.card.footer" = "Gratis domän för det första året*"; -/* Status of a domain in `Renew` state */ -"domain.status.renew" = "Förnya"; +/* Domain management choose site card subtitle */ +"domain.management.site.card.subtitle" = "Använd med en webbplats du redan startat."; -/* Status of a domain in `Verify Email` state */ -"domain.status.verify.email" = "Verifiera e-post"; +/* Domain management choose site card title */ +"domain.management.site.card.title" = "Befintlig WordPress.com-webbplats"; -/* Status of a domain in `Verifying` state */ -"domain.status.verifying" = "Verifierar"; +/* Domain Management Screen Title */ +"domain.management.title" = "Alla domäner"; + +/* Domain Purchase Completion footer */ +"domain.purchase.preview.footer" = "Det kan ta upp till 30 minuter för din anpassade domän att börja fungera."; + +/* Domain Purchase Completion description (only for FREE domains). */ +"domain.purchase.preview.free.description" = "Nu ska vi hjälpa dig att göra den redo att visas i webbläsare."; + +/* Domain Purchase Completion description (only for PAID domains). */ +"domain.purchase.preview.paid.description" = "Vi har skickat dig ett kvitto på din e-post. Nu ska vi hjälpa dig att få den redo för alla."; + +/* Reflects that site is live when domain purchase feature flag is ON. */ +"domain.purchase.preview.title" = "Grattis, din webbplats är live!"; /* The 'Best Alternative' label under the domain name in 'Choose a domain' screen */ "domain.suggestions.row.best-alternative" = "Bästa alternativ"; @@ -9936,12 +9788,24 @@ Example: Reply to Pamela Nguyen */ /* The text to display for paid domains in 'Site Creation > Choose a domain' screen */ "domain.suggestions.row.yearly" = "per år"; +/* Title for the checkout screen. */ +"domains.checkout.title" = "Kassa"; + /* Action shown in a bottom notice to dismiss it. */ "domains.failure.dismiss" = "Avfärda"; /* Content show when the domain selection action fails. */ "domains.failure.title" = "Domänen du försöker lägga till kan inte köpas i Jetpack-appen just nu."; +/* Title for the screen where the user can choose how to use the domain they're end up purchasing. */ +"domains.purchase.choice.title" = "Köp domän"; + +/* Back button title that navigates back to the search domains screen. */ +"domains.search.backButton.title" = "Sök"; + +/* Title of screen where user chooses a site to connect to their selected domain */ +"domains.sitePicker.title" = "Välj webbplats"; + /* No comment provided by engineer. */ "double-tap to change unit" = "dubbeltryck för att ändra enhet"; @@ -9959,6 +9823,15 @@ Example: Reply to Pamela Nguyen */ Site Address placeholder */ "example.com" = "example.com"; +/* Title for confirmation navigation bar button item */ +"externalMediaPicker.add" = "Lägg till"; + +/* Bottom toolbar title in the selection mode */ +"externalMediaPicker.toolbarSelectItemsPrompt" = "Välj bilder"; + +/* Bottom toolbar title in the selection mode */ +"externalMediaPicker.toolbarViewSelected" = "Visa valda (%@)"; + /* Title of screen the displays the details of an advertisement campaign. */ "feature.blaze.campaignDetails.title" = "Kampanjdetaljer"; @@ -10058,9 +9931,6 @@ Example: Reply to Pamela Nguyen */ /* (placeholder) Help the user enter a URL into the field */ "http:\/\/my-site-address (URL)" = "http:\/\/min-webbplats-adress (URL)"; -/* Label displayed on image media items. */ -"image" = "bild"; - /* Sentence to justify why the app is asking permission from the user to use their camera. */ "infoplist.NSCameraUsageDescription" = "För att ta bilder eller spela in videoklipp som kan användas i dina inlägg."; @@ -10361,6 +10231,9 @@ Example: Reply to Pamela Nguyen */ /* Indicating that referrer was marked as spam */ "marked as spam" = "markerad som skräppost"; +/* Products header text in Me Screen. */ +"me.products.header" = "Produkter"; + /* Title of error prompt shown when a sync fails. */ "media.syncFailed" = "Det gick inte att synkronisera media"; @@ -10373,18 +10246,24 @@ Example: Reply to Pamela Nguyen */ /* Message of an alert informing users that the video they are trying to select is not allowed. */ "mediaExporter.videoLimitExceededError" = "För att ladda upp videoklipp som är längre än 5 minuter krävs ett betalt paket."; -/* Verb. User action to dismiss error alert when failing to load media item. */ -"mediaItemTable.errorAlert.dismissButton" = "Avfärda"; - /* Accessibility hint for add button to add items to the user's media library */ "mediaLibrary.addButtonAccessibilityHint" = "Lägg till ny media"; /* Accessibility label for add button to add items to the user's media library */ "mediaLibrary.addButtonAccessibilityLabel" = "Lägg till"; +/* Button name in the more menu */ +"mediaLibrary.aspectRatioGrid" = "Bildförhållande för rutnät"; + +/* Context menu button */ +"mediaLibrary.buttonDelete" = "Ta bort"; + /* Media screen navigation bar button Select title */ "mediaLibrary.buttonSelect" = "Välj"; +/* Context menu button */ +"mediaLibrary.buttonShare" = "Dela"; + /* Verb. Button title. Tapping cancels an action. */ "mediaLibrary.deleteConfirmationCancel" = "Avbryt"; @@ -10406,6 +10285,21 @@ Example: Reply to Pamela Nguyen */ /* Text displayed in HUD after successfully deleting a media item */ "mediaLibrary.deletionSuccessMessage" = "Borttaget!"; +/* The name of the media filter */ +"mediaLibrary.filterAll" = "Allt"; + +/* The name of the media filter */ +"mediaLibrary.filterAudio" = "Ljud"; + +/* The name of the media filter */ +"mediaLibrary.filterDocuments" = "Dokument"; + +/* The name of the media filter */ +"mediaLibrary.filterImages" = "Bilder"; + +/* The name of the media filter */ +"mediaLibrary.filterVideos" = "Videoklipp"; + /* User action to delete un-uploaded media. */ "mediaLibrary.retryOptionsAlert.delete" = "Ta bort"; @@ -10418,6 +10312,12 @@ Example: Reply to Pamela Nguyen */ /* Message displayed when no results are returned from a media library search. Should match Calypso. */ "mediaLibrary.searchResultsEmptyTitle" = "Ingen media matchar din sökning"; +/* Text displayed in HUD if there was an error attempting to share a group of media items. */ +"mediaLibrary.sharingFailureMessage" = "Det gick inte att dela de valda objekten."; + +/* Button name in the more menu */ +"mediaLibrary.squareGrid" = "Fyrkantigt rutnät"; + /* Media screen navigation title */ "mediaLibrary.title" = "Media"; @@ -10439,6 +10339,12 @@ Example: Reply to Pamela Nguyen */ /* The title of the button to dismiss the alert shown when the picked media cannot be imported into stories. */ "mediaPicker.failedMediaExportAlert.dismissButton" = "Avfärda"; +/* Error message when picked media cannot be imported into stories. */ +"mediaPicker.failedMediaExportAlert.message" = "Det gick inte att exportera din media. Om problemet kvarstår kan du kontakta oss via skärmen Jag > Hjälp och support."; + +/* Error title when picked media cannot be imported into stories. */ +"mediaPicker.failedMediaExportAlert.title" = "Export av media misslyckades"; + /* Message for alert when access to camera is not granted */ "mediaPicker.noCameraAccessMessage" = "Denna app behöver åtkomst till kameran för att kunna ta foton och spela in videoklipp. Ändra integritetsinställningarna om du vill tillåta detta."; @@ -10472,6 +10378,12 @@ Example: Reply to Pamela Nguyen */ /* The name of the action in the context menu */ "mediaPicker.takeVideo" = "Spela in en video"; +/* Navigation title for media preview. Example: 1 of 3 */ +"mediaPreview.NofM" = "%1$@ av %2$@"; + +/* Max image size in pixels (e.g. 300x300px) */ +"mediaSizeSlider.valueFormat" = "%1$d × %2$d px"; + /* The description in the Delete WordPress screen */ "migration.deleteWordpress.description" = "Det ser ut som att du fortfarande har WordPress-appen installerad."; @@ -10484,9 +10396,6 @@ Example: Reply to Pamela Nguyen */ /* The title in the Delete WordPress screen */ "migration.deleteWordpress.title" = "Du behöver inte längre WordPress-appen på din enhet"; -/* Primary button title in the migration done screen. */ -"migration.done.actions.primary.title" = "Avsluta"; - /* Footer for the migration done screen. */ "migration.done.footer" = "Vi rekommenderar att du avinstallerar WordPress-appen på din enhet för att undvika datakonflikter."; @@ -10496,6 +10405,9 @@ Example: Reply to Pamela Nguyen */ /* Primary description in the migration done screen. */ "migration.done.primaryDescription" = "Vi har flyttat över alla data och dina inställningar, så allt är precis som förut."; +/* Secondary description (second paragraph) in the migration done screen. */ +"migration.done.secondaryDescription" = "Det är dags att fortsätta din WordPress-resa med Jetpack-appen!"; + /* Title of the migration done screen. */ "migration.done.title" = "Tack för att du bytt till Jetpack!"; @@ -10544,6 +10456,9 @@ Example: Reply to Pamela Nguyen */ /* The title in the migration welcome screen */ "migration.welcome.title" = "Välkommen till Jetpack!"; +/* Primary button title in the migration done screen. */ +"migrationDone.actions.primaryTitle" = "Sätt igång"; + /* Description for the static screen displayed prompting users to switch the Jetpack app. */ "movedToJetpack.description" = "Jetpack-appen har alla WordPress-appens funktionalitet och nu exklusiv åtkomst till statistik, Läsare, aviseringar och mer."; @@ -10619,6 +10534,30 @@ Example: Reply to Pamela Nguyen */ /* Message title for when a user has no sites. */ "mySite.noSites.title" = "Du har inga webbplatser"; +/* Menu title for the add site option */ +"mySite.siteActions.addSite" = "Lägg till webbplats"; + +/* Button that reveals more site actions */ +"mySite.siteActions.button" = "Webbplatsåtgärder"; + +/* Accessibility hint for button used to show more site actions */ +"mySite.siteActions.hint" = "Tryck för att visa fler webbplatsåtgärder"; + +/* Menu title for the personalize home option */ +"mySite.siteActions.personalizeHome" = "Personifiera hem"; + +/* Menu title for the change site icon option */ +"mySite.siteActions.siteIcon" = "Ändra webbplats-ikon"; + +/* Menu title for the change site title option */ +"mySite.siteActions.siteTitle" = "Ändra webbplatsrubrik"; + +/* Menu title for the switch site option */ +"mySite.siteActions.switchSite" = "Byt webbplats"; + +/* Menu title for the visit site option */ +"mySite.siteActions.visitSite" = "Besök webbplats"; + /* Dismiss button title. */ "noResultsViewController.dismissButton" = "Avfärda"; @@ -10634,14 +10573,20 @@ Example: Reply to Pamela Nguyen */ /* This is one of the buttons we display when prompting the user for a review */ "notifications.appRatings.sendFeedback.yes.buttonTitle" = "Skicka feedback"; -/* Word separating the current index from the total amount. I.e.: 7 of 9 */ -"of" = "av"; +/* Badge for page cells */ +"pageList.badgeHomepage" = "Startsida"; + +/* Badge for page cells */ +"pageList.badgeLocalChanges" = "Lokala ändringar"; -/* Label displayed on media items that are not video, image, or audio. */ -"other" = "övrigt"; +/* Badge for page cells */ +"pageList.badgePendingReview" = "Inväntar granskning"; -/* Promote the page with Blaze. */ -"pages.blaze.actionTitle" = "Marknadsför med Blaze"; +/* Badge for page cells */ +"pageList.badgePosts" = "Inläggssida"; + +/* Badge for page cells */ +"pageList.badgePrivate" = "Privat"; /* Subtitle of the theme template homepage cell */ "pages.template.subtitle" = "Din startsida använder en temamall och kommer att öppnas i webbredigeraren."; @@ -10649,6 +10594,36 @@ Example: Reply to Pamela Nguyen */ /* Title of the theme template homepage cell */ "pages.template.title" = "Startsida"; +/* Message informing the user that their static homepage page was set successfully */ +"pages.updatePage.successTitle" = "Sidan har uppdaterats"; + +/* Delete option in the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.actionTitle" = "Ta bort permanent"; + +/* Message of the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.alertMessage" = "Är du säker på att du vill ta bort denna sida permanent?"; + +/* Title of the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.alertTitle" = "Ta bort permanent?"; + +/* Menu option for filtering posts by everyone */ +"pagesList.pagesByEveryone" = "Sidor av alla"; + +/* Menu option for filtering posts by me */ +"pagesList.pagesByMe" = "Sidor av mig"; + +/* Trash option in the trash page confirmation alert. */ +"pagesList.trash.actionTitle" = "Flytta till papperskorgen"; + +/* Message of the trash page confirmation alert. */ +"pagesList.trash.alertMessage" = "Är det säkert att du vill lägga denna sida i papperskorgen?"; + +/* Title of the trash page confirmation alert. */ +"pagesList.trash.alertTitle" = "Vill du lägga denna sida i papperskorgen?"; + +/* Cancels an Action */ +"pagesList.trash.cancel" = "Avbryt"; + /* No comment provided by engineer. */ "password" = "lösenord"; @@ -10671,7 +10646,7 @@ Example: Reply to Pamela Nguyen */ "personalizeHome.dashboardCard.pages" = "Sidor"; /* Card title for the pesonalization menu */ -"personalizeHome.dashboardCard.prompts" = "Bloggningsprompter"; +"personalizeHome.dashboardCard.prompts" = "Bloggningsförslag"; /* Card title for the pesonalization menu */ "personalizeHome.dashboardCard.scheduledPosts" = "Schemalagda inlägg"; @@ -10688,6 +10663,51 @@ Example: Reply to Pamela Nguyen */ /* Register Domain - Domain contact information field Phone */ "phone number" = "telefonnummer"; +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.createdTimeAgo" = "Skapades %@"; + +/* Status mesasge for post cells */ +"post.deletingPostPermanentlyStatusMessage" = "Tar bort inlägg …"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.editedTimeAgo" = "Redigerades %@"; + +/* Status mesasge for post cells */ +"post.movingToTrashStatusMessage" = "Flyttar inlägget till papperskorgen …"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.publishedTimeAgo" = "Publicerades %@"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.scheduledForDate" = "Schemalagd %@"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.trashedTimeAgo" = "Borttaget %@"; + +/* Accessibility label for the post author in the post list. The parameter is the author name. For example, \"By Elsa.\" */ +"postList.a11y.authorChunkFormat" = "Av %@."; + +/* Accessibility label for a post's excerpt in the post list. The parameter is the post excerpt. For example, \"Excerpt. This is the first paragraph.\" */ +"postList.a11y.exerptChunkFormat" = "Utdrag. %@."; + +/* Accessibility label for a sticky post in the post list. */ +"postList.a11y.sticky" = "Klistrat."; + +/* Accessibility label for a post in the post list. The first placeholder is the post title. The second placeholder is the date. */ +"postList.a11y.titleAndDateChunkFormat" = "%1$@, %2$@."; + +/* Title for the 'Trash' post list row swipe action */ +"postList.swipeActionDelete" = "Papperskorg"; + +/* Title for the 'Delete' post list row swipe action */ +"postList.swipeActionDeletePermanently" = "Ta bort"; + +/* Title for the 'Share' post list row swipe action */ +"postList.swipeActionShare" = "Dela"; + +/* Title for the 'View' post list row swipe action */ +"postList.swipeActionView" = "Visa"; + /* User action to dismiss featured media options. */ "postSettings.featuredImageUploadActionSheet.dismiss" = "Avfärda"; @@ -10706,9 +10726,84 @@ Example: Reply to Pamela Nguyen */ /* Button in Post Settings */ "postSettings.setFeaturedImageButton" = "Ange utvald bild"; +/* Error message on post/page settings screen */ +"postSettings.updateFailedMessage" = "Det gick inte att uppdatera inläggsinställningarna"; + /* Promote the post with Blaze. */ "posts.blaze.actionTitle" = "Marknadsför med Blaze"; +/* Label for the Post List option that cancels automatic uploading of a post. */ +"posts.cancelUpload.actionTitle" = "Avbryt uppladdning"; + +/* Label for post comments option. Tapping displays comments for a post. */ +"posts.comments.actionTitle" = "Kommentarer"; + +/* Label for the delete post option. Tapping permanently deletes a post. */ +"posts.delete.actionTitle" = "Ta bort permanent"; + +/* Label for an option that moves a post to the draft folder */ +"posts.draft.actionTitle" = "Flytta till utkast"; + +/* Label for post duplicate option. Tapping creates a copy of the post. */ +"posts.duplicate.actionTitle" = "Duplicera"; + +/* Opens a submenu for page attributes. */ +"posts.pageAttributes.actionTitle" = "Sidattribut"; + +/* Label for the preview post button. Tapping displays the post as it appears on the web. */ +"posts.preview.actionTitle" = "Förhandsgranska"; + +/* Label for an option that moves a publishes a post immediately */ +"posts.publish.actionTitle" = "Publicera nu"; + +/* Retry uploading the post. */ +"posts.retry.actionTitle" = "Försök igen"; + +/* Set the selected page as the homepage. */ +"posts.setHomepage.actionTitle" = "Ställ in som startsida"; + +/* Set the parent page for the selected page. */ +"posts.setParent.actionTitle" = "Ställ in överordnad"; + +/* Set the selected page as a posts page. */ +"posts.setPostsPage.actionTitle" = "Ställ in som inläggssida"; + +/* Set the selected page as a regular page. */ +"posts.setRegularPage.actionTitle" = "Ställ in som vanlig sida"; + +/* Label for post settings option. Tapping displays settings for a post. */ +"posts.settings.actionTitle" = "Inställningar"; + +/* Share the post. */ +"posts.share.actionTitle" = "Dela"; + +/* Label for post stats option. Tapping displays statistics for a post. */ +"posts.stats.actionTitle" = "Statistik"; + +/* Label for a option that moves a post to the trash folder */ +"posts.trash.actionTitle" = "Flytta till papperskorg"; + +/* Label for the view post button. Tapping displays the post as it appears on the web. */ +"posts.view.actionTitle" = "Visa"; + +/* A short message explaining that a page was deleted permanently. */ +"postsList.deletePage.message" = "Sida borttagen permanent"; + +/* A short message explaining that a post was deleted permanently. */ +"postsList.deletePost.message" = "Inlägg borttaget permanent"; + +/* A short message explaining that a page was moved to the trash bin. */ +"postsList.movePageToTrash.message" = "Sidan flyttad till papperskorgen"; + +/* A short message explaining that a post was moved to the trash bin. */ +"postsList.movePostToTrash.message" = "Inlägget flyttat till papperskorgen"; + +/* Menu option for filtering posts by everyone */ +"postsList.postsByEveryone" = "Inlägg av alla"; + +/* Menu option for filtering posts by me */ +"postsList.postsByMe" = "Inlägg av mig"; + /* Title for the button to subscribe to Jetpack Social on the remaining shares view */ "postsettings.social.remainingshares.subscribe" = "Prenumerera nu för att dela mer"; @@ -10853,13 +10948,15 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Text for the 'Like' button on the reader post card cell. */ "reader.post.button.like" = "Gilla"; -/* Accessibility hint for the like button on the reader post card cell - Accessibility hint for the liked button on the reader post card cell */ +/* Accessibility hint for the like button on the reader post card cell */ "reader.post.button.like.accessibility.hint" = "Gillar inlägget."; /* Text for the 'Liked' button on the reader post card cell. */ "reader.post.button.liked" = "Gillat"; +/* Accessibility hint for the liked button on the reader post card cell */ +"reader.post.button.liked.accessibility.hint" = "Tar bort gillamarkeringen för inlägget."; + /* Accessibility hint for the site header on the reader post card cell */ "reader.post.button.menu.accessibility.hint" = "Öppnar en meny med fler åtgärder."; @@ -10923,6 +11020,12 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Title for the tooltip anchor. */ "readerDetail.tooltipAnchorTitle.accessibilityLabel" = "Nyheter"; +/* The button title for the transfer footer view in Register Domain screen */ +"register.domain.transfer.button.title" = "Överför domän"; + +/* The title for the transfer footer view in Register Domain screen */ +"register.domain.transfer.title" = "Letar du efter att överföra en domän du redan äger?"; + /* Information of what related post are and how they are presented */ "relatedPostsSettings.optionsFooter" = "Relaterade inlägg visar relevant innehåll från din webbplats under dina inlägg."; @@ -11022,6 +11125,21 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Accessibility hint for actions when displaying media items. */ "siteMedia.cellAccessibilityHint" = "Välj media."; +/* Accessibility hint for media item preview for user's viewing an item in their media library */ +"siteMediaItem.contentViewAccessibilityHint" = "Tryck för att visa media i helskärm"; + +/* Accessibility label for media item preview for user's viewing an item in their media library */ +"siteMediaItem.contentViewAccessibilityLabel" = "Förhandsgranska media"; + +/* Title for confirmation navigation bar button item */ +"siteMediaPicker.add" = "Lägg till"; + +/* Button selection media in media picker */ +"siteMediaPicker.deselect" = "Avmarkera"; + +/* Button selection media in media picker */ +"siteMediaPicker.select" = "Markera"; + /* Media screen navigation title */ "siteMediaPicker.title" = "Media"; @@ -11029,7 +11147,7 @@ Example: given a notice format "Following %@" and empty site name, this will be "siteSettings.privacy.title" = "Integritet"; /* Hint for users when hidden privacy setting is set */ -"siteVisibility.hidden.hint" = "Din webbplats är synlig för alla, men begär av sökmotorer att inte indexera den."; +"siteVisibility.hidden.hint" = "Din webbplats döljs för besökare bakom ett \"Kommer snart\"-meddelande tills den är klar att visas."; /* Text for privacy settings: Hidden */ "siteVisibility.hidden.title" = "Dold"; @@ -11190,6 +11308,12 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Dismiss the AlertView */ "stockPhotos.strings.dismiss" = "Avfärda"; +/* Subtitle for placeholder in Free Photos. The company name 'Pexels' should always be written as it is. */ +"stockPhotos.subtitle" = "Foton från Pexels"; + +/* Title for placeholder in Free Photos */ +"stockPhotos.title" = "Sök efter gratis foton att lägga till i ditt mediabibliotek!"; + /* Section title for prominent suggestions */ "suggestions.section.prominent" = "I denna konversation"; @@ -11337,6 +11461,9 @@ Example: given a notice format "Following %@" and empty site name, this will be /* View title for Help & Support page. */ "support.title" = "Hjälp"; +/* Title for placeholder in Tenor picker */ +"tenor.welcomeMessage" = "Leta efter GIF-filer att lägga till i ditt mediabibliotek!"; + /* Header of delete screen section listing things that will be deleted. */ "these items will be deleted:" = "Dessa saker kommer att raderas:"; @@ -11352,9 +11479,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Displayed in the confirmation alert when marking unread notifications as read. */ "unread" = "ej läst"; -/* Label displayed on video media items. */ -"video" = "video"; - /* Portion of a message for Jetpack users that have multisite WP installation, thus Restore is not available. This part is a link, colored with a different color. */ "visit our documentation page" = "besök vår dokumentationssida"; diff --git a/WordPress/Resources/th.lproj/Localizable.strings b/WordPress/Resources/th.lproj/Localizable.strings index aa8f53556545..48563a999685 100644 --- a/WordPress/Resources/th.lproj/Localizable.strings +++ b/WordPress/Resources/th.lproj/Localizable.strings @@ -39,9 +39,6 @@ /* Age between dates over one year. */ "%d years" = "%d ปี"; -/* Max image size in pixels (e.g. 300x300px) */ -"%dx%dpx" = "%1$dx%2$dpx"; - /* Empty Post Title */ "(No Title)" = "(ไม่มีหัวข้อ)"; @@ -73,10 +70,6 @@ /* No comment provided by engineer. */ "Activity Logs" = "log กิจกรรม"; -/* Accessibility label for add button to add items to the user's media library - Remove asset from media picker list */ -"Add" = "เพิ่ม"; - /* The title on the add category screen */ "Add a Category" = "เพิ่มหมวดหมู่"; @@ -90,9 +83,6 @@ Register Domain - Address information field placeholder for Address line */ "Address" = "ที่อยู่"; -/* Description of albums in the photo libraries */ -"Albums" = "อัลบั้ม"; - /* Image alignment option title. Title of the screen for choosing an image's alignment. */ "Alignment" = "จัดตำแหน่ง"; @@ -184,8 +174,7 @@ /* Title of a prompt letting the user know that they must wait until the current aciton completes. */ "Busy" = "ยุ่ง"; -/* Accessibility label for taking an image or video with the camera on formatting toolbar. - Accessibility label for the camera tile in the collection view */ +/* Accessibility label for taking an image or video with the camera on formatting toolbar. */ "Camera" = "กล้อง"; /* Alert dismissal title @@ -224,10 +213,7 @@ Cancels an Action Cancels an alert. Cancels the mark all as read action. - Dismiss the post action sheet Editing GIF alert cancel action button. - Label for a cancel button - Label for the auto-upload cancelation button in the post list. Tapping will prevent the app from auto-uploading the post. Menus cancel button for deleting a menu. Menus cancel button within text bar while editing items. Menus: Cancel button title for canceling an edited menu item. @@ -344,10 +330,6 @@ /* Part of a prompt suggesting that there is more content for the user to read. */ "Continue reading" = "อ่านเพิ่มเติม"; -/* Copy the post url and paste anywhere in phone - Label for page copy link. Tapping copy the url of page */ -"Copy Link" = "คัดลอกลิงก์"; - /* Error message informing the user that there was a problem subscribing to a site or feed. */ "Could not follow the site at the address specified." = "ไม่สามารถติดตามเว็บไซต์ตามที่อยู่ที่ระบุ"; @@ -363,9 +345,6 @@ /* No comment provided by engineer. */ "Couldn't Connect" = "ไม่สามารถเชื่อมต่อได้"; -/* Message to show while media data source is finding the number of items available. */ -"Counting media items..." = "กำลังนับรายการไฟล์สื่อ..."; - /* Period Stats 'Countries' header */ "Countries" = "ประเทศ"; @@ -417,15 +396,11 @@ /* Delete Delete button title for the warning shown to the user when he refuses to re-login when the authToken is missing. Title for button that permanently deletes a media item (photo / video) - Title for button that permanently deletes one or more media items (photos / videos) Title of a delete button Title of the trash confirmation alert. */ "Delete" = "ลบ"; -/* Delete option in the confirmation alert when deleting a page from the trash. - Delete option in the confirmation alert when deleting a post from the trash. - Label for a button permanently deletes a page. - Label for the delete post option. Tapping permanently deletes a post. +/* Delete option in the confirmation alert when deleting a post from the trash. Title for button on the comment details page that deletes the comment when tapped. */ "Delete Permanently" = "ลบอย่างถาวร"; @@ -464,7 +439,6 @@ /* Accessibility label for button to dismiss a bottom sheet Accessibility label for the transparent space above the login dialog which acts as a button to dismiss the dialog. Accessibility label for the transparent space above the signup dialog which acts as a button to dismiss the dialog. - Action to show on alert when view asset fails. Dismiss a view. Verb */ "Dismiss" = "ยกเลิก"; @@ -494,13 +468,9 @@ /* Name for the status of a draft post. */ "Draft" = "โครงเรื่อง"; -/* Accessibility label for edit button to enable multi selection mode in the user's media library - Edit the post. - Editing GIF alert default action button. +/* Editing GIF alert default action button. Edits a Comment Edits the comment - Label for a button that opens the Edit Page view controller - Label for the edit post button. Tapping displays the editor. User action to edit media details. Verb, edit a comment */ "Edit" = "แก้ไข"; @@ -561,9 +531,6 @@ /* Text displayed in HUD after attempting to save a draft post and an error occurred. */ "Error occurred\nduring saving" = "ความผิดพลาดเกิดขึ้น\nในขณะกำลังบันทึก"; -/* Label for the post author filter. This filter shows posts for all users on the blog. */ -"Everyone" = "ทุกคน"; - /* Placeholder text for the tagline of a site */ "Explain what this site is about." = "อธิบายว่าเว็บนี้เกี่ยวกับอะไร"; @@ -687,9 +654,6 @@ /* Title of the screen for choosing an image's size. */ "Image Size" = "ขนาดรูปภาพ"; -/* Accessibility label for image thumbnails in the media collection view. The parameter is the creation date of the image. */ -"Image, %@" = "รูปภาพ %@"; - /* Undated post time label */ "Immediately" = "ทันที"; @@ -859,7 +823,6 @@ "Max Image Upload Size" = "ขนาดรูปภาพอัปโหลดใหญ่สุด"; /* Accessibility label for the Me button in My Site. - Label for the post author filter. This filter shows posts only authored by the current user. Me page title Noun. Title. Links to the Me screen. The accessibility value of the me tab. @@ -867,20 +830,11 @@ "Me" = "ฉัน"; /* Noun. Title. Links to the blog's Media library. - Tab bar title for the Media tab in Media Picker The menu item to select during a guided tour. - Title for Media Library section of the app. Title for the media section in site settings screen Title label for the media settings section in the app settings */ "Media" = "ไฟล์สื่อ"; -/* Title for alert when access to media capture is not granted */ -"Media Capture" = "ตัวจับไฟล์สื่อ"; - -/* Title for alert when a generic error happened when loading media - Title for alert when access to the media library is not granted by the user */ -"Media Library" = "คลังไฟล์สื่อ"; - /* Message to indicate progress of uploading media to server */ "Media Uploading" = "อัปโหลดไฟล์สื่อ"; @@ -898,24 +852,15 @@ "Months and Years" = "เดือนและปี"; /* Accessibility label for more button in dashboard quick start card. - Accessibility label for the More button in Page List. Accessibility label for the More button in Post List (compact view). Accessibility label for the More button on formatting toolbar. Accessibility label for the More button on Reader Cell Accessibility label for the More button on Reader's post details - Action button to display more available options - Label for the more post button. Tapping displays an action sheet with post options. */ + Action button to display more available options */ "More" = "เพิ่มเติม"; -/* Label for a button that moves a page to the draft folder - Label for an option that moves a post to the draft folder */ -"Move to Draft" = "ย้ายไปฉบับร่าง"; - -/* Label for a button that moves a page to the trash folder - Label for a option that moves a post to the trash folder - Title for button on the comment details page that moves the comment to trash when tapped. +/* Title for button on the comment details page that moves the comment to trash when tapped. Trash option in the trash confirmation alert. - Trash option in the trash page confirmation alert. Trashes the comment */ "Move to Trash" = "ย้ายไปถังขยะ"; @@ -928,7 +873,8 @@ Title of My Site tab */ "My Site" = "เว็บของฉัน"; -/* Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ +/* Title for site picker screen. + Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ "My Sites" = "เว็บไซต์ของฉัน"; /* 'Need help?' button label, links off to the WP for iOS FAQ. */ @@ -1020,7 +966,6 @@ Alert dismissal title Button title. Acknowledges a prompt. Button title. An acknowledgement of the message displayed in a prompt. - Confirmation of action Default action Dismisses the alert Marks all notifications as read. @@ -1097,9 +1042,6 @@ Other Sites Notification Settings Title */ "Other Sites" = "เว็บอื่น"; -/* A short message explaining that a page was moved to the trash bin. */ -"Page moved to trash." = "ย้ายหน้าไปถังขยะ"; - /* Noun. Title. Links to the blog's Pages screen. The item to select during a guided tour. This is the section title @@ -1132,8 +1074,7 @@ Title of pending Comments filter. */ "Pending" = "รอตรวจสอบ"; -/* Name for the status of a post pending review. - Title of the Pending Review Badge */ +/* Name for the status of a post pending review. */ "Pending review" = "รอคอยการตรวจสอบ"; /* Noun. Title of the people management feature. @@ -1187,18 +1128,6 @@ The post formats available for the post. Should be the same as in core WP. */ "Post Format" = "รูปแบบเรื่อง"; -/* Prompts the user that a restored post was moved to the drafts list. */ -"Post Restored to Drafts" = "กู้คืนเรื่องไปยังสถานะฉบับร่าง"; - -/* Prompts the user that a restored post was moved to the published list. */ -"Post Restored to Published" = "กู้คืนเรื่องไปยังสถานะเผยแพร่"; - -/* Prompts the user that a restored post was moved to the scheduled list. */ -"Post Restored to Scheduled" = "กู้คืนเรื่องไปยังสถานะรอการเผยแพร่ตามตารางเวลา"; - -/* A short message explaining that a post was moved to the trash bin. */ -"Post moved to trash." = "ย้ายเรื่องไปถังขยะ"; - /* All Time Stats 'Posts' label Insights 'Posts' header Noun. Title. Links to the blog's Posts screen. @@ -1232,8 +1161,7 @@ "Privacy Policy" = "นโยบายความเป็นส่วนตัว"; /* Name for the status of a post that is marked private. - Privacy setting for posts set to 'Private'. Should be the same as in core WP. - Title of the Private Badge */ + Privacy setting for posts set to 'Private'. Should be the same as in core WP. */ "Private" = "ส่วนตัว"; /* Message for the warning shown to the user when he refuses to re-login when the authToken is missing. */ @@ -1250,14 +1178,12 @@ Title for the publish settings view */ "Publish" = "เผยแพร่"; -/* A short phrase indicating a post is due to be immedately published. - Label for a button that moves a page to the published folder, publishing with the current date/time. */ +/* A short phrase indicating a post is due to be immedately published. */ "Publish Immediately" = "เผยแพร่ทันที"; /* Name for the status of a published post. Period Stats 'Published' header - Title of the published filter. This filter shows a list of posts that the user has published. - Title of the published header in search list. */ + Title of the published filter. This filter shows a list of posts that the user has published. */ "Published" = "เผยแพร่แล้ว"; /* Published on [date] */ @@ -1304,8 +1230,7 @@ /* Label for selecting the related posts options */ "Related Posts" = "เรื่องที่เกี่ยวข้อง"; -/* Add asset to media picker list - Alert button to confirm a plugin to be removed +/* Alert button to confirm a plugin to be removed Button label when removing a blog Label for the Remove Feature Image icon. Tapping will show a confirmation screen for removing the feature image from the post. Remove Action @@ -1349,9 +1274,6 @@ /* Setting: WordPress.com Surveys */ "Research" = "การค้นคว้า"; -/* Title of the reset button */ -"Reset" = "ล้างค่า"; - /* Title displayed for restore action. Title for button allowing user to restore their Jetpack site Title for Jetpack Restore Complete screen @@ -1366,12 +1288,9 @@ Button title, displayed when media has failed to upload. Allows the user to try the upload again. Button title. Retries uploading a post. If a user taps the button with this label, the action that evinced this error view will be retried. - Label for a button that attempts to re-upload a page that previously failed to upload. - Label for the retry post upload button. Tapping attempts to upload the post again. Opens the media library . Retries the upload of a user's post. Retry updating User's Role - Retry uploading the post. Retry. Action Retry. Verb – retry a failed media upload. The Jetpack view button title used when an error occurred @@ -1380,9 +1299,6 @@ User action to retry media upload. */ "Retry" = "ลองใหม่"; -/* User action to retry media upload. */ -"Retry Upload" = "ลองอัปโหลดใหม่"; - /* No comment provided by engineer. */ "Retry?" = "ลองใหม่?"; @@ -1405,11 +1321,7 @@ /* Button shown if there are unsaved changes and the author is trying to move away from the post. */ "Save Draft" = "บันทึกโครงร่าง"; -/* Text displayed in HUD when a media item's metadata (title, etc) is saved successfully. */ -"Saved!" = "บันทึกแล้ว!"; - /* Menus save button title while it is saving a Menu. - Text displayed in HUD while a media item's metadata (title, etc) is being saved. Text displayed in HUD while a post is being saved as a draft. */ "Saving..." = "กำลังบันทึก..."; @@ -1465,7 +1377,6 @@ /* Accessibility label for share buttons in nav bars Button label to share a post Button label to share a web page - Share the post. Shares the comment URL Spoken accessibility label Title for a button that allows the user to share their answer to the prompt. @@ -1582,8 +1493,7 @@ /* Standard post format label */ "Standard" = "มาตรฐาน"; -/* Label for post stats option. Tapping displays statistics for a post. - Noun. Abbreviation of Statistics. Name of the Stats feature +/* Noun. Abbreviation of Statistics. Name of the Stats feature Noun. Abbv. of Statistics. Links to a blog's Stats screen. Stats 3D Touch Shortcut Stats window title @@ -1607,7 +1517,7 @@ Theme Support action title */ "Support" = "สนับสนุน"; -/* Button used to switch site */ +/* Title for back button that leads to the site picker screen. */ "Switch Site" = "เปลี่ยนเว็บไซต์"; /* Label for tagline blog setting @@ -1686,7 +1596,7 @@ /* Message shown when the reader finds no posts for the chosen list */ "The sites in this list have not posted anything recently." = "เว็บในรายชื่อนั้นไม่มีการเพิ่มเรื่องใด ๆ เมื่อเร็ว ๆ มานี้"; -/* Error message informing a user about an invalid password. */ +/* No comment provided by engineer. */ "The username or password stored in the app may be out of date. Please re-enter your password in the settings and try again." = "ชื่อผู้ใช้หรือรหัสผ่านที่เก็บอยู่ในแอพ อาจจะเก่าจนใช้ไม่ได้แล้ว กรุณาใส่รหัสผ่านของคุณอีกครั้งในการตั้งค่าและลองใหม่อีกครั้ง"; /* Title of alert when theme activation succeeds */ @@ -1716,15 +1626,9 @@ /* Error message informing the user that there was a problem clearing the block on site preventing its posts from displaying in the reader. */ "There was a problem removing the block for specified site." = "มีปัญหาในการปิดกั้นสำหรับเว็บที่กำหนด"; -/* Explaining to the user there was an generic error accesing media. */ -"There was a problem when trying to access your media. Please try again later." = "มีปัญหาในการพยายามเข้าถึงไฟล์สื่อของคุณ โปรดลองใหม่อีกครั้งภายหลัง"; - /* An error message display if the users device does not have a camera input available */ "This app needs permission to access the Camera to capture new media, please change the privacy settings if you wish to allow this." = "แอพนี้ต้องการการอนุญาตเข้าถึงกล้องเพื่อถ่ายรูป\/วีดีโอใหม่ โปรดเปลี่ยนการตั้งค่าความเป็นส่วนตัวถ้าคุณต้องการที่จะอนุญาตสิ่งนี้"; -/* Explaining to the user why the app needs access to the device media library. */ -"This app needs permission to access your device media library in order to add photos and\/or video to your posts. Please change the privacy settings if you wish to allow this." = "แอพนี้ต้องการการอนุญาตเข้าถึงคลังไฟล์สื่ออุปกรณ์ของคุณเพื่อจะเพิ่มรูปภาพ และ\/หรือ ไฟล์วีดีโอไปยังเรื่องของคุณ โปรดเปลี่ยนการตั้งค่าความเป็นส่วนตัวถ้าคุณต้องการที่จะอนุญาตสิ่งนี้"; - /* Message shown when the reader finds no posts for the chosen site */ "This site has not posted anything yet. Try back later." = "ยังไม่มีการเขียนเรื่องบนเว็บ ลองอีกครั้งภายหลัง"; @@ -1774,8 +1678,7 @@ /* Label displaying total number of WordPress.com followers. %@ is the total. */ "Total WordPress.com Followers: %@" = "ผู้ติดตาม WordPress.com ทั้งหมด: %@"; -/* Accessibility label for trash button to delete items from the user's media library - Accessibility label for trash buttons in nav bars +/* Accessibility label for trash buttons in nav bars Trashes a comment Trashes the comment */ "Trash" = "ถังขยะ"; @@ -1803,9 +1706,6 @@ /* URL text field placeholder */ "URL" = "URL"; -/* An error message. */ -"Unable to Connect" = "ไม่สามารถเชื่อมต่อ"; - /* Title of a prompt saying the app needs an internet connection before it can load posts */ "Unable to Load Posts" = "ไม่สามารถโหลดเรื่อง"; @@ -1830,8 +1730,6 @@ Button title. Reverts the previous notification operation Revert an operation Revert enabling notification after successfully subcribing to the comments for the post. - The title of an 'undo' button. Tapping the button moves a trashed page out of the trash folder. - The title of an 'undo' button. Tapping the button moves a trashed post out of the trash folder. Undo action */ "Undo" = "ย้อนกลับ"; @@ -1911,15 +1809,10 @@ /* No comment provided by engineer. */ "Username must be at least 4 characters." = "ชื่อผู้ใช้ต้องมีอย่างน้อย 4 ตัวอักษร"; -/* Accessibility label for video thumbnails in the media collection view. The parameter is the creation date of the video. */ -"Video, %@" = "ไฟล์วีดีโอ %@"; - /* Period Stats 'Videos' header */ "Videos" = "วีดีโอ"; /* Button title. Displays a summary / sharing screen for a specific post. - Label for a button that opens the page when tapped. - Label for the view post button. Tapping displays the post as it appears on the web. Opens the post epilogue screen to allow sharing / viewing of a post. Theme View action title Verb. The screen title shown when viewing a post inside the app. */ diff --git a/WordPress/Resources/tr.lproj/Localizable.strings b/WordPress/Resources/tr.lproj/Localizable.strings index bc5401b72bbc..fff87e6c9bd8 100644 --- a/WordPress/Resources/tr.lproj/Localizable.strings +++ b/WordPress/Resources/tr.lproj/Localizable.strings @@ -1,4 +1,4 @@ -/* Translation-Revision-Date: 2023-10-18 17:54:08+0000 */ +/* Translation-Revision-Date: 2024-01-03 12:54:08+0000 */ /* Plural-Forms: nplurals=2; plural=n > 1; */ /* Generator: GlotPress/4.0.0-alpha.11 */ /* Language: tr */ @@ -152,9 +152,6 @@ /* Difference label for Period Overview stat, indicating change from previous period. Ex: +99.9K (5%) */ "%@%@ (%@%%)" = "%1$@%2$@ (%3$@%%)"; -/* Accessibility label for a post in the post list. The parameters are the title, and date respectively. For example, \"Let it Go, 1 hour ago.\" */ -"%@, %@." = "%1$@, %2$@."; - /* Accessibility value for a Stats' Posting Activity Month if the user selected a day with posts. The first parameter is day (e.g. November 2019). The second parameter is the number of posts. */ "%@. %d posts." = "%1$@. %2$d yazı."; @@ -210,9 +207,6 @@ /* Age between dates over one year. */ "%d years" = "%d yıl"; -/* Max image size in pixels (e.g. 300x300px) */ -"%dx%dpx" = "%1$dx%2$dpx"; - /* One menu area available in the theme */ "%i menu area in this theme" = "Bu temadaki %i menü alanı"; @@ -271,6 +265,9 @@ translators: Block name. %s: The localized block name */ /* translators: %s: social link name e.g: \"Instagram\". */ "%s social icon" = "%s sosyal simge"; +/* translators: displayed right after the classic block is converted to blocks. %s: The localized classic block name */ +"'%s' block converted to blocks" = "\"%s\" blok, bloklara dönüştürüldü"; + /* translators: Missing block alert title. %s: The localized block name */ "'%s' is not fully-supported" = "\"%s\" tamamen desteklenmiyor"; @@ -478,13 +475,6 @@ translators: Block name. %s: The localized block name */ /* Label for the Activity Type filter button when there are more than 1 activity type selected */ "Activity Type (%1$d)" = "Etkinlik Türü (%1$d)"; -/* Accessibility label for add button to add items to the user's media library - Remove asset from media picker list */ -"Add" = "Ekle"; - -/* Action for Media Picker to indicate selection of media. The argument in the string represents the number of elements (as numeric digits) selected */ -"Add %@" = "%@ ekle"; - /* No comment provided by engineer. */ "Add Block After" = "Blok sonrasına ekle"; @@ -581,9 +571,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Add menu item to children" = "Alt öğelere menü öğesi ekle"; -/* Accessibility hint for add button to add items to the user's media library */ -"Add new media" = "Yeni ortam ekle"; - /* Screen reader text for Menus button that adds a new menu to a site. */ "Add new menu" = "Yeni menü ekle"; @@ -649,9 +636,6 @@ translators: Block name. %s: The localized block name */ /* Option to select the Airmail app when logging in with magic links */ "Airmail" = "Airmail"; -/* Description of albums in the photo libraries */ -"Albums" = "Albümler"; - /* Image alignment option title. Title of the screen for choosing an image's alignment. */ "Alignment" = "Hizalama"; @@ -665,6 +649,9 @@ translators: Block name. %s: The localized block name */ Title of the drafts filter. This filter shows a list of draft posts. */ "All" = "Tümü"; +/* Information about redeeming domain credit on site dashboard. */ +"All WordPress.com annual plans include a custom domain name. Register your free domain now." = "Tüm WordPress.com yıllık paketleri kişisel bir alan adı içerir. Ücretsiz alan adınızı hemen kaydedin."; + /* Footer of the free domain registration section for a paid plan. Information about redeeming domain credit on site dashboard. */ "All WordPress.com plans include a custom domain name. Register your free premium domain now." = "Tüm WordPress.com planları kişisel bir alan adı içerir. Ücretsiz premium alan adınızı hemen kaydedin."; @@ -730,10 +717,13 @@ translators: Block name. %s: The localized block name */ "Alt Text" = "Alternatif metin"; /* No comment provided by engineer. */ -"Alternatively, you can detach and edit these blocks separately by tapping “Detach patterns”." = "Alternatif olarak, \"Desenleri ayır\" seçeneğine dokunarak bu blokları ayırabilir ve ayrı ayrı düzenleyebilirsiniz."; +"Alternatively, you can convert the content to blocks." = "Alternatif olarak, içeriği bloklara dönüştürebilirsiniz."; /* No comment provided by engineer. */ -"Alternatively, you can detach and edit this block separately by tapping “Detach pattern”." = "Alternatif olarak, \"Deseni ayır\" seçeneğine dokunarak bu bloku ayırabilir ve ayrı ayrı düzenleyebilirsiniz."; +"Alternatively, you can detach and edit this block separately by tapping “Detach”." = "Alternatif olarak \"Ayır\" seçeneğine dokunarak bu bloku ayırabilir ve ayrı ayrı düzenleyebilirsiniz."; + +/* translators: Alternative option included in a warning related to having blocks deeply nested. */ +"Alternatively, you can flatten the content by ungrouping the block." = "Alternatif olarak bloku gruplamadan çıkararak içeriği düzleştirebilirsiniz."; /* Instruction text to explain to help users type their password instead of using magic link login option. */ "Alternatively, you may enter the password for this account." = "Alternatif olarak, bu hesabın şifresini girebilirsiniz."; @@ -882,15 +872,9 @@ translators: Block name. %s: The localized block name */ /* Message prompting the user to confirm that they want to disconnect Jetpack from the site. */ "Are you sure you want to disconnect Jetpack from the site?" = "Jetpack ile site bağlantısını kesmek istediğinizden emin misiniz?"; -/* Message prompting the user to confirm that they want to permanently delete a group of media items. */ -"Are you sure you want to permanently delete these items?" = "Seçili öğeleri kalıcı olarak silmek istediğinizden emin misiniz?"; - /* Message prompting the user to confirm that they want to permanently delete a media item. Should match Calypso. */ "Are you sure you want to permanently delete this item?" = "Bu ögeyi kalıcı olarak silmek istediğinizden emin misiniz?"; -/* Message of the confirmation alert when deleting a page from the trash. */ -"Are you sure you want to permanently delete this page?" = "Bu sayfayı kalıcı olarak silmek istediğinizden emin misiniz?"; - /* Message of the confirmation alert when deleting a post from the trash. */ "Are you sure you want to permanently delete this post?" = "Bu yazıyı kalıcı olarak silmek istediğinizden emin misiniz?"; @@ -922,9 +906,6 @@ translators: Block name. %s: The localized block name */ /* Title of message shown when user taps submit for review. */ "Are you sure you want to submit for review?" = "İncelenmek üzere göndermek istediğinizden emin misiniz?"; -/* Message of the trash page confirmation alert. */ -"Are you sure you want to trash this page?" = "Bu sayfayı çöpe atmak istediğinizden emin misiniz?"; - /* Message of the trash confirmation alert. */ "Are you sure you want to trash this post?" = "Bu yazıyı çöpe taşımak istediğinize emin misiniz?"; @@ -965,9 +946,6 @@ translators: Block name. %s: The localized block name */ /* translators: accessibility text. Empty Audio caption. */ "Audio caption. Empty" = "Ses yazısı. Boş"; -/* Accessibility label for audio items in the media collection view. The parameter is the creation date of the audio. */ -"Audio, %@" = "Ses, %@"; - /* No comment provided by engineer. */ "Authenticating" = "Doğrulanıyor"; @@ -1178,10 +1156,7 @@ translators: Block name. %s: The localized block name */ "Blocks menu" = "Bloklar menüsü"; /* translators: Warning related to having blocks deeply nested. %d: The deepest nesting level. */ -"Blocks nested deeper than %d levels may not render properly in the mobile editor. For this reason, we recommend flattening the content by ungrouping the block or editing the block using the web editor." = "%d seviyelerinden daha derin bir şekilde iç içe geçmiş bloklar, mobil düzenleyicide düzgün şekilde oluşmayabilir. Bu nedenle, bloku gruplamadan çıkararak veya web düzenleyicisini kullanarak bloku düzenleme yoluyla içeriği düzleştirmenizi öneririz."; - -/* translators: Warning related to having blocks deeply nested. %d: The deepest nesting level. */ -"Blocks nested deeper than %d levels may not render properly in the mobile editor. For this reason, we recommend flattening the content by ungrouping the block or editing the block using your web browser." = "%d seviyelerinden daha derin bir şekilde iç içe geçmiş bloklar, mobil düzenleyicide düzgün şekilde oluşmayabilir. Bu nedenle, bloku gruplamadan çıkararak veya web tarayıcınızı kullanarak bloku düzenleme yoluyla içeriği düzleştirmenizi öneririz."; +"Blocks nested deeper than %d levels may not render properly in the mobile editor." = "%d seviyelerinden daha derin bir şekilde iç içe geçmiş bloklar, mobil düzenleyicide düzgün şekilde oluşmayabilir."; /* Title of a button that displays the WordPress.com blog */ "Blog" = "Blog"; @@ -1259,9 +1234,6 @@ translators: Block name. %s: The localized block name */ /* Label for the post author in the post detail. */ "By " = "Gönderen:"; -/* Accessibility label for the post author in the post list. The parameter is the author name. For example, \"By Elsa.\" */ -"By %@." = "%@ tarafından."; - /* Legal disclaimer for logging in. The underscores _..._ denote underline. */ "By continuing, you agree to our _Terms of Service_." = "Devam ederek _hizmet koşullarını_ kabul etmiş olursunuz."; @@ -1281,8 +1253,7 @@ translators: Block name. %s: The localized block name */ /* Label for size of media while it's being calculated. */ "Calculating..." = "Hesaplanıyor..."; -/* Accessibility label for taking an image or video with the camera on formatting toolbar. - Accessibility label for the camera tile in the collection view */ +/* Accessibility label for taking an image or video with the camera on formatting toolbar. */ "Camera" = "Kamera"; /* Title of an alert informing the user the camera permission for the app is disabled and its needed to proceed */ @@ -1333,10 +1304,7 @@ translators: Block name. %s: The localized block name */ Cancels an Action Cancels an alert. Cancels the mark all as read action. - Dismiss the post action sheet Editing GIF alert cancel action button. - Label for a cancel button - Label for the auto-upload cancelation button in the post list. Tapping will prevent the app from auto-uploading the post. Menus cancel button for deleting a menu. Menus cancel button within text bar while editing items. Menus: Cancel button title for canceling an edited menu item. @@ -1353,10 +1321,6 @@ translators: Block name. %s: The localized block name */ Verb. Title for Jetpack Restore cancel button. */ "Cancel" = "İptal"; -/* Label for the Post List option that cancels automatic uploading of a post. - Media Library option to cancel an in-progress or failed upload. */ -"Cancel Upload" = "Karşıya yüklemeyi iptal et"; - /* Menus alert button title to cancel discarding changes and not createa a new menu. Menus alert button title to cancel discarding changes and not select a new menu Menus alert button title to cancel discarding changes and not select a new menu location */ @@ -1415,9 +1379,6 @@ translators: Block name. %s: The localized block name */ Main title */ "Change Password" = "Parola değiştir"; -/* Title of button that takes user to the system Settings section for the app */ -"Change Settings" = "Ayarları değiştir"; - /* Change Username title. */ "Change Username" = "Kullanıcı adını değiştir"; @@ -1557,9 +1518,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Choose file" = "Dosya seçin"; -/* Menu option for selecting media from the device's photo library. */ -"Choose from My Device" = "Cihazımdan seç"; - /* Explanatory text for Homepage Settings homepage type selection. */ "Choose from a homepage that displays your latest posts (classic blog) or a fixed \/ static page." = "En son gönderilerinizi görüntüleyen bir ana sayfa (klasik blog) veya sabit\/statik bir sayfa seçin."; @@ -1760,9 +1718,6 @@ translators: Block name. %s: The localized block name */ /* Community & Non-Profit site intent topic */ "Community & Non-Profit" = "Topluluk ve Kâr Amacı Gütmeyen Kuruluş"; -/* Accessibility indication that the current Post List style is currently Compact. */ -"Compact" = "Kompakt"; - /* The action is completed */ "Completed" = "Tamamlandı"; @@ -1948,10 +1903,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Copied block" = "Kopyalanan blok"; -/* Copy the post url and paste anywhere in phone - Label for page copy link. Tapping copy the url of page */ -"Copy Link" = "Bağlantıyı kopyala"; - /* Copy link to oomment button title */ "Copy Link to Comment" = "Bağlantıyı yoruma kopyalayın"; @@ -2060,9 +2011,6 @@ translators: Block name. %s: The localized block name */ /* Error title displayed when unable to close user account. */ "Couldn’t close account automatically" = "Hesap otomatik olarak kapatılamadı"; -/* Message to show while media data source is finding the number of items available. */ -"Counting media items..." = "Ortam öğeleri sayılıyor..."; - /* Period Stats 'Countries' header */ "Countries" = "Ülkeler"; @@ -2313,7 +2261,6 @@ translators: Block name. %s: The localized block name */ /* Delete Delete button title for the warning shown to the user when he refuses to re-login when the authToken is missing. Title for button that permanently deletes a media item (photo / video) - Title for button that permanently deletes one or more media items (photos / videos) Title of a delete button Title of the trash confirmation alert. */ "Delete" = "Sil"; @@ -2321,15 +2268,11 @@ translators: Block name. %s: The localized block name */ /* Menus confirmation button for deleting a menu. */ "Delete Menu" = "Menüyü sil"; -/* Delete option in the confirmation alert when deleting a page from the trash. - Delete option in the confirmation alert when deleting a post from the trash. - Label for a button permanently deletes a page. - Label for the delete post option. Tapping permanently deletes a post. +/* Delete option in the confirmation alert when deleting a post from the trash. Title for button on the comment details page that deletes the comment when tapped. */ "Delete Permanently" = "Kalıcı olarak sil"; -/* Title of the confirmation alert when deleting a page from the trash. - Title of the confirmation alert when deleting a post from the trash. */ +/* Title of the confirmation alert when deleting a post from the trash. */ "Delete Permanently?" = "Kalıcı olarak silinsin mi?"; /* Button label for deleting the current site @@ -2448,7 +2391,6 @@ translators: Block name. %s: The localized block name */ /* Accessibility label for button to dismiss a bottom sheet Accessibility label for the transparent space above the login dialog which acts as a button to dismiss the dialog. Accessibility label for the transparent space above the signup dialog which acts as a button to dismiss the dialog. - Action to show on alert when view asset fails. Dismiss a view. Verb */ "Dismiss" = "Kapat"; @@ -2466,12 +2408,6 @@ translators: Block name. %s: The localized block name */ User's Display Name */ "Display Name" = "Görünen isim"; -/* Accessibility label for other media items in the media collection view. The parameter is the creation date of the document. */ -"Document, %@" = "Belge, %@"; - -/* Accessibility label for other media items in the media collection view. The parameter is the filename file. */ -"Document: %@" = "Belge: %@"; - /* Message shown when all tours have been completed */ "Doesn't it feel good to cross things off a list?" = "Listedeki bazı işleri tamamlamak iyi hissettirmiyor mu?"; @@ -2632,8 +2568,7 @@ translators: Block name. %s: The localized block name */ /* Description of a Quick Start Tour */ "Draft and publish a post." = "Bir yazıyı taslak olarak yazın ve yayınlayın."; -/* Title of the drafts filter. This filter shows a list of draft posts. - Title of the drafts header in search list. */ +/* Title of the drafts filter. This filter shows a list of draft posts. */ "Drafts" = "Taslaklar"; /* No comment provided by engineer. */ @@ -2645,10 +2580,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Drag to adjust focal point" = "Odak noktasını ayarlamak için sürükleyin"; -/* Label for page duplicate option. Tapping creates a copy of the page. - Label for post duplicate option. Tapping creates a copy of the post. */ -"Duplicate" = "Çoğalt"; - /* No comment provided by engineer. */ "Duplicate block" = "Bloğu çoğalt"; @@ -2661,13 +2592,9 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Each block has its own settings. To find them, tap on a block. Its settings will appear on the toolbar at the bottom of the screen." = "Her blokun kendi ayarları vardır. Onları bulmak için bir bloka dokunun. Ayarları, ekranın altındaki araç çubuğunda görünür."; -/* Accessibility label for edit button to enable multi selection mode in the user's media library - Edit the post. - Editing GIF alert default action button. +/* Editing GIF alert default action button. Edits a Comment Edits the comment - Label for a button that opens the Edit Page view controller - Label for the edit post button. Tapping displays the editor. User action to edit media details. Verb, edit a comment */ "Edit" = "Düzenle"; @@ -2675,9 +2602,6 @@ translators: Block name. %s: The localized block name */ /* Title for the edit more button section */ "Edit \"More\" button" = "\"Daha fazla\" tuşunu düzenle"; -/* Button that displays the media editor to the user */ -"Edit %@" = "%@ düzenle"; - /* Blocklist Keyword Edition Title */ "Edit Blocklist Word" = "Kara Liste Sözcüğünü Düzenle"; @@ -2870,9 +2794,6 @@ translators: Block name. %s: The localized block name */ /* Secondary message shown when there are no domains that match the user entered text. */ "Enter different words above and we'll look for an address that matches it." = "Yukarıya farklı sözcükler girin, girdiğiniz sözcüklerle eşleşen adres olup olmadığını arayalım."; -/* Accessibility hint for edit button to enable multi selection mode in the user's media library */ -"Enter edit mode to enable multi select to delete" = "Silmek için çoklu seçimi etkinleştirmek için düzenleme moduna girin"; - /* (placeholder) Help enter WordPress password Placeholder of a field to type a password to protect the post. */ "Enter password" = "Parola girin"; @@ -3028,9 +2949,6 @@ translators: Block name. %s: The localized block name */ /* Short title telling the user they will receive a blogging reminder every day of the week. */ "Every day at %@" = "Her gün saat %@"; -/* Label for the post author filter. This filter shows posts for all users on the blog. */ -"Everyone" = "Herkes"; - /* Example story title description */ "Example story title" = "Öykü başlığı örneği"; @@ -3040,9 +2958,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Excerpt length (words)" = "Özet uzunluğu (kelime)"; -/* Accessibility label for a post's excerpt in the post list. The parameter is the post excerpt. For example, \"Excerpt. This is the first paragraph.\" */ -"Excerpt. %@." = "Alıntı. %@."; - /* Should be the same as the text displayed if the user clicks the (i) in Calypso. */ "Excerpts are optional hand-crafted summaries of your content." = "Özetler, içeriğinizin elle hazırlanmış özetidir."; @@ -3052,8 +2967,7 @@ translators: Block name. %s: The localized block name */ /* Accessibility Label for the exit full screen button on the full screen comment reply mode */ "Exit Full Screen" = "Tam ekran modundan çık"; -/* Accessibility indication that the current Post List style is currently Expanded. - Screen reader text to represent the expanded state of a UI control */ +/* Screen reader text to represent the expanded state of a UI control */ "Expanded" = "Genişletilmiş"; /* Accessibility hint */ @@ -3103,9 +3017,6 @@ translators: Block name. %s: The localized block name */ The action failed */ "Failed" = "Başarısız"; -/* Error title when picked media cannot be imported into stories. */ -"Failed Media Export" = "Medya Dışa Aktarılamadı"; - /* Message for mark all as read success notice */ "Failed marking Notifications as read" = "Bildirim okundu olarak işaretlenirken hata oluştu"; @@ -3307,6 +3218,12 @@ translators: Block name. %s: The localized block name */ /* An example tag used in the login prologue screens. */ "Football" = "Futbol"; +/* translators: Recommendation included in a warning related to having blocks deeply nested. */ +"For this reason, we recommend editing the block using the web editor." = "Bu nedenle, bloku web düzenleyicinizi kullanarak düzenlemenizi öneririz."; + +/* translators: Recommendation included in a warning related to having blocks deeply nested. */ +"For this reason, we recommend editing the block using your web browser." = "Bu nedenle, bloku web tarayıcınızı kullanarak düzenlemenizi öneririz."; + /* Register Domain - Domain contact information section header description */ "For your convenience, we have pre-filled your WordPress.com contact information. Please review to be sure it’s the correct information you want to use for this domain." = "Size kolaylık olması için, WordPress.com iletişim bilgilerinizi önceden doldurduk. Lütfen bu bilgilerin bu alan adında kullanmak istediğiniz doğru bilgiler olduğundan emin olmak için gözden geçirin."; @@ -3624,8 +3541,7 @@ translators: Block name. %s: The localized block name */ "Home" = "Ana Sayfa"; /* Title for setting which shows the current page assigned as a site's homepage - Title for the homepage section in site settings screen - Title of the Homepage Badge */ + Title for the homepage section in site settings screen */ "Homepage" = "Ana sayfa"; /* Label for Homepage Settings site settings section @@ -3722,9 +3638,6 @@ translators: Block name. %s: The localized block name */ /* Hint for image title on image settings. */ "Image title" = "Görsel başlığı"; -/* Accessibility label for image thumbnails in the media collection view. The parameter is the creation date of the image. */ -"Image, %@" = "Görsel, %@"; - /* Undated post time label */ "Immediately" = "Hemen"; @@ -4210,9 +4123,6 @@ translators: Block name. %s: The localized block name */ Settings: Comments Approval settings */ "Links in comments" = "Yorumlardaki bağlantılar"; -/* The accessibility label for the list style button in the Post List. */ -"List style" = "Liste stili"; - /* Title of the screen that load selected the revisions. */ "Load" = "Yükle"; @@ -4228,18 +4138,12 @@ translators: Block name. %s: The localized block name */ /* Text displayed while loading the activity feed for a site */ "Loading Backups..." = "Yedeklemeler yükleniyor..."; -/* Phrase to show when the user has searched for GIFs and they are being loaded. */ -"Loading GIFs..." = "GIFler yükleniyor..."; - /* Menus label text displayed while menus are loading */ "Loading Menus..." = "Menüler yükleniyor..."; /* Text displayed while loading site People. */ "Loading People..." = "Kişiler yükleniyor..."; -/* Phrase to show when the user has searched for images and they are being loaded. */ -"Loading Photos..." = "Fotoğraflar yükleniyor..."; - /* Text displayed while loading plans details */ "Loading Plan..." = "Plan yükleniyor..."; @@ -4300,8 +4204,7 @@ translators: Block name. %s: The localized block name */ /* Local Services site intent topic */ "Local Services" = "Yerel Hizmetler"; -/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. - Title of the Local Changes Badge */ +/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. */ "Local changes" = "Yerel değişiklikler"; /* Title for alert when a generic error happened when trying to find the location of the device */ @@ -4465,7 +4368,6 @@ translators: Block name. %s: The localized block name */ "Max Video Upload Size" = "En büyük dosya yükleme boyutu"; /* Accessibility label for the Me button in My Site. - Label for the post author filter. This filter shows posts only authored by the current user. Me page title Noun. Title. Links to the Me screen. The accessibility value of the me tab. @@ -4473,9 +4375,7 @@ translators: Block name. %s: The localized block name */ "Me" = "Ben"; /* Noun. Title. Links to the blog's Media library. - Tab bar title for the Media tab in Media Picker The menu item to select during a guided tour. - Title for Media Library section of the app. Title for the media section in site settings screen Title label for the media settings section in the app settings */ "Media" = "Ortam"; @@ -4487,13 +4387,6 @@ translators: Block name. %s: The localized block name */ /* Label for size of media cache in the app. */ "Media Cache Size" = "Ortam ön belleği boyutu"; -/* Title for alert when access to media capture is not granted */ -"Media Capture" = "Görsel yakalama"; - -/* Title for alert when a generic error happened when loading media - Title for alert when access to the media library is not granted by the user */ -"Media Library" = "Ortam kütüphanesi"; - /* Title for action sheet with media options. */ "Media Options" = "Ortam seçenekleri"; @@ -4516,9 +4409,6 @@ translators: Block name. %s: The localized block name */ /* translators: %s: block title e.g: \"Paragraph\". */ "Media options" = "Medya seçenekleri"; -/* Alert title when there is issues loading an asset to preview. */ -"Media preview failed." = "Ortam ön izleme hatası."; - /* Alert displayed to the user when multiple media items have uploaded successfully. */ "Media uploaded (%ld files)" = "Ortam yüklendi (%ld dosya)"; @@ -4556,9 +4446,6 @@ translators: Block name. %s: The localized block name */ Label for the share message field on the post settings. */ "Message" = "Mesaj"; -/* Title of section containing image / video metadata such as size and file type */ -"Metadata" = "Metadata"; - /* Option to select the Microsft Outlook app when logging in with magic links */ "Microsoft Outlook" = "Microsoft Outlook"; @@ -4578,13 +4465,11 @@ translators: Block name. %s: The localized block name */ "Months and Years" = "Aylar ve yıllar"; /* Accessibility label for more button in dashboard quick start card. - Accessibility label for the More button in Page List. Accessibility label for the More button in Post List (compact view). Accessibility label for the More button on formatting toolbar. Accessibility label for the More button on Reader Cell Accessibility label for the More button on Reader's post details - Action button to display more available options - Label for the more post button. Tapping displays an action sheet with post options. */ + Action button to display more available options */ "More" = "Daha fazla"; /* Action button to display more available options @@ -4642,15 +4527,8 @@ translators: Block name. %s: The localized block name */ /* Screen reader text for button that will move the menu item */ "Move menu item" = "Menü öğesini taşı"; -/* Label for a button that moves a page to the draft folder - Label for an option that moves a post to the draft folder */ -"Move to Draft" = "Taslaklara taşı"; - -/* Label for a button that moves a page to the trash folder - Label for a option that moves a post to the trash folder - Title for button on the comment details page that moves the comment to trash when tapped. +/* Title for button on the comment details page that moves the comment to trash when tapped. Trash option in the trash confirmation alert. - Trash option in the trash page confirmation alert. Trashes the comment */ "Move to Trash" = "Çöpe taşı"; @@ -4682,7 +4560,8 @@ translators: Block name. %s: The localized block name */ Title of My Site tab */ "My Site" = "Benim sitem"; -/* Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ +/* Title for site picker screen. + Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ "My Sites" = "Sitelerim"; /* Siri Suggestion to open My Sites */ @@ -4932,9 +4811,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title for the view when there aren't any Activities to display in the Activity Log for a given filter. */ "No matching events found." = "Eşleşen etkinlik bulunamadı."; -/* Message displayed when no results are returned from a media library search. Should match Calypso. - Phrase to show when the user search for images but there are no result to show. - Phrase to show when the user searches for GIFs but there are no result to show. */ +/* Message displayed when no results are returned from a media library search. Should match Calypso. */ "No media matching your search" = "Aramanızla eşleşen ortam yok"; /* Menus text shown when no menus were available for loading the Menus editor. */ @@ -4952,8 +4829,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Displayed in the Notifications Tab as a title, when there are no notifications */ "No notifications yet" = "Henüz bildirim yok"; -/* Displayed when the user is searching the pages list and there are no matching pages - Text displayed when there's no matching with the text search */ +/* Text displayed when there's no matching with the text search */ "No pages matching your search" = "Aramanızla eşleşen sayfa yok"; /* Text displayed when search for plugins returns no results */ @@ -4974,9 +4850,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Message shown whent the reader finds no posts for the chosen tag */ "No posts have been made recently with this tag." = "Bu etiket ile yakın zamanda yazı yazılmadı."; -/* Displayed when the user is searching the posts list and there are no matching posts */ -"No posts matching your search" = "Aramanızla eşleşen yazı yok"; - /* Accessibility value for a Stats' Posting Activity Month if there are no posts. */ "No posts." = "Yazı yok."; @@ -5077,9 +4950,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* A message title */ "Nothing liked yet" = "Henüz hiçbir şey beğenilmedi"; -/* Default message for empty media picker */ -"Nothing to show" = "Gösterilecek bir şey yok"; - /* Notifications Details Accessibility Identifier */ "Notification Details Table" = "Bildirim detayları tablosu"; @@ -5139,7 +5009,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Alert dismissal title Button title. Acknowledges a prompt. Button title. An acknowledgement of the message displayed in a prompt. - Confirmation of action Default action Dismisses the alert Marks all notifications as read. @@ -5201,9 +5070,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* No comment provided by engineer. */ "Only show excerpt" = "Sadece özeti göster"; -/* Message telling the user that they've only enabled limited photo library permissions for the app. */ -"Only the selected photos you've given access to are available." = "Yalnızca erişim izni verdiğiniz seçili fotoğraflar kullanılabilir."; - /* An informal exclaimation that means `something went wrong`. Title for the view when there's an error loading a comment. Title for the view when there's an error loading Activity Log @@ -5238,9 +5104,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title of a button that opens the apps settings in the system Settings.app */ "Open Settings" = "Ayarları aç"; -/* Editor button to swich the media picker from quick mode to full picker */ -"Open full media picker" = "Tam ortam seçicisini aç"; - /* No comment provided by engineer. */ "Open in Safari" = "Safari ile aç"; @@ -5280,6 +5143,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Divider on initial auth view separating auth options. */ "Or" = "Veya"; +/* Instruction text for other forms of two-factor auth methods. */ +"Or choose another form of authentication." = "Farklı bir doğrulama biçimi de seçebilirsiniz."; + /* Label for button to log in using site address. Underscores _..._ denote underline. */ "Or log in by _entering your site address_." = "Veya _site adresinizi girerek_ oturum açın."; @@ -5338,15 +5204,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title shown when selecting a post type of Page from the Share Extension. */ "Page" = "Sayfa"; -/* Prompts the user that a restored page was moved to the drafts list. */ -"Page Restored to Drafts" = "Sayfa taslaklara kaydedildi"; - -/* Prompts the user that a restored page was moved to the published list. */ -"Page Restored to Published" = "Sayfa yayımlananlara kaydedildi"; - -/* Prompts the user that a restored page was moved to the scheduled list. */ -"Page Restored to Scheduled" = "Sayfa takvime eklendi"; - /* Name of the button to open the page settings The title of the Page Settings screen. */ "Page Settings" = "Sayfa Ayarları"; @@ -5363,9 +5220,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of notification displayed when a page has failed to upload. */ "Page failed to upload" = "Sayfa karşıya yüklenemedi"; -/* A short message explaining that a page was moved to the trash bin. */ -"Page moved to trash." = "Sayfa çöpe taşındı."; - /* Title of notification displayed when a page has been successfully saved as a draft. */ "Page pending review" = "Sayfa incelenmeyi bekliyor"; @@ -5437,8 +5291,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title of pending Comments filter. */ "Pending" = "Bekleyen"; -/* Name for the status of a post pending review. - Title of the Pending Review Badge */ +/* Name for the status of a post pending review. */ "Pending review" = "Bekleyen eleştiri"; /* Noun. Title of the people management feature. @@ -5467,12 +5320,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Photography site intent topic */ "Photography" = "Fotoğrafçılık"; -/* Tab bar title for the Photos tab in Media Picker */ -"Photos" = "Fotoğraflar"; - -/* Subtitle for placeholder in Free Photos. The company name 'Pexels' should always be written as it is. */ -"Photos provided by Pexels" = "Fotoğraflar Pexels tarafından sağlandı"; - /* Title for selecting a new username in the site creation flow. */ "Pick username" = "Kullanıcı adı seçin"; @@ -5565,7 +5412,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Please enter the password for your WordPress.com account to log in with your Apple ID." = "Lütfen Apple ID’nizle oturum açmak için WordPress.com hesabınızın parolasını girin."; /* Instruction text on the two-factor screen. */ -"Please enter the verification code from your authenticator app, or tap the link below to receive a code via SMS." = "Lütfen kimlik doğrulayıcı uygulamanızdaki doğrulama kodunu girin veya SMS ile bir kod almak için aşağıdaki bağlantıya dokunun."; +"Please enter the verification code from your authenticator app." = "Lütfen kimlik doğrulayıcı uygulamanızdaki doğrulama kodunu girin."; /* Popup message to ask for user credentials (fields shown below). */ "Please enter your credentials" = "Lütfen giriş bilgilerinizi girin"; @@ -5660,15 +5507,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ The post formats available for the post. Should be the same as in core WP. */ "Post Format" = "Yazı biçimi"; -/* Prompts the user that a restored post was moved to the drafts list. */ -"Post Restored to Drafts" = "Yazı taslaklara geri yüklendi"; - -/* Prompts the user that a restored post was moved to the published list. */ -"Post Restored to Published" = "Yazı yayımlanmak üzere geri yüklendi"; - -/* Prompts the user that a restored post was moved to the scheduled list. */ -"Post Restored to Scheduled" = "Yazı takvime eklenmek üzere geri yüklendi"; - /* Name of the button to open the post settings The title of the Post Settings screen. */ "Post Settings" = "Yazı ayarları"; @@ -5688,9 +5526,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of notification displayed when a post has failed to upload. */ "Post failed to upload" = "Yazı karşıya yüklenemedi"; -/* A short message explaining that a post was moved to the trash bin. */ -"Post moved to trash." = "Yazı çöpe taşındı."; - /* Title of notification displayed when a post has been successfully saved as a draft. */ "Post pending review" = "Yazı incelenmeyi bekliyor"; @@ -5749,9 +5584,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Period Stats 'Posts and Pages' header */ "Posts and Pages" = "Yazılar ve sayfalar"; -/* Title of the Posts Page Badge */ -"Posts page" = "Yazılar sayfası"; - /* Message informing the user that their static homepage for posts was set successfully */ "Posts page successfully updated" = "Yazılar sayfası başarıyla güncellendi"; @@ -5764,9 +5596,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* A message explaining the Posts I Like feature in the reader */ "Posts that you like will appear here." = "Beğendiğiniz yazılar burada görüntülenecek."; -/* Subtitle for placeholder in Tenor picker. `The company name 'Tenor' should always be written as it is. */ -"Powered by Tenor" = "Destekleyen Tenor"; - /* Browse premium themes selection title */ "Premium" = "Premium"; @@ -5785,18 +5614,12 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title for screen to preview a static content. */ "Preview" = "Önizleme"; -/* Action for Media Picker to preview the selected media items. The argument in the string represents the number of elements (as numeric digits) selected */ -"Preview %@" = "%@ Önizlemesi"; - /* Title for web preview device switching button */ "Preview Device" = "Ön izleme cihazı"; /* Title on display preview error */ "Preview Unavailable" = "Önizleme kullanılamıyor"; -/* Accessibility label for media item preview for user's viewing an item in their media library */ -"Preview media" = "Ortam dosyasını önizleme"; - /* No comment provided by engineer. */ "Preview page" = "Sayfa önizleme"; @@ -5843,8 +5666,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Privacy notice for California users" = "Californiya kullanıcıları için gizlilik notu"; /* Name for the status of a post that is marked private. - Privacy setting for posts set to 'Private'. Should be the same as in core WP. - Title of the Private Badge */ + Privacy setting for posts set to 'Private'. Should be the same as in core WP. */ "Private" = "Özel"; /* No comment provided by engineer. */ @@ -5894,12 +5716,10 @@ translators: %s: Select control button label e.g. \"Button width\" */ Label for the publish date button. */ "Publish Date" = "Yayımlama tarihi"; -/* A short phrase indicating a post is due to be immedately published. - Label for a button that moves a page to the published folder, publishing with the current date/time. */ +/* A short phrase indicating a post is due to be immedately published. */ "Publish Immediately" = "Hemen yayımla"; /* Label for a button that publishes the post - Label for an option that moves a publishes a post immediately Title of button allowing the user to immediately publish the post they are editing. */ "Publish Now" = "Şimdi yayımla"; @@ -5917,8 +5737,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Name for the status of a published post. Period Stats 'Published' header - Title of the published filter. This filter shows a list of posts that the user has published. - Title of the published header in search list. */ + Title of the published filter. This filter shows a list of posts that the user has published. */ "Published" = "Yayımlandı"; /* Precedes the name of the blog just posted on */ @@ -6060,8 +5879,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of the completion screen of the Blogging Reminders Settings screen when the reminders are removed. */ "Reminders removed" = "Hatırlatıcılar kaldırıldı"; -/* Add asset to media picker list - Alert button to confirm a plugin to be removed +/* Alert button to confirm a plugin to be removed Button label when removing a blog Label for the Remove Feature Image icon. Tapping will show a confirmation screen for removing the feature image from the post. Remove Action @@ -6214,9 +6032,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of secondary button on alert prompting verify their accounts while attempting to publish */ "Resend" = "Tekrar gönder"; -/* Title of the reset button */ -"Reset" = "Sıfırla"; - /* Accessibility label for the reset activity type button */ "Reset Activity Type filter" = "Etkinlik türü filtresini sıfırla"; @@ -6271,12 +6086,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ Button title, displayed when media has failed to upload. Allows the user to try the upload again. Button title. Retries uploading a post. If a user taps the button with this label, the action that evinced this error view will be retried. - Label for a button that attempts to re-upload a page that previously failed to upload. - Label for the retry post upload button. Tapping attempts to upload the post again. Opens the media library . Retries the upload of a user's post. Retry updating User's Role - Retry uploading the post. Retry. Action Retry. Verb – retry a failed media upload. The Jetpack view button title used when an error occurred @@ -6288,9 +6100,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Button title that triggers a scan */ "Retry Scan" = "Taramayı yeniden dene"; -/* User action to retry media upload. */ -"Retry Upload" = "Yüklemeyi yeniden dene"; - /* User action to retry all failed media uploads. */ "Retry all" = "Hepsini tekrar dene"; @@ -6388,9 +6197,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Accessibility label for the 'Save Post' button when a post has been saved. */ "Saved Post" = "Kaydedilen yazı"; -/* Text displayed in HUD when a media item's metadata (title, etc) is saved successfully. */ -"Saved!" = "Kaydedildi!"; - /* Accessibility hint for the 'Save Post' button. */ "Saves this post for later." = "Bu yazıyı sonrası için kaydeder."; @@ -6401,7 +6207,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Saving post…" = "Yazı kaydediliyor..."; /* Menus save button title while it is saving a Menu. - Text displayed in HUD while a media item's metadata (title, etc) is being saved. Text displayed in HUD while a post is being saved as a draft. */ "Saving..." = "Kaydediliyor..."; @@ -6492,21 +6297,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* No comment provided by engineer. */ "Search or type URL" = "URL ara veya yaz"; -/* Text displayed when the search controller will be presented */ -"Search pages" = "Sayfalarda arama yap"; - -/* Text displayed when the search controller will be presented */ -"Search posts" = "Yazıları ara"; - /* No comment provided by engineer. */ "Search settings" = "Arama ayarları"; -/* Title for placeholder in Tenor picker */ -"Search to find GIFs to add to your Media Library!" = "Ortam kütüphanenize eklemek üzere GIF bulmak için ara!"; - -/* Title for placeholder in Free Photos */ -"Search to find free photos to add to your Media Library!" = "Ortam kütüphanenize eklemek üzere ücretsiz fotoğraf bulmak için ara!"; - /* Menus search bar placeholder text. */ "Search..." = "Ara..."; @@ -6577,9 +6370,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Register Domain - Domain contact information field placeholder for Country */ "Select Country" = "Ülke seçin"; -/* Title of button that allows the user to select more photos to access within the app */ -"Select More" = "Daha fazla seç"; - /* Blog Picker's Title */ "Select Site" = "Site seçin"; @@ -6601,9 +6391,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Register domain - Title for the Choose domain button of Suggested domains screen */ "Select domain" = "Alan adı seç"; -/* Accessibility hint for actions when displaying media items. */ -"Select media." = "Ortam dosyası seçin."; - /* Accessibility label for selecting paragraph style button on formatting toolbar. */ "Select paragraph style" = "Paragraf stilini seçin"; @@ -6707,19 +6494,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label for connected service in Publicize stat. */ "Service" = "Hizmet"; -/* Label for a button that opens the Set Parent options view controller - Navigation title displayed on the navigation bar */ +/* Navigation title displayed on the navigation bar */ "Set Parent" = "Ana sayfayı belirleyin"; /* No comment provided by engineer. */ "Set as Featured Image" = "Öne çıkan görsel olarak ayarla"; -/* Label for a button that sets the selected page as the site's Homepage */ -"Set as Homepage" = "Ana sayfa olarak ayarla"; - -/* Label for a button that sets the selected page as the site's Posts page */ -"Set as Posts Page" = "Yazılar sayfası olarak belirle"; - /* Notice confirming that an image has been set as the post's featured image. */ "Set as featured image" = "Öne çıkan görsel olarak ayarla"; @@ -6763,7 +6543,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Accessibility label for share buttons in nav bars Button label to share a post Button label to share a web page - Share the post. Shares the comment URL Spoken accessibility label Title for a button that allows the user to share their answer to the prompt. @@ -7149,8 +6928,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Name of setting configured when a site uses a static page as its homepage */ "Static Homepage" = "Sabit ana sayfa"; -/* Label for post stats option. Tapping displays statistics for a post. - Noun. Abbreviation of Statistics. Name of the Stats feature +/* Noun. Abbreviation of Statistics. Name of the Stats feature Noun. Abbv. of Statistics. Links to a blog's Stats screen. Stats 3D Touch Shortcut Stats window title @@ -7181,9 +6959,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label text that defines a post marked as sticky */ "Sticky" = "Yapışkan"; -/* Accessibility label for a sticky post in the post list. */ -"Sticky." = "Sabit."; - /* User action to stop upload. */ "Stop upload" = "Yüklemeyi durdur"; @@ -7240,7 +7015,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ Theme Support action title */ "Support" = "Destek"; -/* Button used to switch site */ +/* Title for back button that leads to the site picker screen. */ "Switch Site" = "Site değiştir"; /* Switches the Editor to HTML Mode */ @@ -7328,9 +7103,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label explaining why users might want to add tags. */ "Tags help tell readers what a post is about. Separate different tags with commas." = "Etiketler okuyuculara bir gönderinin ne hakkında olduğunu anlatmaya yardımcı olur. Farklı etiketleri virgüller ile ayırın."; -/* Menu option for taking an image or video with the device's camera. */ -"Take Photo or Video" = "Fotoğraf veya video çek"; - /* No comment provided by engineer. */ "Take a Photo" = "Fotoğraf çek"; @@ -7401,12 +7173,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Accessibility hint */ "Tap to select the previous period" = "Bir önceki dönemi seçmek için seçe dokunun"; -/* Accessibility hint for button used to switch site */ -"Tap to switch to another site, or add a new site" = "Başka bir siteye geçmek için dokunun veya yeni bir site ekleyin"; - -/* Accessibility hint for media item preview for user's viewing an item in their media library */ -"Tap to view media in full screen" = "Ortam dosyasını tam ekran görmek için dokunun"; - /* Accessibility hint for a button that opens a new view with more details. */ "Tap to view more details." = "Daha fazla ayrıntı görüntülemek için dokunun."; @@ -7452,10 +7218,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* No comment provided by engineer. */ "Text formatting controls are located within the toolbar positioned above the keyboard while editing a text block" = "Bir metin bloku düzenlenirken, metin biçimlendirme kontrolleri klavyenin üzerindeki araç çubuğunda bulunur"; -/* Button title - The button's title text to send a 2FA code via SMS text message. */ +/* Button title */ "Text me a code instead" = "Bunun yerine bana kodu mesaj olarak gönderin"; +/* The button's title text to send a 2FA code via SMS text message. */ +"Text me a code via SMS" = "Bana SMS ile kod gönder"; + /* Message of alert when theme activation succeeds */ "Thanks for choosing %@ by %@" = "%2$@ tarafından hazırlanan %1$@ temasını seçtiğiniz için teşekkürler"; @@ -7483,9 +7251,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Error message shown to a user who is trying to share to Facebook but does not have any available Facebook Pages. */ "The Facebook connection cannot find any Pages. Publicize cannot connect to Facebook Profiles, only published Pages." = "Facebook bağlantısı hiçbir sayfa bulamıyor. Duyuru, Facebook profillerine bağlanamaz, yalnızca yayınlanan sayfalara bağlanabilir."; -/* Message shown when a GIF failed to load while trying to add it to the Media library. */ -"The GIF could not be added to the Media Library." = "GIF, ortam kütüphanesine eklenemez."; - /* Description shown when a user logs in with Google but no matching WordPress.com account is found */ "The Google account \"%@\" doesn't match any account on WordPress.com" = " Google hesabı \"%@\" WordPress.com'daki herhangi bir hesapla eşleşmiyor"; @@ -7613,7 +7378,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Error message shown when user attempts to remove the site owner. */ "The user you are trying to remove is the owner of this site. Please contact support for assistance." = "Silmeye çalıştığınız kullanıcı bı sitenin yöneticisi. Lütfen yardım almak için destek ile iletişime geçin."; -/* Error message informing a user about an invalid password. */ +/* No comment provided by engineer. */ "The username or password stored in the app may be out of date. Please re-enter your password in the settings and try again." = "Uygulama içinde saklanan kullanıcı adı ve parolanın zamanı geçmiş olabilir. Lütfen parolanızı tekrar girerek yeniden deneyiniz."; /* Message shown when a video failed to load while trying to add it to the Media library. */ @@ -7681,9 +7446,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* A short error message letting the user know about a problem displaying a post. */ "There was a problem displaying this post." = "Bu yazıyı görüntülerken bir problem oluştu."; -/* Error message displayed when the Media Library is unable to load a full sized preview of an item. */ -"There was a problem loading the media item." = "Ortam ögesi yüklenirken bir sorun oluştu."; - /* The loading view subtitle displayed when an error occurred */ "There was a problem loading your data, refresh your page to try again." = "Verileriniz yüklenirken bir sorun oluştu, sayfanızı yenileyip tekrar deneyin."; @@ -7696,9 +7458,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Explaining to the user there was an error trying to obtain the current location of the user. */ "There was a problem when trying to access your location. Please try again later." = "Konumunuza ulaşırken bir problem oluştu. Lütfen daha sonra tekrar deneyin."; -/* Explaining to the user there was an generic error accesing media. */ -"There was a problem when trying to access your media. Please try again later." = "Ortam dosyasına erişmeye çalışılırken bir sorun oluştu. Lütfen daha sonra tekrar deneyin."; - /* Message for stories unknown error. */ "There was a problem with the Stories editor. If the problem persists you can contact us via the Me > Help & Support screen." = "Öyküler düzenleyicisinde bir sorun oluştu. Sorun tekrar ederse lütfen Ben > Yardım ve Destek ekranından bizimle iletişim kurun."; @@ -7769,9 +7528,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* A description informing the user in order to proceed with this feature we will need camera permissions, and how to enable it. */ "This app needs permission to access the Camera to scan login codes, tap on the Open Settings button to enable it." = "Bu uygulamanın giriş kodlarını taramak için Kameraya erişmek için izne ihtiyacı vardır, etkinleştirmek için Ayarları Aç düğmesine dokunun."; -/* Explaining to the user why the app needs access to the device media library. */ -"This app needs permission to access your device media library in order to add photos and\/or video to your posts. Please change the privacy settings if you wish to allow this." = "Bu uygulama yazılarınıza fotoğraf ve\/veya video eklemek için cihazınızın görsel kitaplığa erişim izni istiyor. Buna izin vermek istiyorsanız lütfen gizlilik ayarlarını değiştirin."; - /* No comment provided by engineer. */ "This color combination may be hard for people to read. Try using a brighter background color and\/or a darker text color." = "Bu renk birleşimi insanların okuması zor olabilir. Daha parlak bir arka plan ve\/veya daha koyu bir metin rengi kullanmayı deneyin."; @@ -7881,6 +7637,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Description of a Quick Start Tour */ "Time to finish setting up your site! Our checklist walks you through the next steps." = "Site kurulumunuzu tamamlamanın zamanı geldi! Listemiz bir sonraki adımları size açıklar."; +/* Error when the uses takes more than 1 minute to submit a security key. */ +"Time's up, but don't worry, your security is our priority. Please try again!" = "Süre doldu, ancak endişelenmenize gerek yok. Güvenliğiniz önceliğimizdir. Lütfen yeniden deneyin!"; + /* WordPress.com Marketing Footer Text */ "Tips for getting the most out of WordPress.com." = "WordPress.com#&8217;dan en fazlasını almak için ipuçları."; @@ -8004,24 +7763,20 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title for the traffic section in site settings screen */ "Traffic" = "Trafik"; +/* Describes a domain that was transferred from elsewhere to wordpress.com */ +"Transferred Domain" = "Aktarılan Alan Adı"; + /* translators: %s: block title e.g: \"Paragraph\". */ "Transform %s to" = "%s öğesini şuna dönüştür:"; /* No comment provided by engineer. */ "Transform block…" = "Bloku dönüştür…"; -/* Accessibility label for trash button to delete items from the user's media library - Accessibility label for trash buttons in nav bars +/* Accessibility label for trash buttons in nav bars Trashes a comment Trashes the comment */ "Trash" = "Çöp kutusu"; -/* Accessibility hint for trash button to delete items from the user's media library */ -"Trash selected media" = "Seçilen ortamı çöpe at"; - -/* Title of the trash page confirmation alert. */ -"Trash this page?" = "Bu sayfa çöp kutusuna gönderilsin mi?"; - /* Title of the trash confirmation alert. */ "Trash this post?" = "Bu yazı silinsin mi?"; @@ -8139,9 +7894,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Shown when a user logs in with Google but it subsequently fails to work as login to WordPress.com */ "Unable To Connect" = "Bağlanılamıyor"; -/* An error message. */ -"Unable to Connect" = "Bağlanamıyor"; - /* Title for stories unknown error. */ "Unable to Create Stories Editor" = "Öyküler Düzenleyicisi Oluşturulamıyor"; @@ -8157,9 +7909,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* An error message shown when there is an issue creating new invite links. */ "Unable to create new invite links." = "Yeni davet bağlantıları oluşturulamıyor."; -/* Text displayed in HUD if there was an error attempting to delete a group of media items. */ -"Unable to delete all media items." = "Tüm ortam dosyaları silinemiyor."; - /* Text displayed in HUD if there was an error attempting to delete a media item. */ "Unable to delete media item." = "Ortam dosyası silinemiyor."; @@ -8223,12 +7972,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Message displayed when sharing a link to the downloadable backup fails. */ "Unable to share link" = "Bağlantı paylaşılamıyor"; -/* Message that appears when a user tries to trash a page while their device is offline. */ -"Unable to trash pages while offline. Please try again later." = "Çevrimdışı iken sayfalar çöpe atılamaz. Lütfen daha sonra tekrar deneyin."; - -/* Message that appears when a user tries to trash a post while their device is offline. */ -"Unable to trash posts while offline. Please try again later." = "Çevrimdışıyken yazılar çöpe atılamıyor. Lütfen daha sonra tekrar deneyin."; - /* Notice title when turning site notifications off fails. */ "Unable to turn off site notifications" = "Site bildirimleri kapatılamıyor"; @@ -8301,8 +8044,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ Button title. Reverts the previous notification operation Revert an operation Revert enabling notification after successfully subcribing to the comments for the post. - The title of an 'undo' button. Tapping the button moves a trashed page out of the trash folder. - The title of an 'undo' button. Tapping the button moves a trashed post out of the trash folder. Undo action */ "Undo" = "Geri al"; @@ -8345,9 +8086,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title for Unknown HTML Editor */ "Unknown HTML" = "Bilinmeyen HTML"; -/* Label to use when creation date from media asset is not know. */ -"Unknown creation date" = "Bilinmeyen oluşturma tarihi"; - /* No comment provided by engineer. */ "Unknown error" = "Bilinmeyen hata"; @@ -8513,6 +8251,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of a row displayed on the debug screen used to configure the sandbox store use in the App. */ "Use Sandbox Store" = "Sandbox Store'u kullanın"; +/* The button's title text to use a security key. */ +"Use a security key" = "Bir güvenlik anahtarı kullanın"; + /* Option to enable the block editor for new posts */ "Use block editor" = "Blok düzenleyici kullan"; @@ -8588,15 +8329,10 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of an alert informing users that the video they are trying to select is not allowed. */ "Video not uploaded" = "Video yüklenmedi"; -/* Accessibility label for video thumbnails in the media collection view. The parameter is the creation date of the video. */ -"Video, %@" = "Video, %@"; - /* Period Stats 'Videos' header */ "Videos" = "Videolar"; /* Button title. Displays a summary / sharing screen for a specific post. - Label for a button that opens the page when tapped. - Label for the view post button. Tapping displays the post as it appears on the web. Opens the post epilogue screen to allow sharing / viewing of a post. Theme View action title Verb. The screen title shown when viewing a post inside the app. */ @@ -8711,6 +8447,10 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Message shown on screen while waiting for Google to finish its signup process. */ "Waiting for Google to complete…" = "Tamamlanması için Google bekleniyor…"; +/* Text while the webauthn signature is being verified + Text while waiting for a security key challenge */ +"Waiting for security key" = "Güvenlik anahtarı bekleniyor"; + /* View title during the Google auth process. */ "Waiting..." = "Bekliyor..."; @@ -8722,6 +8462,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ Title for Jetpack Restore Warning screen */ "Warning" = "Uyarı"; +/* No comment provided by engineer. */ +"Warning message" = "Uyarı mesajı"; + /* Caption displayed in promotional screens shown during the login flow. */ "Watch your audience grow with in-depth analytics." = "Kapsamlı analizlerle kitlenizin nasıl büyüdüğünü izleyin."; @@ -9082,6 +8825,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* An error message shown when a wpcom user provides the wrong password. */ "Whoops, something went wrong and we couldn't log you in. Please try again!" = "Amanın, bir şeyler yanlış gitti ve giriş yapmanızı sağlayamadık. Lütfen tekrar deneyin!"; +/* Generic error on the 2FA screen */ +"Whoops, something went wrong. Please try again!" = "Hay aksi, bir yanlışlık oldu. Lütfen yeniden deneyin!"; + +/* Error when the uses chooses an invalid security key on the 2FA screen. */ +"Whoops, that security key does not seem valid. Please try again with another one" = "Hay aksi, görünüşe göre bu güvenlik anahtarı geçerli değil. Lütfen farklı bir anahtarla yeniden deneyin"; + /* Error message shown when an incorrect two factor code is provided. */ "Whoops, that's not a valid two-factor verification code. Double-check your code and try again!" = "I-ıh, bu geçerli bir iki adımlı doğrulama kodu değil. Kodunuzu iki kere kontrol edin ve tekrar deneyin!"; @@ -9109,9 +8858,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Siri Suggestion to open Support */ "WordPress Help" = "WordPress yardımı"; -/* Name for the WordPress Media Library */ -"WordPress Media" = "WordPress ortamı"; - /* Accessibility label for selecting an image or video from the user's WordPress media library on formatting toolbar. */ "WordPress Media Library" = "WordPress ortam kütüphanesi"; @@ -9426,9 +9172,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Share extension error dialog text. */ "Your account does not have permission to upload media to this site. The Site Administrator can change these permissions." = "Hesabınızın bu siteye ortam yükleme izni yok. Site yöneticisi bu izinleri değiştirebilir."; -/* Explaining to the user why the app needs access to the device media library. */ -"Your app is not authorized to access media library due to active restrictions such as parental controls. Please check your parental control settings in this device." = "Ebeveyn kontrolleri gibi etkin kısıtlamalar nedeniyle uygulamanızın ortam kitaplığına erişme yetkisi yok. Lütfen bu cihazdaki ebeveyn kontrolü ayarlarınızı kontrol edin."; - /* Title for the Jetpack Backup Complete message. */ "Your backup is now available for download" = "Yedeklemeniz artık indirilebilir"; @@ -9447,9 +9190,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of the site address section in the Domains Dashboard. */ "Your free WordPress.com address is" = "Ücretsiz WordPress.com adresiniz"; -/* Error message when picked media cannot be imported into stories. */ -"Your media could not be exported. If the problem persists you can contact us via the Me > Help & Support screen." = "Medyanız dışa aktarılamadı. If the problem persists you can contact us via the Me > Yardım ve Destek ekranı."; - /* Details about recently acquired domain on domain credit redemption success screen */ "Your new domain %@ is being set up. It may take up to 30 minutes for your domain to start working." = "Yeni alan adınız %@ ayarlanıyor. Alan adınızın çalışmaya başlaması 30 dakika kadar sürebilir."; @@ -9573,8 +9313,38 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* This is the string we display when prompting the user to review the WordPress app */ "appRatings.wordpress.prompt" = "WordPress hakkında ne düşünüyorsunuz?"; -/* Label displayed on audio media items. */ -"audio" = "ses"; +/* Option to enable the optimization of images when uploading. */ +"appSettings.media.imageOptimizationRow" = "Görselleri İyileştir"; + +/* Indicates an image will use high quality when uploaded. */ +"appSettings.media.imageQuality.high" = "Yüksek"; + +/* Indicates an image will use low quality when uploaded. */ +"appSettings.media.imageQuality.low" = "Düşük"; + +/* Indicates an image will use maximum quality when uploaded. */ +"appSettings.media.imageQuality.maximum" = "Maksimum"; + +/* Indicates an image will use medium quality when uploaded. */ +"appSettings.media.imageQuality.medium" = "Orta"; + +/* The quality of image used when uploading */ +"appSettings.media.imageQuality.title" = "Kalite"; + +/* Title for the image quality settings option. */ +"appSettings.media.imageQualityRow" = "Görsel Kalitesi"; + +/* Message of an alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.message" = "Görsel iyileştirme özelliği, daha hızlı yüklenmeleri için görselleri küçültür.\n\nBu seçenek varsayılan olarak etkindir, ancak dilediğiniz zaman uygulama ayarlarından değiştirebilirsiniz."; + +/* Title of an alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.title" = "Görseller iyileştirilmeye devam edilsin mi?"; + +/* Title of button for turning off image optimization, displayed in the alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.turnOff" = "Hayır, kapat"; + +/* Title of button for leaving on image optimization, displayed in the alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.turnOn" = "Evet, açık kalsın"; /* translators: displays audio file extension. e.g. MP3 audio file */ "audio file" = "ses dosyası"; @@ -9688,7 +9458,44 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "blogHeader.actionCopyURL" = "Adresi Kopyala"; /* Context menu button title */ -"blogHeader.actionOpenInBrowser" = "Tarayıcıda Aç"; +"blogHeader.actionVisitSite" = "Siteyi ziyaret et"; + +/* Title for a button that, when tapped, shows more info about participating in Bloganuary. */ +"bloganuary.dashboard.card.button.learnMore" = "Daha fazla bilgi edinin"; + +/* Short description for the Bloganuary event, shown right below the title. */ +"bloganuary.dashboard.card.description" = "Yeni yılda blog yazma alışkanlığı oluşturmayı hedefleyen topluluk etkinliğimiz Bloganuary'den Ocak ayı boyunca blog istemleri alacaksınız."; + +/* Title for the Bloganuary dashboard card while Bloganuary is running. */ +"bloganuary.dashboard.card.runningTitle" = "Bloganuary burada!"; + +/* Title for the Bloganuary dashboard card. */ +"bloganuary.dashboard.card.title" = "Bloganuary yaklaşıyor!"; + +/* Title of a button that calls the user to enable the Blogging Prompts feature. */ +"bloganuary.learnMore.modal.button.promptsDisabled" = "Blog istemlerini aç"; + +/* Title of a button that will dismiss the Bloganuary modal when tapped. +Note that the word 'go' here should have a closer meaning to 'start' rather than 'move forward'. */ +"bloganuary.learnMore.modal.button.promptsEnabled" = "Başlayalım!"; + +/* The second line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.description.second" = "Yanıtınızı yayımlayın."; + +/* The third line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.description.third" = "İlham almak ve yeni bağlantılar kurmak için diğer blog yazarlarının yanıtlarını okuyun."; + +/* The first line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.descriptions.first" = "Her gün size ilham verecek yeni istemler alın."; + +/* An additional piece of information shown in case the user has the Blogging Prompts feature disabled. */ +"bloganuary.learnMore.modal.footer.addition" = "Bloganuary'ye katılmak için Blog İstemlerini etkinleştirmeniz gerekir."; + +/* An informative excerpt shown in a subtler tone. */ +"bloganuary.learnMore.modal.footer.text" = "Bloganuary Ocak ayı boyunca size konular göndermek için Günlük Blog İstemlerini kullanacak."; + +/* The headline text of the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.headline" = "Bir ay sürecek yazma etkinliğimize katılın"; /* Verb. Dismisses the blogging prompt notification. */ "bloggingPrompt.pushNotification.customActionDescription.dismiss" = "Kapat"; @@ -9717,6 +9524,12 @@ Example: Comment on Example: Reply to Pamela Nguyen */ "comment.header.subText.reply" = "%1$@ için yanıt verin"; +/* Error message informing a user about an invalid password. */ +"common.reEnterPasswordMessage" = "Uygulamada depolanan kullanıcı adı veya parolanın süresi geçmiş olabilir. Lütfen ayarlarda parolanızı tekrar girin ve tekrar deneyin."; + +/* An error message. */ +"common.unableToConnect" = "Bağlanamıyor"; + /* Footnote for the privacy compliance popover. */ "compliance.analytics.popover.footnote" = "Bu tanımlama bilgileri, kullanıcıların web sitelerimizle nasıl etkileşimde bulunduğu hakkında bilgi toplayarak performansı optimize etmemizi sağlar."; @@ -9867,50 +9680,92 @@ Example: Reply to Pamela Nguyen */ /* Title for a menu action in the context menu on the Jetpack install card. */ "domain.dashboard.card.menu.hide" = "Bunu gizle"; -/* Domain Purchase Completion footer */ -"domain.purchase.preview.footer" = "Özel alan adınızın çalışmaya başlaması 30 dakika kadar sürebilir."; +/* Search domain - Title for the Suggested domains screen */ +"domain.management.addDomain.search.title" = "Bir alan adı arayın"; -/* Domain Purchase Completion description (only for FREE domains). */ -"domain.purchase.preview.free.description" = "Ardından, sitenizi göz atılmaya hazır hale getirmenize yardımcı olacağız."; +/* Domain management buy domain card button title */ +"domain.management.buy.card.button.title" = "Alan Adı Al"; -/* Domain Purchase Completion description (only for PAID domains). */ -"domain.purchase.preview.paid.description" = "Makbuzunuzu e-posta ile gönderdik. Ardından, sitenizi herkes için hazırlamanıza yardımcı olacağız."; +/* Domain management buy domain card subtitle */ +"domain.management.buy.card.subtitle" = "Siteyi daha sonra ekleyin."; -/* Reflects that site is live when domain purchase feature flag is ON. */ -"domain.purchase.preview.title" = "Tebrikler, siteniz yayında!"; +/* Domain management buy domain card title */ +"domain.management.buy.card.title" = "Sadece alan adı satın alın"; + +/* The expired label of the domain card in All Domains screen. */ +"domain.management.card.expired.label" = "Süresi Dolmuş"; -/* Status of a domain in `Action Required` state */ -"domain.status.action.required" = "Eylem Gerekli"; +/* The renews label of the domain card in All Domains screen. */ +"domain.management.card.renews.label" = "Yenilemeler"; -/* Status of a domain in `Active` state */ -"domain.status.active" = "Etkin"; +/* The empty state button title in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.button.title" = "Bir alan adı bulun"; -/* Status of a domain in `Complete Setup` state */ -"domain.status.complete.setup" = "Kurulumu Tamamla"; +/* The empty state description in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.description" = "Mükemmel alan adınızı bulmak için aşağıya tıklayın."; -/* Status of a domain in `Error` state */ -"domain.status.error" = "Hata"; +/* The empty state title in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.title" = "Alan adınız yok"; -/* Status of a domain in `Expired` state */ -"domain.status.expired" = "Süresi Dolmuş"; +/* The empty state description in All Domains screen when an error occurs */ +"domain.management.error.empty.state.description" = "Alan adlarınız yüklenirken bir hatayla karşılaştık. Sorun devam ederse lütfen desteğe başvurun."; -/* Status of a domain in `Expiring Soon` state */ -"domain.status.expiring.soon" = "Süresi Yakında Dolacak"; +/* The empty state title in All Domains screen when an error occurs */ +"domain.management.error.empty.state.title" = "Bir sorun oluştu"; -/* Status of a domain in `Failed` state */ -"domain.status.failed" = "Başarısız"; +/* The empty state button title in All Domains screen when an error occurs */ +"domain.management.error.state.button.title" = "Tekrar dene"; -/* Status of a domain in `In Progress` state */ -"domain.status.in.progress" = "Devam Ediyor"; +/* The empty state description in All Domains screen when the user is offline */ +"domain.management.offline.empty.state.description" = "Lütfen ağ bağlantınızı kontrol edip tekrar deneyin."; -/* Status of a domain in `Renew` state */ -"domain.status.renew" = "Yenile"; +/* The empty state title in All Domains screen when the user is offline */ +"domain.management.offline.empty.state.title" = "İnternet Bağlantısı Yok"; -/* Status of a domain in `Verify Email` state */ -"domain.status.verify.email" = "E-posta Adresini Doğrula"; +/* Domain management choose site card button title */ +"domain.management.purchase.footer" = "*Tüm ücretli yıllık paketlere bir yıllık ücretsiz alan adı dahildir"; -/* Status of a domain in `Verifying` state */ -"domain.status.verifying" = "Doğrulanıyor"; +/* Domain management purchase domain screen title */ +"domain.management.purchase.subtitle" = "Merak etmeyin, daha sonra site ekleyebilirsiniz."; + +/* Domain management purchase domain screen title. */ +"domain.management.purchase.title" = "Alan adınızı nasıl kullanacağınızı seçin"; + +/* The search bar title in All Domains screen. */ +"domain.management.search-bar.title" = "Alan adı arayın"; + +/* The empty state description in All Domains screen when the are no domains matching the search criteria */ +"domain.management.search.empty.state.description" = "\"%@\" aramanızla eşleşen alan adı bulunamadı"; + +/* The empty state title in All Domains screen when the are no domains matching the search criteria */ +"domain.management.search.empty.state.title" = "Eşleşen Alan Adı Bulunamadı"; + +/* Domain management choose site card button title */ +"domain.management.site.card.button.title" = "Site Seç"; + +/* Domain management choose site card subtitle */ +"domain.management.site.card.footer" = "İlk yıl için ücretsiz alan adı*"; + +/* Domain management choose site card subtitle */ +"domain.management.site.card.subtitle" = "Zaten başlatmış olduğunuz bir siteyle kullanın."; + +/* Domain management choose site card title */ +"domain.management.site.card.title" = "Var olan WordPress.com sitesi"; + +/* Domain Management Screen Title */ +"domain.management.title" = "Tüm alan adları"; + +/* Domain Purchase Completion footer */ +"domain.purchase.preview.footer" = "Özel alan adınızın çalışmaya başlaması 30 dakika kadar sürebilir."; + +/* Domain Purchase Completion description (only for FREE domains). */ +"domain.purchase.preview.free.description" = "Ardından, sitenizi göz atılmaya hazır hale getirmenize yardımcı olacağız."; + +/* Domain Purchase Completion description (only for PAID domains). */ +"domain.purchase.preview.paid.description" = "Makbuzunuzu e-posta ile gönderdik. Ardından, sitenizi herkes için hazırlamanıza yardımcı olacağız."; + +/* Reflects that site is live when domain purchase feature flag is ON. */ +"domain.purchase.preview.title" = "Tebrikler, siteniz yayında!"; /* The 'Best Alternative' label under the domain name in 'Choose a domain' screen */ "domain.suggestions.row.best-alternative" = "En İyi Alternatif"; @@ -9933,12 +9788,24 @@ Example: Reply to Pamela Nguyen */ /* The text to display for paid domains in 'Site Creation > Choose a domain' screen */ "domain.suggestions.row.yearly" = "yıllık"; +/* Title for the checkout screen. */ +"domains.checkout.title" = "Ödeme"; + /* Action shown in a bottom notice to dismiss it. */ "domains.failure.dismiss" = "Kapat"; /* Content show when the domain selection action fails. */ "domains.failure.title" = "Maalesef eklemeye çalıştığınız alan adı şu anda Jetpack'ten satın alınamıyor."; +/* Title for the screen where the user can choose how to use the domain they're end up purchasing. */ +"domains.purchase.choice.title" = "Alan Adı Satın Alın"; + +/* Back button title that navigates back to the search domains screen. */ +"domains.search.backButton.title" = "Ara"; + +/* Title of screen where user chooses a site to connect to their selected domain */ +"domains.sitePicker.title" = "Site Seçin"; + /* No comment provided by engineer. */ "double-tap to change unit" = "birimi değiştirmek için çift dokunun"; @@ -9956,6 +9823,15 @@ Example: Reply to Pamela Nguyen */ Site Address placeholder */ "example.com" = "example.com"; +/* Title for confirmation navigation bar button item */ +"externalMediaPicker.add" = "Ekle"; + +/* Bottom toolbar title in the selection mode */ +"externalMediaPicker.toolbarSelectItemsPrompt" = "Görselleri seç"; + +/* Bottom toolbar title in the selection mode */ +"externalMediaPicker.toolbarViewSelected" = "Seçileni görüntüle (%@)"; + /* Title of screen the displays the details of an advertisement campaign. */ "feature.blaze.campaignDetails.title" = "Kampanya Ayrıntıları"; @@ -10055,9 +9931,6 @@ Example: Reply to Pamela Nguyen */ /* (placeholder) Help the user enter a URL into the field */ "http:\/\/my-site-address (URL)" = "http:\/\/benim-site-adresim (URL)"; -/* Label displayed on image media items. */ -"image" = "görsel"; - /* Sentence to justify why the app is asking permission from the user to use their camera. */ "infoplist.NSCameraUsageDescription" = "Yazılarınızda kullanacağınız fotoğraf veya videoları çekmek için."; @@ -10358,6 +10231,9 @@ Example: Reply to Pamela Nguyen */ /* Indicating that referrer was marked as spam */ "marked as spam" = "spam olarak işaretlendi"; +/* Products header text in Me Screen. */ +"me.products.header" = "Ürünler"; + /* Title of error prompt shown when a sync fails. */ "media.syncFailed" = "Ortam senkronize edilemiyor"; @@ -10370,18 +10246,24 @@ Example: Reply to Pamela Nguyen */ /* Message of an alert informing users that the video they are trying to select is not allowed. */ "mediaExporter.videoLimitExceededError" = "5 dakikadan uzun video yüklemek için ücretli bir paket gerekir."; -/* Verb. User action to dismiss error alert when failing to load media item. */ -"mediaItemTable.errorAlert.dismissButton" = "Kapat"; - /* Accessibility hint for add button to add items to the user's media library */ "mediaLibrary.addButtonAccessibilityHint" = "Yeni ortam ekle"; /* Accessibility label for add button to add items to the user's media library */ "mediaLibrary.addButtonAccessibilityLabel" = "Ekle"; +/* Button name in the more menu */ +"mediaLibrary.aspectRatioGrid" = "En Boy Oranı Izgarası"; + +/* Context menu button */ +"mediaLibrary.buttonDelete" = "Sil"; + /* Media screen navigation bar button Select title */ "mediaLibrary.buttonSelect" = "Seç"; +/* Context menu button */ +"mediaLibrary.buttonShare" = "Paylaş"; + /* Verb. Button title. Tapping cancels an action. */ "mediaLibrary.deleteConfirmationCancel" = "İptal et"; @@ -10403,6 +10285,21 @@ Example: Reply to Pamela Nguyen */ /* Text displayed in HUD after successfully deleting a media item */ "mediaLibrary.deletionSuccessMessage" = "Silindi!"; +/* The name of the media filter */ +"mediaLibrary.filterAll" = "Hepsi"; + +/* The name of the media filter */ +"mediaLibrary.filterAudio" = "Ses"; + +/* The name of the media filter */ +"mediaLibrary.filterDocuments" = "Belgeler"; + +/* The name of the media filter */ +"mediaLibrary.filterImages" = "Resimler"; + +/* The name of the media filter */ +"mediaLibrary.filterVideos" = "Videolar"; + /* User action to delete un-uploaded media. */ "mediaLibrary.retryOptionsAlert.delete" = "Sil"; @@ -10415,6 +10312,12 @@ Example: Reply to Pamela Nguyen */ /* Message displayed when no results are returned from a media library search. Should match Calypso. */ "mediaLibrary.searchResultsEmptyTitle" = "Aramanızla eşleşen ortam yok"; +/* Text displayed in HUD if there was an error attempting to share a group of media items. */ +"mediaLibrary.sharingFailureMessage" = "Seçilen öğeler paylaşılamadı."; + +/* Button name in the more menu */ +"mediaLibrary.squareGrid" = "Kare Izgara"; + /* Media screen navigation title */ "mediaLibrary.title" = "Ortam"; @@ -10436,6 +10339,12 @@ Example: Reply to Pamela Nguyen */ /* The title of the button to dismiss the alert shown when the picked media cannot be imported into stories. */ "mediaPicker.failedMediaExportAlert.dismissButton" = "Kapat"; +/* Error message when picked media cannot be imported into stories. */ +"mediaPicker.failedMediaExportAlert.message" = "Ortamınız dışa aktarılamadı. Sorun tekrar ederse lütfen Ben > Yardım ve Destek ekranından bizimle iletişim kurun."; + +/* Error title when picked media cannot be imported into stories. */ +"mediaPicker.failedMediaExportAlert.title" = "Ortam Dışa Aktarılamadı"; + /* Message for alert when access to camera is not granted */ "mediaPicker.noCameraAccessMessage" = "Bu uygulama yeni görsel yakalamak için Kamera’ya erişim izni istiyor, buna izin vermek istiyorsanız lütfen gizlilik ayarlarını değiştirin."; @@ -10469,6 +10378,12 @@ Example: Reply to Pamela Nguyen */ /* The name of the action in the context menu */ "mediaPicker.takeVideo" = "Video Çek"; +/* Navigation title for media preview. Example: 1 of 3 */ +"mediaPreview.NofM" = "%1$@ \/ %2$@"; + +/* Max image size in pixels (e.g. 300x300px) */ +"mediaSizeSlider.valueFormat" = "%1$d × %2$d piksel"; + /* The description in the Delete WordPress screen */ "migration.deleteWordpress.description" = "Görünüşe göre WordPress uygulaması hala yüklü."; @@ -10481,9 +10396,6 @@ Example: Reply to Pamela Nguyen */ /* The title in the Delete WordPress screen */ "migration.deleteWordpress.title" = "Artık cihazınızda WordPress uygulamasına ihtiyacınız yok"; -/* Primary button title in the migration done screen. */ -"migration.done.actions.primary.title" = "Bitir"; - /* Footer for the migration done screen. */ "migration.done.footer" = "Veri çakışmalarını önlemek için cihazınızdan WordPress uygulamasını kaldırmanızı öneririz."; @@ -10493,6 +10405,9 @@ Example: Reply to Pamela Nguyen */ /* Primary description in the migration done screen. */ "migration.done.primaryDescription" = "Tüm verilerinizi ve ayarlarınızı aktardık. Her şey bıraktığınız yerde."; +/* Secondary description (second paragraph) in the migration done screen. */ +"migration.done.secondaryDescription" = "Jetpack uygulamasındaki WordPress yolculuğunuza devam etme vakti."; + /* Title of the migration done screen. */ "migration.done.title" = "Jetpack'e geçtiğiniz için teşekkürler!"; @@ -10541,6 +10456,9 @@ Example: Reply to Pamela Nguyen */ /* The title in the migration welcome screen */ "migration.welcome.title" = "Jetpack'e hoş geldiniz."; +/* Primary button title in the migration done screen. */ +"migrationDone.actions.primaryTitle" = "Haydi başlayalım"; + /* Description for the static screen displayed prompting users to switch the Jetpack app. */ "movedToJetpack.description" = "Jetpack uygulaması, WordPress uygulamasının tüm işlevlerine ve artık İstatistikler, Okuyucu, Bildirimler ve daha fazlasına özel erişime sahiptir."; @@ -10616,6 +10534,30 @@ Example: Reply to Pamela Nguyen */ /* Message title for when a user has no sites. */ "mySite.noSites.title" = "Hiçbir siteniz yok"; +/* Menu title for the add site option */ +"mySite.siteActions.addSite" = "Site ekleyin"; + +/* Button that reveals more site actions */ +"mySite.siteActions.button" = "Site eylemleri"; + +/* Accessibility hint for button used to show more site actions */ +"mySite.siteActions.hint" = "Daha fazla site eylemi göstermek için dokunun"; + +/* Menu title for the personalize home option */ +"mySite.siteActions.personalizeHome" = "Ana sayfayı kişiselleştir"; + +/* Menu title for the change site icon option */ +"mySite.siteActions.siteIcon" = "Site simgesini değiştir"; + +/* Menu title for the change site title option */ +"mySite.siteActions.siteTitle" = "Site başlığını değiştir"; + +/* Menu title for the switch site option */ +"mySite.siteActions.switchSite" = "Site değiştir"; + +/* Menu title for the visit site option */ +"mySite.siteActions.visitSite" = "Siteyi ziyaret et"; + /* Dismiss button title. */ "noResultsViewController.dismissButton" = "Kapat"; @@ -10631,14 +10573,20 @@ Example: Reply to Pamela Nguyen */ /* This is one of the buttons we display when prompting the user for a review */ "notifications.appRatings.sendFeedback.yes.buttonTitle" = "Geri Bildirim gönderin"; -/* Word separating the current index from the total amount. I.e.: 7 of 9 */ -"of" = ","; +/* Badge for page cells */ +"pageList.badgeHomepage" = "Ana sayfa"; -/* Label displayed on media items that are not video, image, or audio. */ -"other" = "diğer"; +/* Badge for page cells */ +"pageList.badgeLocalChanges" = "Yerel değişiklikler"; -/* Promote the page with Blaze. */ -"pages.blaze.actionTitle" = "Blaze ile tanıt"; +/* Badge for page cells */ +"pageList.badgePendingReview" = "İnceleme bekliyor"; + +/* Badge for page cells */ +"pageList.badgePosts" = "Gönderiler sayfası"; + +/* Badge for page cells */ +"pageList.badgePrivate" = "Gizli"; /* Subtitle of the theme template homepage cell */ "pages.template.subtitle" = "Ana sayfanız, Tema şablonunu kullanıyor ve web düzenleyicisinde açılacak."; @@ -10646,6 +10594,36 @@ Example: Reply to Pamela Nguyen */ /* Title of the theme template homepage cell */ "pages.template.title" = "Ana sayfa"; +/* Message informing the user that their static homepage page was set successfully */ +"pages.updatePage.successTitle" = "Sayfa başarıyla güncellendi"; + +/* Delete option in the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.actionTitle" = "Kalıcı Olarak Sil"; + +/* Message of the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.alertMessage" = "Bu sayfayı kalıcı olarak silmek istediğinizden emin misiniz?"; + +/* Title of the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.alertTitle" = "Kalıcı olarak silinsin mi?"; + +/* Menu option for filtering posts by everyone */ +"pagesList.pagesByEveryone" = "Herkese ait sayfalar"; + +/* Menu option for filtering posts by me */ +"pagesList.pagesByMe" = "Bana ait sayfalar"; + +/* Trash option in the trash page confirmation alert. */ +"pagesList.trash.actionTitle" = "Çöp Kutusuna Taşı"; + +/* Message of the trash page confirmation alert. */ +"pagesList.trash.alertMessage" = "Bu sayfayı çöpe atmak istediğinizden emin misiniz?"; + +/* Title of the trash page confirmation alert. */ +"pagesList.trash.alertTitle" = "Bu sayfa çöpe atılsın mı?"; + +/* Cancels an Action */ +"pagesList.trash.cancel" = "İptal Et"; + /* No comment provided by engineer. */ "password" = "parola"; @@ -10685,6 +10663,51 @@ Example: Reply to Pamela Nguyen */ /* Register Domain - Domain contact information field Phone */ "phone number" = "telefon numarası"; +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.createdTimeAgo" = "Oluşturma tarihi: %@"; + +/* Status mesasge for post cells */ +"post.deletingPostPermanentlyStatusMessage" = "Gönderi siliniyor..."; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.editedTimeAgo" = "Düzenlenme tarihi: %@"; + +/* Status mesasge for post cells */ +"post.movingToTrashStatusMessage" = "Gönderi çöpe taşınıyor..."; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.publishedTimeAgo" = "Yayımlanma tarihi: %@"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.scheduledForDate" = "Zamanlanma tarihi: %@"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.trashedTimeAgo" = "Çöpe atılma tarihi: %@"; + +/* Accessibility label for the post author in the post list. The parameter is the author name. For example, \"By Elsa.\" */ +"postList.a11y.authorChunkFormat" = "%@ tarafından."; + +/* Accessibility label for a post's excerpt in the post list. The parameter is the post excerpt. For example, \"Excerpt. This is the first paragraph.\" */ +"postList.a11y.exerptChunkFormat" = "Alıntı. %@."; + +/* Accessibility label for a sticky post in the post list. */ +"postList.a11y.sticky" = "Yapışkan."; + +/* Accessibility label for a post in the post list. The first placeholder is the post title. The second placeholder is the date. */ +"postList.a11y.titleAndDateChunkFormat" = "%1$@, %2$@."; + +/* Title for the 'Trash' post list row swipe action */ +"postList.swipeActionDelete" = "Sil"; + +/* Title for the 'Delete' post list row swipe action */ +"postList.swipeActionDeletePermanently" = "Sil"; + +/* Title for the 'Share' post list row swipe action */ +"postList.swipeActionShare" = "Paylaş"; + +/* Title for the 'View' post list row swipe action */ +"postList.swipeActionView" = "Görüntüle"; + /* User action to dismiss featured media options. */ "postSettings.featuredImageUploadActionSheet.dismiss" = "Kapat"; @@ -10703,9 +10726,84 @@ Example: Reply to Pamela Nguyen */ /* Button in Post Settings */ "postSettings.setFeaturedImageButton" = "Öne Çıkan Görseli Ayarla"; +/* Error message on post/page settings screen */ +"postSettings.updateFailedMessage" = "Gönderi ayarları güncellenemedi."; + /* Promote the post with Blaze. */ "posts.blaze.actionTitle" = "Blaze ile tanıt"; +/* Label for the Post List option that cancels automatic uploading of a post. */ +"posts.cancelUpload.actionTitle" = "Yüklemeyi iptal et"; + +/* Label for post comments option. Tapping displays comments for a post. */ +"posts.comments.actionTitle" = "Yorumlar"; + +/* Label for the delete post option. Tapping permanently deletes a post. */ +"posts.delete.actionTitle" = "Kalıcı olarak sil"; + +/* Label for an option that moves a post to the draft folder */ +"posts.draft.actionTitle" = "Taslağa taşı"; + +/* Label for post duplicate option. Tapping creates a copy of the post. */ +"posts.duplicate.actionTitle" = "Çoğalt"; + +/* Opens a submenu for page attributes. */ +"posts.pageAttributes.actionTitle" = "Sayfa nitelikleri"; + +/* Label for the preview post button. Tapping displays the post as it appears on the web. */ +"posts.preview.actionTitle" = "Önizle"; + +/* Label for an option that moves a publishes a post immediately */ +"posts.publish.actionTitle" = "Şimdi yayınla"; + +/* Retry uploading the post. */ +"posts.retry.actionTitle" = "Tekrar dene"; + +/* Set the selected page as the homepage. */ +"posts.setHomepage.actionTitle" = "Anasayfa olarak ayarla"; + +/* Set the parent page for the selected page. */ +"posts.setParent.actionTitle" = "Üst öğe belirle"; + +/* Set the selected page as a posts page. */ +"posts.setPostsPage.actionTitle" = "Gönderiler sayfası olarak ayarla"; + +/* Set the selected page as a regular page. */ +"posts.setRegularPage.actionTitle" = "Normal sayfa olarak ayarla"; + +/* Label for post settings option. Tapping displays settings for a post. */ +"posts.settings.actionTitle" = "Ayarlar"; + +/* Share the post. */ +"posts.share.actionTitle" = "Paylaş"; + +/* Label for post stats option. Tapping displays statistics for a post. */ +"posts.stats.actionTitle" = "İstatistikler"; + +/* Label for a option that moves a post to the trash folder */ +"posts.trash.actionTitle" = "Çöp kutusuna taşı"; + +/* Label for the view post button. Tapping displays the post as it appears on the web. */ +"posts.view.actionTitle" = "Görüntüle"; + +/* A short message explaining that a page was deleted permanently. */ +"postsList.deletePage.message" = "Sayfa kalıcı olarak silindi"; + +/* A short message explaining that a post was deleted permanently. */ +"postsList.deletePost.message" = "Gönderi kalıcı olarak silindi"; + +/* A short message explaining that a page was moved to the trash bin. */ +"postsList.movePageToTrash.message" = "Sayfa çöpe taşındı"; + +/* A short message explaining that a post was moved to the trash bin. */ +"postsList.movePostToTrash.message" = "Gönderi çöpe taşındı"; + +/* Menu option for filtering posts by everyone */ +"postsList.postsByEveryone" = "Herkese ait gönderiler"; + +/* Menu option for filtering posts by me */ +"postsList.postsByMe" = "Bana ait gönderiler"; + /* Title for the button to subscribe to Jetpack Social on the remaining shares view */ "postsettings.social.remainingshares.subscribe" = "Daha fazla paylaşım için şimdi abone olun"; @@ -10850,13 +10948,15 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Text for the 'Like' button on the reader post card cell. */ "reader.post.button.like" = "Beğen"; -/* Accessibility hint for the like button on the reader post card cell - Accessibility hint for the liked button on the reader post card cell */ +/* Accessibility hint for the like button on the reader post card cell */ "reader.post.button.like.accessibility.hint" = "Gönderiyi beğenir."; /* Text for the 'Liked' button on the reader post card cell. */ "reader.post.button.liked" = "Beğenildi"; +/* Accessibility hint for the liked button on the reader post card cell */ +"reader.post.button.liked.accessibility.hint" = "Gönderinin beğenisini kaldırır."; + /* Accessibility hint for the site header on the reader post card cell */ "reader.post.button.menu.accessibility.hint" = "Daha fazla işlem içeren bir menü açar."; @@ -10920,6 +11020,12 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Title for the tooltip anchor. */ "readerDetail.tooltipAnchorTitle.accessibilityLabel" = "Yeni"; +/* The button title for the transfer footer view in Register Domain screen */ +"register.domain.transfer.button.title" = "Alan adını aktar"; + +/* The title for the transfer footer view in Register Domain screen */ +"register.domain.transfer.title" = "Zaten sahip olduğunuz bir alan adını aktarmak mı istiyorsunuz?"; + /* Information of what related post are and how they are presented */ "relatedPostsSettings.optionsFooter" = "Benzer Yazılar, yazılarınızın altında sitenizden ilgili içerikler gösterir."; @@ -11010,12 +11116,30 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Accessibility label for image thumbnails in the media collection view. The parameter is the creation date of the image. */ "siteMedia.accessibilityLabelImage" = "Görsel, %@"; +/* Accessibility label for video thumbnails in the media collection view. The parameter is the creation date of the video. */ +"siteMedia.accessibilityLabelVideo" = "Video, %@"; + /* Accessibility label to use when creation date from media asset is not know. */ "siteMedia.accessibilityUnknownCreationDate" = "Bilinmeyen oluşturma tarihi"; /* Accessibility hint for actions when displaying media items. */ "siteMedia.cellAccessibilityHint" = "Ortam dosyası seçin."; +/* Accessibility hint for media item preview for user's viewing an item in their media library */ +"siteMediaItem.contentViewAccessibilityHint" = "Ortamı tam ekranda görüntülemek için dokunun"; + +/* Accessibility label for media item preview for user's viewing an item in their media library */ +"siteMediaItem.contentViewAccessibilityLabel" = "Ortamı önizle"; + +/* Title for confirmation navigation bar button item */ +"siteMediaPicker.add" = "Ekle"; + +/* Button selection media in media picker */ +"siteMediaPicker.deselect" = "Seçimi kaldır"; + +/* Button selection media in media picker */ +"siteMediaPicker.select" = "Seç"; + /* Media screen navigation title */ "siteMediaPicker.title" = "Ortam"; @@ -11023,7 +11147,7 @@ Example: given a notice format "Following %@" and empty site name, this will be "siteSettings.privacy.title" = "Gizlilik"; /* Hint for users when hidden privacy setting is set */ -"siteVisibility.hidden.hint" = "Siteniz herkes tarafından görüntülenebilir, ancak arama motorlarından kendisini indekslememelerini ister."; +"siteVisibility.hidden.hint" = "Siteniz görüntülemeye hazır oluncaya kadar “Yakında” uyarısıyla birlikte ziyaretçilerden gizlenir."; /* Text for privacy settings: Hidden */ "siteVisibility.hidden.title" = "Gizli"; @@ -11184,6 +11308,12 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Dismiss the AlertView */ "stockPhotos.strings.dismiss" = "Kapat"; +/* Subtitle for placeholder in Free Photos. The company name 'Pexels' should always be written as it is. */ +"stockPhotos.subtitle" = "Pexels tarafından sağlanan fotoğraflar"; + +/* Title for placeholder in Free Photos */ +"stockPhotos.title" = "Ortam kitaplığınıza eklemek için ücretsiz fotoğraflar arayın"; + /* Section title for prominent suggestions */ "suggestions.section.prominent" = "Bu sohbette"; @@ -11331,6 +11461,9 @@ Example: given a notice format "Following %@" and empty site name, this will be /* View title for Help & Support page. */ "support.title" = "Yardım"; +/* Title for placeholder in Tenor picker */ +"tenor.welcomeMessage" = "Ortam kitaplığınıza eklemek için GIF arayın"; + /* Header of delete screen section listing things that will be deleted. */ "these items will be deleted:" = "bu ögeler silinecekler:"; @@ -11346,9 +11479,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Displayed in the confirmation alert when marking unread notifications as read. */ "unread" = "okunmamış"; -/* Label displayed on video media items. */ -"video" = "video"; - /* Portion of a message for Jetpack users that have multisite WP installation, thus Restore is not available. This part is a link, colored with a different color. */ "visit our documentation page" = "belge sayfamızı ziyaret edin"; diff --git a/WordPress/Resources/zh-Hans.lproj/Localizable.strings b/WordPress/Resources/zh-Hans.lproj/Localizable.strings index 0b5e50b194e6..fa4934032067 100644 --- a/WordPress/Resources/zh-Hans.lproj/Localizable.strings +++ b/WordPress/Resources/zh-Hans.lproj/Localizable.strings @@ -1,4 +1,4 @@ -/* Translation-Revision-Date: 2023-10-18 14:54:08+0000 */ +/* Translation-Revision-Date: 2024-01-04 10:54:08+0000 */ /* Plural-Forms: nplurals=1; plural=0; */ /* Generator: GlotPress/4.0.0-alpha.11 */ /* Language: zh_CN */ @@ -152,9 +152,6 @@ /* Difference label for Period Overview stat, indicating change from previous period. Ex: +99.9K (5%) */ "%@%@ (%@%%)" = "%1$@%2$@ (%3$@%%)"; -/* Accessibility label for a post in the post list. The parameters are the title, and date respectively. For example, \"Let it Go, 1 hour ago.\" */ -"%@, %@." = "%1$@,%2$@。"; - /* Accessibility value for a Stats' Posting Activity Month if the user selected a day with posts. The first parameter is day (e.g. November 2019). The second parameter is the number of posts. */ "%@. %d posts." = "%1$@。%2$d篇文章。"; @@ -210,9 +207,6 @@ /* Age between dates over one year. */ "%d years" = "%d 年后"; -/* Max image size in pixels (e.g. 300x300px) */ -"%dx%dpx" = "%1$dx%2$dpx"; - /* One menu area available in the theme */ "%i menu area in this theme" = "此主题中有 %i 个菜单区域"; @@ -271,6 +265,9 @@ translators: Block name. %s: The localized block name */ /* translators: %s: social link name e.g: \"Instagram\". */ "%s social icon" = "%s 社交图标"; +/* translators: displayed right after the classic block is converted to blocks. %s: The localized classic block name */ +"'%s' block converted to blocks" = "‘%s’区块已转换为多个区块"; + /* translators: Missing block alert title. %s: The localized block name */ "'%s' is not fully-supported" = "不完全支持 '%s'"; @@ -478,13 +475,6 @@ translators: Block name. %s: The localized block name */ /* Label for the Activity Type filter button when there are more than 1 activity type selected */ "Activity Type (%1$d)" = "活动类型 (%1$d)"; -/* Accessibility label for add button to add items to the user's media library - Remove asset from media picker list */ -"Add" = "添加"; - -/* Action for Media Picker to indicate selection of media. The argument in the string represents the number of elements (as numeric digits) selected */ -"Add %@" = "添加 %@"; - /* No comment provided by engineer. */ "Add Block After" = "在之后添加区块"; @@ -581,9 +571,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Add menu item to children" = "将菜单项添加至子网页"; -/* Accessibility hint for add button to add items to the user's media library */ -"Add new media" = "添加新媒体"; - /* Screen reader text for Menus button that adds a new menu to a site. */ "Add new menu" = "添加新菜单"; @@ -649,9 +636,6 @@ translators: Block name. %s: The localized block name */ /* Option to select the Airmail app when logging in with magic links */ "Airmail" = "Airmail"; -/* Description of albums in the photo libraries */ -"Albums" = "相册"; - /* Image alignment option title. Title of the screen for choosing an image's alignment. */ "Alignment" = "对齐方式"; @@ -665,6 +649,9 @@ translators: Block name. %s: The localized block name */ Title of the drafts filter. This filter shows a list of draft posts. */ "All" = "全部"; +/* Information about redeeming domain credit on site dashboard. */ +"All WordPress.com annual plans include a custom domain name. Register your free domain now." = "所有 WordPress.com 年度套餐均包含一个自定义域名。 立即注册您的免费域名。"; + /* Footer of the free domain registration section for a paid plan. Information about redeeming domain credit on site dashboard. */ "All WordPress.com plans include a custom domain name. Register your free premium domain now." = "全部 WordPress.com 套餐包含自定义域名。立即注册您的免费高级域。"; @@ -730,10 +717,13 @@ translators: Block name. %s: The localized block name */ "Alt Text" = "替代文本"; /* No comment provided by engineer. */ -"Alternatively, you can detach and edit these blocks separately by tapping “Detach patterns”." = "或者,您可以轻点“分离样板”来单独分离和编辑这些区块。"; +"Alternatively, you can convert the content to blocks." = "或者,您可以将内容转换为多个区块。"; /* No comment provided by engineer. */ -"Alternatively, you can detach and edit this block separately by tapping “Detach pattern”." = "或者,您可以轻点“分离样板”来单独分离和编辑此区块。"; +"Alternatively, you can detach and edit this block separately by tapping “Detach”." = "或者,您也可以轻点“分离”来分离并单独编辑此区块。"; + +/* translators: Alternative option included in a warning related to having blocks deeply nested. */ +"Alternatively, you can flatten the content by ungrouping the block." = "或者,您也可以取消区块分组,以使内容扁平化。"; /* Instruction text to explain to help users type their password instead of using magic link login option. */ "Alternatively, you may enter the password for this account." = "此外,您也可以输入此账户的密码。"; @@ -882,15 +872,9 @@ translators: Block name. %s: The localized block name */ /* Message prompting the user to confirm that they want to disconnect Jetpack from the site. */ "Are you sure you want to disconnect Jetpack from the site?" = "确定要断开 Jetpack 与该站点的连接吗?"; -/* Message prompting the user to confirm that they want to permanently delete a group of media items. */ -"Are you sure you want to permanently delete these items?" = "是否确定要永久删除这些项目?"; - /* Message prompting the user to confirm that they want to permanently delete a media item. Should match Calypso. */ "Are you sure you want to permanently delete this item?" = "是否确定要永久删除此项目?"; -/* Message of the confirmation alert when deleting a page from the trash. */ -"Are you sure you want to permanently delete this page?" = "是否确定要永久删除此页面?"; - /* Message of the confirmation alert when deleting a post from the trash. */ "Are you sure you want to permanently delete this post?" = "确定要永久删除此文章吗?"; @@ -922,9 +906,6 @@ translators: Block name. %s: The localized block name */ /* Title of message shown when user taps submit for review. */ "Are you sure you want to submit for review?" = "是否确定要提供以供审核?"; -/* Message of the trash page confirmation alert. */ -"Are you sure you want to trash this page?" = "是否确定要删除此页面?"; - /* Message of the trash confirmation alert. */ "Are you sure you want to trash this post?" = "是否确定要删除此文章?"; @@ -965,9 +946,6 @@ translators: Block name. %s: The localized block name */ /* translators: accessibility text. Empty Audio caption. */ "Audio caption. Empty" = "音频说明。空"; -/* Accessibility label for audio items in the media collection view. The parameter is the creation date of the audio. */ -"Audio, %@" = "音频,%@"; - /* No comment provided by engineer. */ "Authenticating" = "正在验证"; @@ -1178,10 +1156,7 @@ translators: Block name. %s: The localized block name */ "Blocks menu" = "区块菜单"; /* translators: Warning related to having blocks deeply nested. %d: The deepest nesting level. */ -"Blocks nested deeper than %d levels may not render properly in the mobile editor. For this reason, we recommend flattening the content by ungrouping the block or editing the block using the web editor." = "嵌套深度超过 %d 层级的区块在移动端编辑器中可能无法正常渲染。 因此,我们建议取消区块分组或使用 Web 编辑器编辑区块,以使内容扁平化。"; - -/* translators: Warning related to having blocks deeply nested. %d: The deepest nesting level. */ -"Blocks nested deeper than %d levels may not render properly in the mobile editor. For this reason, we recommend flattening the content by ungrouping the block or editing the block using your web browser." = "嵌套深度超过 %d 层级的区块在移动端编辑器中可能无法正常渲染。 因此,我们建议取消区块分组或使用 Web 浏览器编辑区块,以使内容扁平化。"; +"Blocks nested deeper than %d levels may not render properly in the mobile editor." = "嵌套深度超过 %d 层级的区块在移动端编辑器中可能无法正常渲染。"; /* Title of a button that displays the WordPress.com blog */ "Blog" = "博客"; @@ -1259,9 +1234,6 @@ translators: Block name. %s: The localized block name */ /* Label for the post author in the post detail. */ "By " = "作者"; -/* Accessibility label for the post author in the post list. The parameter is the author name. For example, \"By Elsa.\" */ -"By %@." = "作者是 %@。"; - /* Legal disclaimer for logging in. The underscores _..._ denote underline. */ "By continuing, you agree to our _Terms of Service_." = "继续即表示您同意我们的_服务条款_。"; @@ -1281,8 +1253,7 @@ translators: Block name. %s: The localized block name */ /* Label for size of media while it's being calculated. */ "Calculating..." = "正在计算..."; -/* Accessibility label for taking an image or video with the camera on formatting toolbar. - Accessibility label for the camera tile in the collection view */ +/* Accessibility label for taking an image or video with the camera on formatting toolbar. */ "Camera" = "照相机"; /* Title of an alert informing the user the camera permission for the app is disabled and its needed to proceed */ @@ -1333,10 +1304,7 @@ translators: Block name. %s: The localized block name */ Cancels an Action Cancels an alert. Cancels the mark all as read action. - Dismiss the post action sheet Editing GIF alert cancel action button. - Label for a cancel button - Label for the auto-upload cancelation button in the post list. Tapping will prevent the app from auto-uploading the post. Menus cancel button for deleting a menu. Menus cancel button within text bar while editing items. Menus: Cancel button title for canceling an edited menu item. @@ -1353,10 +1321,6 @@ translators: Block name. %s: The localized block name */ Verb. Title for Jetpack Restore cancel button. */ "Cancel" = "取消"; -/* Label for the Post List option that cancels automatic uploading of a post. - Media Library option to cancel an in-progress or failed upload. */ -"Cancel Upload" = "取消上传"; - /* Menus alert button title to cancel discarding changes and not createa a new menu. Menus alert button title to cancel discarding changes and not select a new menu Menus alert button title to cancel discarding changes and not select a new menu location */ @@ -1415,9 +1379,6 @@ translators: Block name. %s: The localized block name */ Main title */ "Change Password" = "修改密码"; -/* Title of button that takes user to the system Settings section for the app */ -"Change Settings" = "更改设置"; - /* Change Username title. */ "Change Username" = "更改用户名"; @@ -1557,9 +1518,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Choose file" = "选择文件"; -/* Menu option for selecting media from the device's photo library. */ -"Choose from My Device" = "从我的设备中选择"; - /* Explanatory text for Homepage Settings homepage type selection. */ "Choose from a homepage that displays your latest posts (classic blog) or a fixed \/ static page." = "选择显示您的近期文章(经典博客)的主页或固定\/静态页面。"; @@ -1760,9 +1718,6 @@ translators: Block name. %s: The localized block name */ /* Community & Non-Profit site intent topic */ "Community & Non-Profit" = "社区与非营利组织"; -/* Accessibility indication that the current Post List style is currently Compact. */ -"Compact" = "紧凑"; - /* The action is completed */ "Completed" = "已完成"; @@ -1948,10 +1903,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Copied block" = "已拷贝区块"; -/* Copy the post url and paste anywhere in phone - Label for page copy link. Tapping copy the url of page */ -"Copy Link" = "复制链接"; - /* Copy link to oomment button title */ "Copy Link to Comment" = "将链接复制到评论中"; @@ -2060,9 +2011,6 @@ translators: Block name. %s: The localized block name */ /* Error title displayed when unable to close user account. */ "Couldn’t close account automatically" = "无法自动关闭帐户"; -/* Message to show while media data source is finding the number of items available. */ -"Counting media items..." = "正在统计媒体项目数…"; - /* Period Stats 'Countries' header */ "Countries" = "国家\/地区"; @@ -2313,7 +2261,6 @@ translators: Block name. %s: The localized block name */ /* Delete Delete button title for the warning shown to the user when he refuses to re-login when the authToken is missing. Title for button that permanently deletes a media item (photo / video) - Title for button that permanently deletes one or more media items (photos / videos) Title of a delete button Title of the trash confirmation alert. */ "Delete" = "删除"; @@ -2321,15 +2268,11 @@ translators: Block name. %s: The localized block name */ /* Menus confirmation button for deleting a menu. */ "Delete Menu" = "删除菜单"; -/* Delete option in the confirmation alert when deleting a page from the trash. - Delete option in the confirmation alert when deleting a post from the trash. - Label for a button permanently deletes a page. - Label for the delete post option. Tapping permanently deletes a post. +/* Delete option in the confirmation alert when deleting a post from the trash. Title for button on the comment details page that deletes the comment when tapped. */ "Delete Permanently" = "永久删除"; -/* Title of the confirmation alert when deleting a page from the trash. - Title of the confirmation alert when deleting a post from the trash. */ +/* Title of the confirmation alert when deleting a post from the trash. */ "Delete Permanently?" = "是否永久删除?"; /* Button label for deleting the current site @@ -2448,7 +2391,6 @@ translators: Block name. %s: The localized block name */ /* Accessibility label for button to dismiss a bottom sheet Accessibility label for the transparent space above the login dialog which acts as a button to dismiss the dialog. Accessibility label for the transparent space above the signup dialog which acts as a button to dismiss the dialog. - Action to show on alert when view asset fails. Dismiss a view. Verb */ "Dismiss" = "关闭"; @@ -2466,12 +2408,6 @@ translators: Block name. %s: The localized block name */ User's Display Name */ "Display Name" = "显示名称"; -/* Accessibility label for other media items in the media collection view. The parameter is the creation date of the document. */ -"Document, %@" = "文档,%@"; - -/* Accessibility label for other media items in the media collection view. The parameter is the filename file. */ -"Document: %@" = "文档:%@"; - /* Message shown when all tours have been completed */ "Doesn't it feel good to cross things off a list?" = "将清单上的事情划掉是不是感觉很棒?"; @@ -2632,8 +2568,7 @@ translators: Block name. %s: The localized block name */ /* Description of a Quick Start Tour */ "Draft and publish a post." = "起草并发布文章。"; -/* Title of the drafts filter. This filter shows a list of draft posts. - Title of the drafts header in search list. */ +/* Title of the drafts filter. This filter shows a list of draft posts. */ "Drafts" = "草稿"; /* No comment provided by engineer. */ @@ -2645,10 +2580,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Drag to adjust focal point" = "拖动以调整焦点"; -/* Label for page duplicate option. Tapping creates a copy of the page. - Label for post duplicate option. Tapping creates a copy of the post. */ -"Duplicate" = "复制"; - /* No comment provided by engineer. */ "Duplicate block" = "复制区块"; @@ -2661,13 +2592,9 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Each block has its own settings. To find them, tap on a block. Its settings will appear on the toolbar at the bottom of the screen." = "每个区块都有各自的设置。要找到这些设置,请点击一个区块。区块的设置将出现在屏幕底部的工具栏上。"; -/* Accessibility label for edit button to enable multi selection mode in the user's media library - Edit the post. - Editing GIF alert default action button. +/* Editing GIF alert default action button. Edits a Comment Edits the comment - Label for a button that opens the Edit Page view controller - Label for the edit post button. Tapping displays the editor. User action to edit media details. Verb, edit a comment */ "Edit" = "编辑"; @@ -2675,9 +2602,6 @@ translators: Block name. %s: The localized block name */ /* Title for the edit more button section */ "Edit \"More\" button" = "编辑“更多”按钮"; -/* Button that displays the media editor to the user */ -"Edit %@" = "编辑 %@"; - /* Blocklist Keyword Edition Title */ "Edit Blocklist Word" = "编辑禁止名单关键词"; @@ -2867,9 +2791,6 @@ translators: Block name. %s: The localized block name */ /* Secondary message shown when there are no domains that match the user entered text. */ "Enter different words above and we'll look for an address that matches it." = "在上面输入不同的字词,我们会查找与其相符的地址。"; -/* Accessibility hint for edit button to enable multi selection mode in the user's media library */ -"Enter edit mode to enable multi select to delete" = "进入编辑模式可支持多选删除"; - /* (placeholder) Help enter WordPress password Placeholder of a field to type a password to protect the post. */ "Enter password" = "输入密码"; @@ -3025,9 +2946,6 @@ translators: Block name. %s: The localized block name */ /* Short title telling the user they will receive a blogging reminder every day of the week. */ "Every day at %@" = "每天%@"; -/* Label for the post author filter. This filter shows posts for all users on the blog. */ -"Everyone" = "所有人"; - /* Example story title description */ "Example story title" = "故事标题示例"; @@ -3037,9 +2955,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Excerpt length (words)" = "摘录长度(字数)"; -/* Accessibility label for a post's excerpt in the post list. The parameter is the post excerpt. For example, \"Excerpt. This is the first paragraph.\" */ -"Excerpt. %@." = "摘录。%@。"; - /* Should be the same as the text displayed if the user clicks the (i) in Calypso. */ "Excerpts are optional hand-crafted summaries of your content." = "摘录是为您的内容手动生成的可选摘要。"; @@ -3049,8 +2964,7 @@ translators: Block name. %s: The localized block name */ /* Accessibility Label for the exit full screen button on the full screen comment reply mode */ "Exit Full Screen" = "退出全屏"; -/* Accessibility indication that the current Post List style is currently Expanded. - Screen reader text to represent the expanded state of a UI control */ +/* Screen reader text to represent the expanded state of a UI control */ "Expanded" = "已展开"; /* Accessibility hint */ @@ -3100,9 +3014,6 @@ translators: Block name. %s: The localized block name */ The action failed */ "Failed" = "失败"; -/* Error title when picked media cannot be imported into stories. */ -"Failed Media Export" = "媒体导出失败"; - /* Message for mark all as read success notice */ "Failed marking Notifications as read" = "未能将通知标为已读"; @@ -3304,6 +3215,12 @@ translators: Block name. %s: The localized block name */ /* An example tag used in the login prologue screens. */ "Football" = "足球"; +/* translators: Recommendation included in a warning related to having blocks deeply nested. */ +"For this reason, we recommend editing the block using the web editor." = "因此,我们建议使用 Web 编辑器编辑该区块。"; + +/* translators: Recommendation included in a warning related to having blocks deeply nested. */ +"For this reason, we recommend editing the block using your web browser." = "因此,我们建议使用 Web 浏览器编辑该区块。"; + /* Register Domain - Domain contact information section header description */ "For your convenience, we have pre-filled your WordPress.com contact information. Please review to be sure it’s the correct information you want to use for this domain." = "为方便起见,我们已预先填充您的 WordPress.com 联系信息。请检查此信息,以确保这是您想在此域上使用的正确信息。"; @@ -3621,8 +3538,7 @@ translators: Block name. %s: The localized block name */ "Home" = "主页"; /* Title for setting which shows the current page assigned as a site's homepage - Title for the homepage section in site settings screen - Title of the Homepage Badge */ + Title for the homepage section in site settings screen */ "Homepage" = "主页"; /* Label for Homepage Settings site settings section @@ -3719,9 +3635,6 @@ translators: Block name. %s: The localized block name */ /* Hint for image title on image settings. */ "Image title" = "图像标题"; -/* Accessibility label for image thumbnails in the media collection view. The parameter is the creation date of the image. */ -"Image, %@" = "图像,%@"; - /* Undated post time label */ "Immediately" = "立即"; @@ -4207,9 +4120,6 @@ translators: Block name. %s: The localized block name */ Settings: Comments Approval settings */ "Links in comments" = "评论中的链接"; -/* The accessibility label for the list style button in the Post List. */ -"List style" = "列表样式"; - /* Title of the screen that load selected the revisions. */ "Load" = "加载"; @@ -4225,18 +4135,12 @@ translators: Block name. %s: The localized block name */ /* Text displayed while loading the activity feed for a site */ "Loading Backups..." = "正在加载备份…"; -/* Phrase to show when the user has searched for GIFs and they are being loaded. */ -"Loading GIFs..." = "正在加载 GIF..."; - /* Menus label text displayed while menus are loading */ "Loading Menus..." = "正在加载菜单..."; /* Text displayed while loading site People. */ "Loading People..." = "正在加载人员信息…"; -/* Phrase to show when the user has searched for images and they are being loaded. */ -"Loading Photos..." = "正在加载照片..."; - /* Text displayed while loading plans details */ "Loading Plan..." = "正在加载套餐..."; @@ -4297,8 +4201,7 @@ translators: Block name. %s: The localized block name */ /* Local Services site intent topic */ "Local Services" = "本地服务"; -/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. - Title of the Local Changes Badge */ +/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. */ "Local changes" = "本地更改"; /* Title for alert when a generic error happened when trying to find the location of the device */ @@ -4462,7 +4365,6 @@ translators: Block name. %s: The localized block name */ "Max Video Upload Size" = "视频上传尺寸上限"; /* Accessibility label for the Me button in My Site. - Label for the post author filter. This filter shows posts only authored by the current user. Me page title Noun. Title. Links to the Me screen. The accessibility value of the me tab. @@ -4470,9 +4372,7 @@ translators: Block name. %s: The localized block name */ "Me" = "我"; /* Noun. Title. Links to the blog's Media library. - Tab bar title for the Media tab in Media Picker The menu item to select during a guided tour. - Title for Media Library section of the app. Title for the media section in site settings screen Title label for the media settings section in the app settings */ "Media" = "媒体"; @@ -4484,13 +4384,6 @@ translators: Block name. %s: The localized block name */ /* Label for size of media cache in the app. */ "Media Cache Size" = "媒体缓存大小"; -/* Title for alert when access to media capture is not granted */ -"Media Capture" = "媒体拍摄"; - -/* Title for alert when a generic error happened when loading media - Title for alert when access to the media library is not granted by the user */ -"Media Library" = "媒体库"; - /* Title for action sheet with media options. */ "Media Options" = "媒体选项"; @@ -4513,9 +4406,6 @@ translators: Block name. %s: The localized block name */ /* translators: %s: block title e.g: \"Paragraph\". */ "Media options" = "媒体选项"; -/* Alert title when there is issues loading an asset to preview. */ -"Media preview failed." = "媒体预览加载失败。"; - /* Alert displayed to the user when multiple media items have uploaded successfully. */ "Media uploaded (%ld files)" = "已上传媒体(%ld 个文件)"; @@ -4553,9 +4443,6 @@ translators: Block name. %s: The localized block name */ Label for the share message field on the post settings. */ "Message" = "邮件"; -/* Title of section containing image / video metadata such as size and file type */ -"Metadata" = "元数据"; - /* Option to select the Microsft Outlook app when logging in with magic links */ "Microsoft Outlook" = "Microsoft Outlook"; @@ -4575,13 +4462,11 @@ translators: Block name. %s: The localized block name */ "Months and Years" = "月和年"; /* Accessibility label for more button in dashboard quick start card. - Accessibility label for the More button in Page List. Accessibility label for the More button in Post List (compact view). Accessibility label for the More button on formatting toolbar. Accessibility label for the More button on Reader Cell Accessibility label for the More button on Reader's post details - Action button to display more available options - Label for the more post button. Tapping displays an action sheet with post options. */ + Action button to display more available options */ "More" = "更多"; /* Action button to display more available options @@ -4639,15 +4524,8 @@ translators: Block name. %s: The localized block name */ /* Screen reader text for button that will move the menu item */ "Move menu item" = "移动菜单项"; -/* Label for a button that moves a page to the draft folder - Label for an option that moves a post to the draft folder */ -"Move to Draft" = "移动到“草稿”"; - -/* Label for a button that moves a page to the trash folder - Label for a option that moves a post to the trash folder - Title for button on the comment details page that moves the comment to trash when tapped. +/* Title for button on the comment details page that moves the comment to trash when tapped. Trash option in the trash confirmation alert. - Trash option in the trash page confirmation alert. Trashes the comment */ "Move to Trash" = "移动到回收站"; @@ -4679,7 +4557,8 @@ translators: Block name. %s: The localized block name */ Title of My Site tab */ "My Site" = "我的站点"; -/* Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ +/* Title for site picker screen. + Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ "My Sites" = "我的站点"; /* Siri Suggestion to open My Sites */ @@ -4929,9 +4808,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title for the view when there aren't any Activities to display in the Activity Log for a given filter. */ "No matching events found." = "未发现符合条件的事件。"; -/* Message displayed when no results are returned from a media library search. Should match Calypso. - Phrase to show when the user search for images but there are no result to show. - Phrase to show when the user searches for GIFs but there are no result to show. */ +/* Message displayed when no results are returned from a media library search. Should match Calypso. */ "No media matching your search" = "没有与您的搜索匹配的媒体"; /* Menus text shown when no menus were available for loading the Menus editor. */ @@ -4949,8 +4826,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Displayed in the Notifications Tab as a title, when there are no notifications */ "No notifications yet" = "尚无通知"; -/* Displayed when the user is searching the pages list and there are no matching pages - Text displayed when there's no matching with the text search */ +/* Text displayed when there's no matching with the text search */ "No pages matching your search" = "没有与您的搜索匹配的页面"; /* Text displayed when search for plugins returns no results */ @@ -4971,9 +4847,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Message shown whent the reader finds no posts for the chosen tag */ "No posts have been made recently with this tag." = "没有与此标签相关的近期文章。"; -/* Displayed when the user is searching the posts list and there are no matching posts */ -"No posts matching your search" = "没有与您的搜索匹配的文章"; - /* Accessibility value for a Stats' Posting Activity Month if there are no posts. */ "No posts." = "无文章。"; @@ -5074,9 +4947,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* A message title */ "Nothing liked yet" = "尚无点赞内容"; -/* Default message for empty media picker */ -"Nothing to show" = "没有可显示的内容"; - /* Notifications Details Accessibility Identifier */ "Notification Details Table" = "通知详细信息表"; @@ -5136,7 +5006,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Alert dismissal title Button title. Acknowledges a prompt. Button title. An acknowledgement of the message displayed in a prompt. - Confirmation of action Default action Dismisses the alert Marks all notifications as read. @@ -5198,9 +5067,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* No comment provided by engineer. */ "Only show excerpt" = "仅显示摘录"; -/* Message telling the user that they've only enabled limited photo library permissions for the app. */ -"Only the selected photos you've given access to are available." = "仅可访问您允许访问的照片。"; - /* An informal exclaimation that means `something went wrong`. Title for the view when there's an error loading a comment. Title for the view when there's an error loading Activity Log @@ -5235,9 +5101,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title of a button that opens the apps settings in the system Settings.app */ "Open Settings" = "打开设置"; -/* Editor button to swich the media picker from quick mode to full picker */ -"Open full media picker" = "打开完整媒体选择器"; - /* No comment provided by engineer. */ "Open in Safari" = "用 Safari 打开"; @@ -5277,6 +5140,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Divider on initial auth view separating auth options. */ "Or" = "或"; +/* Instruction text for other forms of two-factor auth methods. */ +"Or choose another form of authentication." = "或者选择另一种身份验证形式。"; + /* Label for button to log in using site address. Underscores _..._ denote underline. */ "Or log in by _entering your site address_." = "或通过 _输入您的站点地址_ 登录。"; @@ -5335,15 +5201,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title shown when selecting a post type of Page from the Share Extension. */ "Page" = "页面"; -/* Prompts the user that a restored page was moved to the drafts list. */ -"Page Restored to Drafts" = "页面已恢复至草稿列表"; - -/* Prompts the user that a restored page was moved to the published list. */ -"Page Restored to Published" = "页面已恢复至已发布列表"; - -/* Prompts the user that a restored page was moved to the scheduled list. */ -"Page Restored to Scheduled" = "页面已恢复至预发布列表"; - /* Name of the button to open the page settings The title of the Page Settings screen. */ "Page Settings" = "页面设置"; @@ -5360,9 +5217,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of notification displayed when a page has failed to upload. */ "Page failed to upload" = "页面上传失败"; -/* A short message explaining that a page was moved to the trash bin. */ -"Page moved to trash." = "已将页面移动到回收站。"; - /* Title of notification displayed when a page has been successfully saved as a draft. */ "Page pending review" = "页面待审核"; @@ -5434,8 +5288,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title of pending Comments filter. */ "Pending" = "待审"; -/* Name for the status of a post pending review. - Title of the Pending Review Badge */ +/* Name for the status of a post pending review. */ "Pending review" = "等待复审"; /* Noun. Title of the people management feature. @@ -5464,12 +5317,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Photography site intent topic */ "Photography" = "摄影"; -/* Tab bar title for the Photos tab in Media Picker */ -"Photos" = "照片"; - -/* Subtitle for placeholder in Free Photos. The company name 'Pexels' should always be written as it is. */ -"Photos provided by Pexels" = "Pexels 提供的照片"; - /* Title for selecting a new username in the site creation flow. */ "Pick username" = "选择用户名"; @@ -5562,7 +5409,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Please enter the password for your WordPress.com account to log in with your Apple ID." = "请输入您的 WordPress.com 账户密码,以使用 Apple ID 登录。"; /* Instruction text on the two-factor screen. */ -"Please enter the verification code from your authenticator app, or tap the link below to receive a code via SMS." = "请输入验证器应用程序中的验证码,或轻点以下链接以通过短信接收验证码。"; +"Please enter the verification code from your authenticator app." = "请输入验证器应用程序中的验证码。"; /* Popup message to ask for user credentials (fields shown below). */ "Please enter your credentials" = "请输入你的验证信息"; @@ -5657,15 +5504,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ The post formats available for the post. Should be the same as in core WP. */ "Post Format" = "文章格式"; -/* Prompts the user that a restored post was moved to the drafts list. */ -"Post Restored to Drafts" = "已将文章还原到“草稿”"; - -/* Prompts the user that a restored post was moved to the published list. */ -"Post Restored to Published" = "已将文章还原到已发布"; - -/* Prompts the user that a restored post was moved to the scheduled list. */ -"Post Restored to Scheduled" = "已将文章还原到预发布"; - /* Name of the button to open the post settings The title of the Post Settings screen. */ "Post Settings" = "文章设置"; @@ -5685,9 +5523,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of notification displayed when a post has failed to upload. */ "Post failed to upload" = "文章上传失败"; -/* A short message explaining that a post was moved to the trash bin. */ -"Post moved to trash." = "已将文章移动到回收站。"; - /* Title of notification displayed when a post has been successfully saved as a draft. */ "Post pending review" = "文章待审核"; @@ -5746,9 +5581,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Period Stats 'Posts and Pages' header */ "Posts and Pages" = "文章和页面"; -/* Title of the Posts Page Badge */ -"Posts page" = "文章页面"; - /* Message informing the user that their static homepage for posts was set successfully */ "Posts page successfully updated" = "已成功更新文章页面"; @@ -5761,9 +5593,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* A message explaining the Posts I Like feature in the reader */ "Posts that you like will appear here." = "您喜欢的文章将在此处显示。"; -/* Subtitle for placeholder in Tenor picker. `The company name 'Tenor' should always be written as it is. */ -"Powered by Tenor" = "由 Tenor 提供支持"; - /* Browse premium themes selection title */ "Premium" = "付费"; @@ -5782,18 +5611,12 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title for screen to preview a static content. */ "Preview" = "预览"; -/* Action for Media Picker to preview the selected media items. The argument in the string represents the number of elements (as numeric digits) selected */ -"Preview %@" = "预览 %@"; - /* Title for web preview device switching button */ "Preview Device" = "预览设备"; /* Title on display preview error */ "Preview Unavailable" = "预览不可用"; -/* Accessibility label for media item preview for user's viewing an item in their media library */ -"Preview media" = "预览媒体"; - /* No comment provided by engineer. */ "Preview page" = "预览页面"; @@ -5840,8 +5663,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Privacy notice for California users" = "面向加州用户的隐私声明"; /* Name for the status of a post that is marked private. - Privacy setting for posts set to 'Private'. Should be the same as in core WP. - Title of the Private Badge */ + Privacy setting for posts set to 'Private'. Should be the same as in core WP. */ "Private" = "私密"; /* No comment provided by engineer. */ @@ -5891,12 +5713,10 @@ translators: %s: Select control button label e.g. \"Button width\" */ Label for the publish date button. */ "Publish Date" = "发布日期"; -/* A short phrase indicating a post is due to be immedately published. - Label for a button that moves a page to the published folder, publishing with the current date/time. */ +/* A short phrase indicating a post is due to be immedately published. */ "Publish Immediately" = "立即发布"; /* Label for a button that publishes the post - Label for an option that moves a publishes a post immediately Title of button allowing the user to immediately publish the post they are editing. */ "Publish Now" = "立即发布"; @@ -5914,8 +5734,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Name for the status of a published post. Period Stats 'Published' header - Title of the published filter. This filter shows a list of posts that the user has published. - Title of the published header in search list. */ + Title of the published filter. This filter shows a list of posts that the user has published. */ "Published" = "已发布"; /* Precedes the name of the blog just posted on */ @@ -6057,8 +5876,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of the completion screen of the Blogging Reminders Settings screen when the reminders are removed. */ "Reminders removed" = "删除提醒"; -/* Add asset to media picker list - Alert button to confirm a plugin to be removed +/* Alert button to confirm a plugin to be removed Button label when removing a blog Label for the Remove Feature Image icon. Tapping will show a confirmation screen for removing the feature image from the post. Remove Action @@ -6211,9 +6029,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of secondary button on alert prompting verify their accounts while attempting to publish */ "Resend" = "重新发送"; -/* Title of the reset button */ -"Reset" = "重设"; - /* Accessibility label for the reset activity type button */ "Reset Activity Type filter" = "重置活动类型过滤器"; @@ -6268,12 +6083,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ Button title, displayed when media has failed to upload. Allows the user to try the upload again. Button title. Retries uploading a post. If a user taps the button with this label, the action that evinced this error view will be retried. - Label for a button that attempts to re-upload a page that previously failed to upload. - Label for the retry post upload button. Tapping attempts to upload the post again. Opens the media library . Retries the upload of a user's post. Retry updating User's Role - Retry uploading the post. Retry. Action Retry. Verb – retry a failed media upload. The Jetpack view button title used when an error occurred @@ -6285,9 +6097,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Button title that triggers a scan */ "Retry Scan" = "重试扫描"; -/* User action to retry media upload. */ -"Retry Upload" = "重试上传"; - /* User action to retry all failed media uploads. */ "Retry all" = "全部重试"; @@ -6385,9 +6194,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Accessibility label for the 'Save Post' button when a post has been saved. */ "Saved Post" = "已保存文章"; -/* Text displayed in HUD when a media item's metadata (title, etc) is saved successfully. */ -"Saved!" = "已保存!"; - /* Accessibility hint for the 'Save Post' button. */ "Saves this post for later." = "保存此文章以供稍后查看。"; @@ -6398,7 +6204,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Saving post…" = "正在保存文章…"; /* Menus save button title while it is saving a Menu. - Text displayed in HUD while a media item's metadata (title, etc) is being saved. Text displayed in HUD while a post is being saved as a draft. */ "Saving..." = "正在保存..."; @@ -6489,21 +6294,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* No comment provided by engineer. */ "Search or type URL" = "搜索或键入 URL"; -/* Text displayed when the search controller will be presented */ -"Search pages" = "搜索页面"; - -/* Text displayed when the search controller will be presented */ -"Search posts" = "搜索文章"; - /* No comment provided by engineer. */ "Search settings" = "搜索设置"; -/* Title for placeholder in Tenor picker */ -"Search to find GIFs to add to your Media Library!" = "搜索查找要添加到您的媒体库中的 GIF!"; - -/* Title for placeholder in Free Photos */ -"Search to find free photos to add to your Media Library!" = "搜索查找要添加到您的媒体库中的免费照片!"; - /* Menus search bar placeholder text. */ "Search..." = "搜索..."; @@ -6574,9 +6367,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Register Domain - Domain contact information field placeholder for Country */ "Select Country" = "选择国家\/地区"; -/* Title of button that allows the user to select more photos to access within the app */ -"Select More" = "选择更多"; - /* Blog Picker's Title */ "Select Site" = "选择站点"; @@ -6598,9 +6388,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Register domain - Title for the Choose domain button of Suggested domains screen */ "Select domain" = "选择域"; -/* Accessibility hint for actions when displaying media items. */ -"Select media." = "选择媒体。"; - /* Accessibility label for selecting paragraph style button on formatting toolbar. */ "Select paragraph style" = "选择段落样式"; @@ -6704,19 +6491,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label for connected service in Publicize stat. */ "Service" = "服务"; -/* Label for a button that opens the Set Parent options view controller - Navigation title displayed on the navigation bar */ +/* Navigation title displayed on the navigation bar */ "Set Parent" = "设置父项"; /* No comment provided by engineer. */ "Set as Featured Image" = "设为特色图片"; -/* Label for a button that sets the selected page as the site's Homepage */ -"Set as Homepage" = "设为主页"; - -/* Label for a button that sets the selected page as the site's Posts page */ -"Set as Posts Page" = "设为文章页面"; - /* Notice confirming that an image has been set as the post's featured image. */ "Set as featured image" = "设为特色图片"; @@ -6760,7 +6540,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Accessibility label for share buttons in nav bars Button label to share a post Button label to share a web page - Share the post. Shares the comment URL Spoken accessibility label Title for a button that allows the user to share their answer to the prompt. @@ -7146,8 +6925,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Name of setting configured when a site uses a static page as its homepage */ "Static Homepage" = "静态主页"; -/* Label for post stats option. Tapping displays statistics for a post. - Noun. Abbreviation of Statistics. Name of the Stats feature +/* Noun. Abbreviation of Statistics. Name of the Stats feature Noun. Abbv. of Statistics. Links to a blog's Stats screen. Stats 3D Touch Shortcut Stats window title @@ -7178,9 +6956,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label text that defines a post marked as sticky */ "Sticky" = "置顶"; -/* Accessibility label for a sticky post in the post list. */ -"Sticky." = "置顶。"; - /* User action to stop upload. */ "Stop upload" = "停止上传"; @@ -7237,7 +7012,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ Theme Support action title */ "Support" = "支持"; -/* Button used to switch site */ +/* Title for back button that leads to the site picker screen. */ "Switch Site" = "转换站点"; /* Switches the Editor to HTML Mode */ @@ -7325,9 +7100,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label explaining why users might want to add tags. */ "Tags help tell readers what a post is about. Separate different tags with commas." = "标签有助于读者了解文章内容。使用英文逗号分隔不同的标签。"; -/* Menu option for taking an image or video with the device's camera. */ -"Take Photo or Video" = "拍照或录像"; - /* No comment provided by engineer. */ "Take a Photo" = "拍照"; @@ -7398,12 +7170,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Accessibility hint */ "Tap to select the previous period" = "点击选择前一个时段"; -/* Accessibility hint for button used to switch site */ -"Tap to switch to another site, or add a new site" = "点击切换到另一站点,或添加一个新站点。"; - -/* Accessibility hint for media item preview for user's viewing an item in their media library */ -"Tap to view media in full screen" = "轻点可全屏查看媒体"; - /* Accessibility hint for a button that opens a new view with more details. */ "Tap to view more details." = "轻点以查看更多详细信息。"; @@ -7449,10 +7215,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* No comment provided by engineer. */ "Text formatting controls are located within the toolbar positioned above the keyboard while editing a text block" = "编辑文本区块时,文本格式控件位于键盘上方的工具栏内"; -/* Button title - The button's title text to send a 2FA code via SMS text message. */ +/* Button title */ "Text me a code instead" = "通过短信向我发送代码"; +/* The button's title text to send a 2FA code via SMS text message. */ +"Text me a code via SMS" = "通过短信将代码发送给我"; + /* Message of alert when theme activation succeeds */ "Thanks for choosing %@ by %@" = "感谢选择 %2$@ 提供的 %1$@"; @@ -7480,9 +7248,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Error message shown to a user who is trying to share to Facebook but does not have any available Facebook Pages. */ "The Facebook connection cannot find any Pages. Publicize cannot connect to Facebook Profiles, only published Pages." = "Facebook 连接无法找到任何页面。Publicize 无法连接到 Facebook 个人资料,只能连接到已发布的页面。"; -/* Message shown when a GIF failed to load while trying to add it to the Media library. */ -"The GIF could not be added to the Media Library." = "无法将 GIF 格式的图片添加到媒体库。"; - /* Description shown when a user logs in with Google but no matching WordPress.com account is found */ "The Google account \"%@\" doesn't match any account on WordPress.com" = "Google 账户“%@”与 WordPress.com 上的任何账户均不匹配"; @@ -7610,7 +7375,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Error message shown when user attempts to remove the site owner. */ "The user you are trying to remove is the owner of this site. Please contact support for assistance." = "您尝试删除的用户是此站点的所有者。请联系支持人员以获取帮助。"; -/* Error message informing a user about an invalid password. */ +/* No comment provided by engineer. */ "The username or password stored in the app may be out of date. Please re-enter your password in the settings and try again." = "已存用户名或密码可能已失效。请在设置中重新输入账户信息。"; /* Message shown when a video failed to load while trying to add it to the Media library. */ @@ -7678,9 +7443,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* A short error message letting the user know about a problem displaying a post. */ "There was a problem displaying this post." = "显示该文章时出现问题。"; -/* Error message displayed when the Media Library is unable to load a full sized preview of an item. */ -"There was a problem loading the media item." = "加载媒体项目时出现问题。"; - /* The loading view subtitle displayed when an error occurred */ "There was a problem loading your data, refresh your page to try again." = "加载数据时出现问题,请刷新页面以重试。"; @@ -7693,9 +7455,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Explaining to the user there was an error trying to obtain the current location of the user. */ "There was a problem when trying to access your location. Please try again later." = "尝试访问您的位置时出错。请稍后重试。"; -/* Explaining to the user there was an generic error accesing media. */ -"There was a problem when trying to access your media. Please try again later." = "尝试访问您的媒体时出现问题。请稍后重试。"; - /* Message for stories unknown error. */ "There was a problem with the Stories editor. If the problem persists you can contact us via the Me > Help & Support screen." = "关注站点时出现问题。如果该问题多次出现,请通过“我 > 帮助与支持”界面与我们联系。"; @@ -7766,9 +7525,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* A description informing the user in order to proceed with this feature we will need camera permissions, and how to enable it. */ "This app needs permission to access the Camera to scan login codes, tap on the Open Settings button to enable it." = "此应用程序需要相机访问权限才能扫描登录代码,点击“打开设置”按钮启用此权限。"; -/* Explaining to the user why the app needs access to the device media library. */ -"This app needs permission to access your device media library in order to add photos and\/or video to your posts. Please change the privacy settings if you wish to allow this." = "此应用程序需要获取设备媒体库的访问权限,才可以向您的文章内添加照片和\/或视频。如要允许此操作,请更改隐私设置。"; - /* No comment provided by engineer. */ "This color combination may be hard for people to read. Try using a brighter background color and\/or a darker text color." = "此颜色搭配可能不便于用户阅读。 不妨尝试使用较浅的背景颜色和\/或较深的文本颜色。"; @@ -7878,6 +7634,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Description of a Quick Start Tour */ "Time to finish setting up your site! Our checklist walks you through the next steps." = "是时候完成站点设置了!我们的清单将指引您进入下一步。"; +/* Error when the uses takes more than 1 minute to submit a security key. */ +"Time's up, but don't worry, your security is our priority. Please try again!" = "时间到了,请别担心,我们会优先保证您的安全。 请重试!"; + /* WordPress.com Marketing Footer Text */ "Tips for getting the most out of WordPress.com." = "有关畅享 WordPress.com 的提示。"; @@ -8001,24 +7760,20 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title for the traffic section in site settings screen */ "Traffic" = "浏览量"; +/* Describes a domain that was transferred from elsewhere to wordpress.com */ +"Transferred Domain" = "已转移的域名"; + /* translators: %s: block title e.g: \"Paragraph\". */ "Transform %s to" = "将%s转换为"; /* No comment provided by engineer. */ "Transform block…" = "转换区块…"; -/* Accessibility label for trash button to delete items from the user's media library - Accessibility label for trash buttons in nav bars +/* Accessibility label for trash buttons in nav bars Trashes a comment Trashes the comment */ "Trash" = "移到回收站"; -/* Accessibility hint for trash button to delete items from the user's media library */ -"Trash selected media" = "将选中的媒体移至回收站"; - -/* Title of the trash page confirmation alert. */ -"Trash this page?" = "是否将此页面放入回收站?"; - /* Title of the trash confirmation alert. */ "Trash this post?" = "是否将此文章放入回收站?"; @@ -8136,9 +7891,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Shown when a user logs in with Google but it subsequently fails to work as login to WordPress.com */ "Unable To Connect" = "无法连接"; -/* An error message. */ -"Unable to Connect" = "无法连接"; - /* Title for stories unknown error. */ "Unable to Create Stories Editor" = "无法创建故事编辑器"; @@ -8154,9 +7906,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* An error message shown when there is an issue creating new invite links. */ "Unable to create new invite links." = "无法创建新的邀请链接。"; -/* Text displayed in HUD if there was an error attempting to delete a group of media items. */ -"Unable to delete all media items." = "无法删除所有媒体项目。"; - /* Text displayed in HUD if there was an error attempting to delete a media item. */ "Unable to delete media item." = "无法删除媒体项目。"; @@ -8220,12 +7969,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Message displayed when sharing a link to the downloadable backup fails. */ "Unable to share link" = "无法共享链接"; -/* Message that appears when a user tries to trash a page while their device is offline. */ -"Unable to trash pages while offline. Please try again later." = "离线时无法将页面放入回收站。请稍后重试。"; - -/* Message that appears when a user tries to trash a post while their device is offline. */ -"Unable to trash posts while offline. Please try again later." = "离线时无法将文章放入回收站。请稍后重试。"; - /* Notice title when turning site notifications off fails. */ "Unable to turn off site notifications" = "无法关闭站点通知"; @@ -8298,8 +8041,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ Button title. Reverts the previous notification operation Revert an operation Revert enabling notification after successfully subcribing to the comments for the post. - The title of an 'undo' button. Tapping the button moves a trashed page out of the trash folder. - The title of an 'undo' button. Tapping the button moves a trashed post out of the trash folder. Undo action */ "Undo" = "撤消"; @@ -8342,9 +8083,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title for Unknown HTML Editor */ "Unknown HTML" = "未知 HTML"; -/* Label to use when creation date from media asset is not know. */ -"Unknown creation date" = "创建日期未知"; - /* No comment provided by engineer. */ "Unknown error" = "未知错误"; @@ -8510,6 +8248,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of a row displayed on the debug screen used to configure the sandbox store use in the App. */ "Use Sandbox Store" = "使用沙盒商店"; +/* The button's title text to use a security key. */ +"Use a security key" = "使用安全密钥"; + /* Option to enable the block editor for new posts */ "Use block editor" = "使用区块编辑器"; @@ -8585,15 +8326,10 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of an alert informing users that the video they are trying to select is not allowed. */ "Video not uploaded" = "视频无法上传"; -/* Accessibility label for video thumbnails in the media collection view. The parameter is the creation date of the video. */ -"Video, %@" = "视频,%@"; - /* Period Stats 'Videos' header */ "Videos" = "视频"; /* Button title. Displays a summary / sharing screen for a specific post. - Label for a button that opens the page when tapped. - Label for the view post button. Tapping displays the post as it appears on the web. Opens the post epilogue screen to allow sharing / viewing of a post. Theme View action title Verb. The screen title shown when viewing a post inside the app. */ @@ -8708,6 +8444,10 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Message shown on screen while waiting for Google to finish its signup process. */ "Waiting for Google to complete…" = "正在等待 Google 完成…"; +/* Text while the webauthn signature is being verified + Text while waiting for a security key challenge */ +"Waiting for security key" = "正在等待安全密钥插入"; + /* View title during the Google auth process. */ "Waiting..." = "正在等待…"; @@ -9082,6 +8822,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* An error message shown when a wpcom user provides the wrong password. */ "Whoops, something went wrong and we couldn't log you in. Please try again!" = "糟糕,出问题了,您无法登录。请重试!"; +/* Generic error on the 2FA screen */ +"Whoops, something went wrong. Please try again!" = "糟糕,出错了。 请重试!"; + +/* Error when the uses chooses an invalid security key on the 2FA screen. */ +"Whoops, that security key does not seem valid. Please try again with another one" = "糟糕,安全密钥似乎无效。 请换个密钥再试一次"; + /* Error message shown when an incorrect two factor code is provided. */ "Whoops, that's not a valid two-factor verification code. Double-check your code and try again!" = "糟糕,双因素验证码无效。请仔细检查代码,然后重试!"; @@ -9109,9 +8855,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Siri Suggestion to open Support */ "WordPress Help" = "WordPress 帮助"; -/* Name for the WordPress Media Library */ -"WordPress Media" = "WordPress 媒体"; - /* Accessibility label for selecting an image or video from the user's WordPress media library on formatting toolbar. */ "WordPress Media Library" = "WordPress 媒体库"; @@ -9426,9 +9169,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Share extension error dialog text. */ "Your account does not have permission to upload media to this site. The Site Administrator can change these permissions." = "您的账户无权将媒体文件上传到此站点。站点管理员可以更改这些权限。"; -/* Explaining to the user why the app needs access to the device media library. */ -"Your app is not authorized to access media library due to active restrictions such as parental controls. Please check your parental control settings in this device." = "因家长控制等活动限制,您的应用程序无权访问媒体库。请检查此设备的家长控制设置。"; - /* Title for the Jetpack Backup Complete message. */ "Your backup is now available for download" = "您的备份现已可供下载"; @@ -9447,9 +9187,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of the site address section in the Domains Dashboard. */ "Your free WordPress.com address is" = "您的免费 WordPress.com 地址是"; -/* Error message when picked media cannot be imported into stories. */ -"Your media could not be exported. If the problem persists you can contact us via the Me > Help & Support screen." = "您的媒体无法导出。如果问题仍然存在,您可以通过 \"我\">\"帮助和支持 \"屏幕与我们联系。"; - /* Details about recently acquired domain on domain credit redemption success screen */ "Your new domain %@ is being set up. It may take up to 30 minutes for your domain to start working." = "您的新域 %@ 正在设置中。 最多可能需花费 30 分钟时间,您的域才能开始运行。"; @@ -9573,8 +9310,38 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* This is the string we display when prompting the user to review the WordPress app */ "appRatings.wordpress.prompt" = "您认为 WordPress 怎么样?"; -/* Label displayed on audio media items. */ -"audio" = "音频"; +/* Option to enable the optimization of images when uploading. */ +"appSettings.media.imageOptimizationRow" = "优化图像"; + +/* Indicates an image will use high quality when uploaded. */ +"appSettings.media.imageQuality.high" = "高"; + +/* Indicates an image will use low quality when uploaded. */ +"appSettings.media.imageQuality.low" = "低"; + +/* Indicates an image will use maximum quality when uploaded. */ +"appSettings.media.imageQuality.maximum" = "最高"; + +/* Indicates an image will use medium quality when uploaded. */ +"appSettings.media.imageQuality.medium" = "中"; + +/* The quality of image used when uploading */ +"appSettings.media.imageQuality.title" = "质量"; + +/* Title for the image quality settings option. */ +"appSettings.media.imageQualityRow" = "图像质量"; + +/* Message of an alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.message" = "优化图像可缩小图像尺寸,以便更快地上传。\n\n默认启用该选项,但是您可以随时在应用程序设置中进行更改。"; + +/* Title of an alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.title" = "继续优化图像?"; + +/* Title of button for turning off image optimization, displayed in the alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.turnOff" = "不,停止优化"; + +/* Title of button for leaving on image optimization, displayed in the alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.turnOn" = "是,继续优化"; /* translators: displays audio file extension. e.g. MP3 audio file */ "audio file" = "音频文件"; @@ -9685,7 +9452,41 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "blogHeader.actionCopyURL" = "复制 URL"; /* Context menu button title */ -"blogHeader.actionOpenInBrowser" = "在浏览器中打开"; +"blogHeader.actionVisitSite" = "查看站点"; + +/* Title for a button that, when tapped, shows more info about participating in Bloganuary. */ +"bloganuary.dashboard.card.button.learnMore" = "了解更多"; + +/* Title for the Bloganuary dashboard card while Bloganuary is running. */ +"bloganuary.dashboard.card.runningTitle" = "Bloganuary 上线了!"; + +/* Title for the Bloganuary dashboard card. */ +"bloganuary.dashboard.card.title" = "Bloganuary 即将上线!"; + +/* Title of a button that calls the user to enable the Blogging Prompts feature. */ +"bloganuary.learnMore.modal.button.promptsDisabled" = "打开博客提示"; + +/* Title of a button that will dismiss the Bloganuary modal when tapped. +Note that the word 'go' here should have a closer meaning to 'start' rather than 'move forward'. */ +"bloganuary.learnMore.modal.button.promptsEnabled" = "开始吧!"; + +/* The second line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.description.second" = "发表您的回复。"; + +/* The third line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.description.third" = "阅读其他博主的回复,获得灵感并建立新的联系。"; + +/* The first line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.descriptions.first" = "每天接收新的提示,以获取灵感。"; + +/* An additional piece of information shown in case the user has the Blogging Prompts feature disabled. */ +"bloganuary.learnMore.modal.footer.addition" = "如需加入 Bloganuary,您需要启用“博客提示”。"; + +/* An informative excerpt shown in a subtler tone. */ +"bloganuary.learnMore.modal.footer.text" = "Bloganuary 将通过“每日博客提示”向您发送一月份的主题。"; + +/* The headline text of the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.headline" = "加入我们为期一个月的写作挑战"; /* Verb. Dismisses the blogging prompt notification. */ "bloggingPrompt.pushNotification.customActionDescription.dismiss" = "忽略"; @@ -9714,6 +9515,12 @@ Example: Comment on Example: Reply to Pamela Nguyen */ "comment.header.subText.reply" = "回复 %1$@"; +/* Error message informing a user about an invalid password. */ +"common.reEnterPasswordMessage" = "应用程序中存储的用户名或密码可能已过期。 请在设置中重新输入密码,然后重试。"; + +/* An error message. */ +"common.unableToConnect" = "无法连接"; + /* Footnote for the privacy compliance popover. */ "compliance.analytics.popover.footnote" = "这些 Cookie 让我们能够收集有关用户与我们网站互动情况的信息,来优化网站性能。"; @@ -9864,50 +9671,92 @@ Example: Reply to Pamela Nguyen */ /* Title for a menu action in the context menu on the Jetpack install card. */ "domain.dashboard.card.menu.hide" = "隐藏此内容"; -/* Domain Purchase Completion footer */ -"domain.purchase.preview.footer" = "您的自定义域名最多可能需要 30 分钟才能开始运行。"; +/* Search domain - Title for the Suggested domains screen */ +"domain.management.addDomain.search.title" = "搜索域名"; -/* Domain Purchase Completion description (only for FREE domains). */ -"domain.purchase.preview.free.description" = "接下来,我们将帮助您做好准备以供浏览。"; +/* Domain management buy domain card button title */ +"domain.management.buy.card.button.title" = "获取域名"; -/* Domain Purchase Completion description (only for PAID domains). */ -"domain.purchase.preview.paid.description" = "我们已通过电子邮件将您的收据发送给您。 接下来,我们将帮助您为每个人做好准备。"; +/* Domain management buy domain card subtitle */ +"domain.management.buy.card.subtitle" = "稍后添加站点。"; -/* Reflects that site is live when domain purchase feature flag is ON. */ -"domain.purchase.preview.title" = "太棒了,您的站点已上线!"; +/* Domain management buy domain card title */ +"domain.management.buy.card.title" = "直接购买域名"; + +/* The expired label of the domain card in All Domains screen. */ +"domain.management.card.expired.label" = "已到期"; + +/* The renews label of the domain card in All Domains screen. */ +"domain.management.card.renews.label" = "续订"; + +/* The empty state button title in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.button.title" = "查找域名"; + +/* The empty state description in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.description" = "轻点下方,查找最适合的域名。"; + +/* The empty state title in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.title" = "您没有任何域名"; + +/* The empty state description in All Domains screen when an error occurs */ +"domain.management.error.empty.state.description" = "加载您的域名时出现错误。 如果问题持续存在,请联系支持人员。"; + +/* The empty state title in All Domains screen when an error occurs */ +"domain.management.error.empty.state.title" = "出错了"; + +/* The empty state button title in All Domains screen when an error occurs */ +"domain.management.error.state.button.title" = "重试"; + +/* The empty state description in All Domains screen when the user is offline */ +"domain.management.offline.empty.state.description" = "请检查您的网络连接,然后重试。"; + +/* The empty state title in All Domains screen when the user is offline */ +"domain.management.offline.empty.state.title" = "未连接互联网"; -/* Status of a domain in `Action Required` state */ -"domain.status.action.required" = "需要采取措施"; +/* Domain management choose site card button title */ +"domain.management.purchase.footer" = "*所有付费年度套餐均包含为期一年的免费域名"; -/* Status of a domain in `Active` state */ -"domain.status.active" = "已启用"; +/* Domain management purchase domain screen title */ +"domain.management.purchase.subtitle" = "别担心,您可以在之后轻松添加站点。"; -/* Status of a domain in `Complete Setup` state */ -"domain.status.complete.setup" = "完成设置"; +/* Domain management purchase domain screen title. */ +"domain.management.purchase.title" = "选择使用域名的方式"; -/* Status of a domain in `Error` state */ -"domain.status.error" = "错误"; +/* The search bar title in All Domains screen. */ +"domain.management.search-bar.title" = "搜索域名"; -/* Status of a domain in `Expired` state */ -"domain.status.expired" = "已到期"; +/* The empty state description in All Domains screen when the are no domains matching the search criteria */ +"domain.management.search.empty.state.description" = "我们找不到任何与您搜索的‘%@’相匹配的域名"; -/* Status of a domain in `Expiring Soon` state */ -"domain.status.expiring.soon" = "即将到期"; +/* The empty state title in All Domains screen when the are no domains matching the search criteria */ +"domain.management.search.empty.state.title" = "未找到匹配的域名"; -/* Status of a domain in `Failed` state */ -"domain.status.failed" = "失败"; +/* Domain management choose site card button title */ +"domain.management.site.card.button.title" = "选择站点"; -/* Status of a domain in `In Progress` state */ -"domain.status.in.progress" = "进行中"; +/* Domain management choose site card subtitle */ +"domain.management.site.card.footer" = "第一年免费使用域名*"; -/* Status of a domain in `Renew` state */ -"domain.status.renew" = "续订"; +/* Domain management choose site card subtitle */ +"domain.management.site.card.subtitle" = "与您已启用的站点搭配使用。"; -/* Status of a domain in `Verify Email` state */ -"domain.status.verify.email" = "验证电子邮件"; +/* Domain management choose site card title */ +"domain.management.site.card.title" = "现有 WordPress.com 站点"; -/* Status of a domain in `Verifying` state */ -"domain.status.verifying" = "正在验证"; +/* Domain Management Screen Title */ +"domain.management.title" = "所有域名"; + +/* Domain Purchase Completion footer */ +"domain.purchase.preview.footer" = "您的自定义域名最多可能需要 30 分钟才能开始运行。"; + +/* Domain Purchase Completion description (only for FREE domains). */ +"domain.purchase.preview.free.description" = "接下来,我们将帮助您做好准备以供浏览。"; + +/* Domain Purchase Completion description (only for PAID domains). */ +"domain.purchase.preview.paid.description" = "我们已通过电子邮件将您的收据发送给您。 接下来,我们将帮助您为每个人做好准备。"; + +/* Reflects that site is live when domain purchase feature flag is ON. */ +"domain.purchase.preview.title" = "太棒了,您的站点已上线!"; /* The 'Best Alternative' label under the domain name in 'Choose a domain' screen */ "domain.suggestions.row.best-alternative" = "更好的替代方案"; @@ -9930,12 +9779,24 @@ Example: Reply to Pamela Nguyen */ /* The text to display for paid domains in 'Site Creation > Choose a domain' screen */ "domain.suggestions.row.yearly" = "\/年"; +/* Title for the checkout screen. */ +"domains.checkout.title" = "结账"; + /* Action shown in a bottom notice to dismiss it. */ "domains.failure.dismiss" = "忽略"; /* Content show when the domain selection action fails. */ "domains.failure.title" = "抱歉,您目前无法在 Jetpack 上购买您要添加的域名。"; +/* Title for the screen where the user can choose how to use the domain they're end up purchasing. */ +"domains.purchase.choice.title" = "购买域名"; + +/* Back button title that navigates back to the search domains screen. */ +"domains.search.backButton.title" = "搜索"; + +/* Title of screen where user chooses a site to connect to their selected domain */ +"domains.sitePicker.title" = "选择站点"; + /* No comment provided by engineer. */ "double-tap to change unit" = "双击更改单位"; @@ -9953,6 +9814,15 @@ Example: Reply to Pamela Nguyen */ Site Address placeholder */ "example.com" = "example.com"; +/* Title for confirmation navigation bar button item */ +"externalMediaPicker.add" = "添加"; + +/* Bottom toolbar title in the selection mode */ +"externalMediaPicker.toolbarSelectItemsPrompt" = "选择图片"; + +/* Bottom toolbar title in the selection mode */ +"externalMediaPicker.toolbarViewSelected" = "查看已选项 (%@)"; + /* Title of screen the displays the details of an advertisement campaign. */ "feature.blaze.campaignDetails.title" = "活动详细信息"; @@ -10052,9 +9922,6 @@ Example: Reply to Pamela Nguyen */ /* (placeholder) Help the user enter a URL into the field */ "http:\/\/my-site-address (URL)" = "http:\/\/我的站点 (URL)"; -/* Label displayed on image media items. */ -"image" = "图像"; - /* Sentence to justify why the app is asking permission from the user to use their camera. */ "infoplist.NSCameraUsageDescription" = "拍摄要在您的文章内使用的照片或视频。"; @@ -10355,6 +10222,9 @@ Example: Reply to Pamela Nguyen */ /* Indicating that referrer was marked as spam */ "marked as spam" = "标记为垃圾内容"; +/* Products header text in Me Screen. */ +"me.products.header" = "产品"; + /* Title of error prompt shown when a sync fails. */ "media.syncFailed" = "无法同步媒体"; @@ -10367,18 +10237,24 @@ Example: Reply to Pamela Nguyen */ /* Message of an alert informing users that the video they are trying to select is not allowed. */ "mediaExporter.videoLimitExceededError" = "上传超过 5 分钟的视频需要使用付费套餐。"; -/* Verb. User action to dismiss error alert when failing to load media item. */ -"mediaItemTable.errorAlert.dismissButton" = "忽略"; - /* Accessibility hint for add button to add items to the user's media library */ "mediaLibrary.addButtonAccessibilityHint" = "添加新媒体"; /* Accessibility label for add button to add items to the user's media library */ "mediaLibrary.addButtonAccessibilityLabel" = "添加"; +/* Button name in the more menu */ +"mediaLibrary.aspectRatioGrid" = "长宽比网格"; + +/* Context menu button */ +"mediaLibrary.buttonDelete" = "删除"; + /* Media screen navigation bar button Select title */ "mediaLibrary.buttonSelect" = "选择"; +/* Context menu button */ +"mediaLibrary.buttonShare" = "共享"; + /* Verb. Button title. Tapping cancels an action. */ "mediaLibrary.deleteConfirmationCancel" = "取消"; @@ -10400,6 +10276,21 @@ Example: Reply to Pamela Nguyen */ /* Text displayed in HUD after successfully deleting a media item */ "mediaLibrary.deletionSuccessMessage" = "已删除!"; +/* The name of the media filter */ +"mediaLibrary.filterAll" = "所有"; + +/* The name of the media filter */ +"mediaLibrary.filterAudio" = "音频"; + +/* The name of the media filter */ +"mediaLibrary.filterDocuments" = "文档"; + +/* The name of the media filter */ +"mediaLibrary.filterImages" = "图像"; + +/* The name of the media filter */ +"mediaLibrary.filterVideos" = "视频"; + /* User action to delete un-uploaded media. */ "mediaLibrary.retryOptionsAlert.delete" = "删除"; @@ -10412,6 +10303,12 @@ Example: Reply to Pamela Nguyen */ /* Message displayed when no results are returned from a media library search. Should match Calypso. */ "mediaLibrary.searchResultsEmptyTitle" = "没有与您的搜索匹配的媒体"; +/* Text displayed in HUD if there was an error attempting to share a group of media items. */ +"mediaLibrary.sharingFailureMessage" = "无法共享选定项目。"; + +/* Button name in the more menu */ +"mediaLibrary.squareGrid" = "方形网格"; + /* Media screen navigation title */ "mediaLibrary.title" = "媒体"; @@ -10433,6 +10330,12 @@ Example: Reply to Pamela Nguyen */ /* The title of the button to dismiss the alert shown when the picked media cannot be imported into stories. */ "mediaPicker.failedMediaExportAlert.dismissButton" = "忽略"; +/* Error message when picked media cannot be imported into stories. */ +"mediaPicker.failedMediaExportAlert.message" = "无法导出您的媒体。 如果问题仍然存在,您可以通过“我 > 帮助与支持”界面与我们联系。"; + +/* Error title when picked media cannot be imported into stories. */ +"mediaPicker.failedMediaExportAlert.title" = "媒体导出失败"; + /* Message for alert when access to camera is not granted */ "mediaPicker.noCameraAccessMessage" = "此应用程序需要获取照相机的访问权限,才可以拍摄新媒体。如要允许此操作,请更改隐私设置。"; @@ -10466,6 +10369,12 @@ Example: Reply to Pamela Nguyen */ /* The name of the action in the context menu */ "mediaPicker.takeVideo" = "拍摄视频"; +/* Navigation title for media preview. Example: 1 of 3 */ +"mediaPreview.NofM" = "第 %1$@ 步,共 %2$@ 步"; + +/* Max image size in pixels (e.g. 300x300px) */ +"mediaSizeSlider.valueFormat" = "%1$d × %2$d 像素"; + /* The description in the Delete WordPress screen */ "migration.deleteWordpress.description" = "似乎您仍然安装了 WordPress 应用。"; @@ -10478,9 +10387,6 @@ Example: Reply to Pamela Nguyen */ /* The title in the Delete WordPress screen */ "migration.deleteWordpress.title" = "您的设备上不再需要 WordPress 应用"; -/* Primary button title in the migration done screen. */ -"migration.done.actions.primary.title" = "完成"; - /* Footer for the migration done screen. */ "migration.done.footer" = "我们建议您卸载设备上的 WordPress 应用以避免发生数据冲突。"; @@ -10490,6 +10396,9 @@ Example: Reply to Pamela Nguyen */ /* Primary description in the migration done screen. */ "migration.done.primaryDescription" = "我们已转移您的所有数据和设置。 一切都完好如初。"; +/* Secondary description (second paragraph) in the migration done screen. */ +"migration.done.secondaryDescription" = "是时候在 Jetpack 应用程序上继续您的 WordPress 之旅了!"; + /* Title of the migration done screen. */ "migration.done.title" = "感谢您切换至 Jetpack!"; @@ -10538,6 +10447,9 @@ Example: Reply to Pamela Nguyen */ /* The title in the migration welcome screen */ "migration.welcome.title" = "欢迎使用 Jetpack!"; +/* Primary button title in the migration done screen. */ +"migrationDone.actions.primaryTitle" = "开始使用"; + /* Description for the static screen displayed prompting users to switch the Jetpack app. */ "movedToJetpack.description" = "Jetpack 应用拥有 WordPress 应用的所有功能,现在可以独家访问统计信息、阅读器、通知等功能。"; @@ -10613,6 +10525,30 @@ Example: Reply to Pamela Nguyen */ /* Message title for when a user has no sites. */ "mySite.noSites.title" = "您没有任何站点"; +/* Menu title for the add site option */ +"mySite.siteActions.addSite" = "添加站点"; + +/* Button that reveals more site actions */ +"mySite.siteActions.button" = "站点操作"; + +/* Accessibility hint for button used to show more site actions */ +"mySite.siteActions.hint" = "轻点以显示更多站点操作"; + +/* Menu title for the personalize home option */ +"mySite.siteActions.personalizeHome" = "对首页进行个性化设置"; + +/* Menu title for the change site icon option */ +"mySite.siteActions.siteIcon" = "更改站点图标"; + +/* Menu title for the change site title option */ +"mySite.siteActions.siteTitle" = "更改站点标题"; + +/* Menu title for the switch site option */ +"mySite.siteActions.switchSite" = "切换站点"; + +/* Menu title for the visit site option */ +"mySite.siteActions.visitSite" = "查看站点"; + /* Dismiss button title. */ "noResultsViewController.dismissButton" = "忽略"; @@ -10628,14 +10564,20 @@ Example: Reply to Pamela Nguyen */ /* This is one of the buttons we display when prompting the user for a review */ "notifications.appRatings.sendFeedback.yes.buttonTitle" = "发送反馈"; -/* Word separating the current index from the total amount. I.e.: 7 of 9 */ -"of" = "\/"; +/* Badge for page cells */ +"pageList.badgeHomepage" = "主页"; + +/* Badge for page cells */ +"pageList.badgeLocalChanges" = "局部变更"; -/* Label displayed on media items that are not video, image, or audio. */ -"other" = "其他"; +/* Badge for page cells */ +"pageList.badgePendingReview" = "待审核"; -/* Promote the page with Blaze. */ -"pages.blaze.actionTitle" = "大力宣传并推广"; +/* Badge for page cells */ +"pageList.badgePosts" = "文章页面"; + +/* Badge for page cells */ +"pageList.badgePrivate" = "私人"; /* Subtitle of the theme template homepage cell */ "pages.template.subtitle" = "您的主页正在使用主题模板,并将在 Web 编辑器中打开。"; @@ -10643,6 +10585,36 @@ Example: Reply to Pamela Nguyen */ /* Title of the theme template homepage cell */ "pages.template.title" = "主页"; +/* Message informing the user that their static homepage page was set successfully */ +"pages.updatePage.successTitle" = "页面更新成功"; + +/* Delete option in the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.actionTitle" = "永久删除"; + +/* Message of the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.alertMessage" = "确定要永久删除此页面吗?"; + +/* Title of the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.alertTitle" = "是否永久删除?"; + +/* Menu option for filtering posts by everyone */ +"pagesList.pagesByEveryone" = "所有人的页面"; + +/* Menu option for filtering posts by me */ +"pagesList.pagesByMe" = "我的页面"; + +/* Trash option in the trash page confirmation alert. */ +"pagesList.trash.actionTitle" = "移至回收站"; + +/* Message of the trash page confirmation alert. */ +"pagesList.trash.alertMessage" = "确定要将此页面放入回收站吗?"; + +/* Title of the trash page confirmation alert. */ +"pagesList.trash.alertTitle" = "是否将此页面放入回收站?"; + +/* Cancels an Action */ +"pagesList.trash.cancel" = "取消"; + /* No comment provided by engineer. */ "password" = "密码"; @@ -10682,6 +10654,51 @@ Example: Reply to Pamela Nguyen */ /* Register Domain - Domain contact information field Phone */ "phone number" = "电话号码"; +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.createdTimeAgo" = "已创建 %@"; + +/* Status mesasge for post cells */ +"post.deletingPostPermanentlyStatusMessage" = "正在删除文章…"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.editedTimeAgo" = "已编辑 %@"; + +/* Status mesasge for post cells */ +"post.movingToTrashStatusMessage" = "正在将文章移至回收站..."; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.publishedTimeAgo" = "已发布 %@"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.scheduledForDate" = "已预发布 %@"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.trashedTimeAgo" = "已移至回收站 %@"; + +/* Accessibility label for the post author in the post list. The parameter is the author name. For example, \"By Elsa.\" */ +"postList.a11y.authorChunkFormat" = "作者是 %@。"; + +/* Accessibility label for a post's excerpt in the post list. The parameter is the post excerpt. For example, \"Excerpt. This is the first paragraph.\" */ +"postList.a11y.exerptChunkFormat" = "摘录。%@。"; + +/* Accessibility label for a sticky post in the post list. */ +"postList.a11y.sticky" = "置顶。"; + +/* Accessibility label for a post in the post list. The first placeholder is the post title. The second placeholder is the date. */ +"postList.a11y.titleAndDateChunkFormat" = "%1$@,%2$@。"; + +/* Title for the 'Trash' post list row swipe action */ +"postList.swipeActionDelete" = "回收站"; + +/* Title for the 'Delete' post list row swipe action */ +"postList.swipeActionDeletePermanently" = "删除"; + +/* Title for the 'Share' post list row swipe action */ +"postList.swipeActionShare" = "共享"; + +/* Title for the 'View' post list row swipe action */ +"postList.swipeActionView" = "查看"; + /* User action to dismiss featured media options. */ "postSettings.featuredImageUploadActionSheet.dismiss" = "忽略"; @@ -10700,9 +10717,84 @@ Example: Reply to Pamela Nguyen */ /* Button in Post Settings */ "postSettings.setFeaturedImageButton" = "设置特色图片"; +/* Error message on post/page settings screen */ +"postSettings.updateFailedMessage" = "更新文章设置失败"; + /* Promote the post with Blaze. */ "posts.blaze.actionTitle" = "大力宣传并推广"; +/* Label for the Post List option that cancels automatic uploading of a post. */ +"posts.cancelUpload.actionTitle" = "取消上传"; + +/* Label for post comments option. Tapping displays comments for a post. */ +"posts.comments.actionTitle" = "评论"; + +/* Label for the delete post option. Tapping permanently deletes a post. */ +"posts.delete.actionTitle" = "永久删除"; + +/* Label for an option that moves a post to the draft folder */ +"posts.draft.actionTitle" = "移动到草稿"; + +/* Label for post duplicate option. Tapping creates a copy of the post. */ +"posts.duplicate.actionTitle" = "重复项"; + +/* Opens a submenu for page attributes. */ +"posts.pageAttributes.actionTitle" = "页面属性"; + +/* Label for the preview post button. Tapping displays the post as it appears on the web. */ +"posts.preview.actionTitle" = "预览"; + +/* Label for an option that moves a publishes a post immediately */ +"posts.publish.actionTitle" = "立即发布"; + +/* Retry uploading the post. */ +"posts.retry.actionTitle" = "重试"; + +/* Set the selected page as the homepage. */ +"posts.setHomepage.actionTitle" = "设为主页"; + +/* Set the parent page for the selected page. */ +"posts.setParent.actionTitle" = "设置父项"; + +/* Set the selected page as a posts page. */ +"posts.setPostsPage.actionTitle" = "设为文章页面"; + +/* Set the selected page as a regular page. */ +"posts.setRegularPage.actionTitle" = "设为常规页面"; + +/* Label for post settings option. Tapping displays settings for a post. */ +"posts.settings.actionTitle" = "设置"; + +/* Share the post. */ +"posts.share.actionTitle" = "共享"; + +/* Label for post stats option. Tapping displays statistics for a post. */ +"posts.stats.actionTitle" = "统计数据"; + +/* Label for a option that moves a post to the trash folder */ +"posts.trash.actionTitle" = "移至回收站"; + +/* Label for the view post button. Tapping displays the post as it appears on the web. */ +"posts.view.actionTitle" = "查看"; + +/* A short message explaining that a page was deleted permanently. */ +"postsList.deletePage.message" = "页面已永久删除"; + +/* A short message explaining that a post was deleted permanently. */ +"postsList.deletePost.message" = "文章已永久删除"; + +/* A short message explaining that a page was moved to the trash bin. */ +"postsList.movePageToTrash.message" = "页面已移至回收站"; + +/* A short message explaining that a post was moved to the trash bin. */ +"postsList.movePostToTrash.message" = "文章已移至回收站"; + +/* Menu option for filtering posts by everyone */ +"postsList.postsByEveryone" = "所有人的文章"; + +/* Menu option for filtering posts by me */ +"postsList.postsByMe" = "我的文章"; + /* Title for the button to subscribe to Jetpack Social on the remaining shares view */ "postsettings.social.remainingshares.subscribe" = "立即订阅以共享更多"; @@ -10847,13 +10939,15 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Text for the 'Like' button on the reader post card cell. */ "reader.post.button.like" = "点赞"; -/* Accessibility hint for the like button on the reader post card cell - Accessibility hint for the liked button on the reader post card cell */ +/* Accessibility hint for the like button on the reader post card cell */ "reader.post.button.like.accessibility.hint" = "点赞此文章。"; /* Text for the 'Liked' button on the reader post card cell. */ "reader.post.button.liked" = "已点赞"; +/* Accessibility hint for the liked button on the reader post card cell */ +"reader.post.button.liked.accessibility.hint" = "取消点赞该文章。"; + /* Accessibility hint for the site header on the reader post card cell */ "reader.post.button.menu.accessibility.hint" = "打开可进行更多操作的菜单。"; @@ -10917,6 +11011,12 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Title for the tooltip anchor. */ "readerDetail.tooltipAnchorTitle.accessibilityLabel" = "新建"; +/* The button title for the transfer footer view in Register Domain screen */ +"register.domain.transfer.button.title" = "转移域名"; + +/* The title for the transfer footer view in Register Domain screen */ +"register.domain.transfer.title" = "想要转移已有的域名?"; + /* Information of what related post are and how they are presented */ "relatedPostsSettings.optionsFooter" = "“相关文章”在您的文章下方显示您站点中的相关内容。"; @@ -11016,6 +11116,21 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Accessibility hint for actions when displaying media items. */ "siteMedia.cellAccessibilityHint" = "选择媒体。"; +/* Accessibility hint for media item preview for user's viewing an item in their media library */ +"siteMediaItem.contentViewAccessibilityHint" = "轻点可全屏查看媒体"; + +/* Accessibility label for media item preview for user's viewing an item in their media library */ +"siteMediaItem.contentViewAccessibilityLabel" = "预览媒体"; + +/* Title for confirmation navigation bar button item */ +"siteMediaPicker.add" = "添加"; + +/* Button selection media in media picker */ +"siteMediaPicker.deselect" = "取消选择"; + +/* Button selection media in media picker */ +"siteMediaPicker.select" = "选择"; + /* Media screen navigation title */ "siteMediaPicker.title" = "媒体"; @@ -11023,7 +11138,7 @@ Example: given a notice format "Following %@" and empty site name, this will be "siteSettings.privacy.title" = "隐私"; /* Hint for users when hidden privacy setting is set */ -"siteVisibility.hidden.hint" = "您的站点对所有人均可见,但要求搜索引擎不要对您的站点进行索引。"; +"siteVisibility.hidden.hint" = "在站点可以查看之前,访客只能看到“即将推出”的通知。"; /* Text for privacy settings: Hidden */ "siteVisibility.hidden.title" = "已隐藏"; @@ -11184,6 +11299,12 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Dismiss the AlertView */ "stockPhotos.strings.dismiss" = "忽略"; +/* Subtitle for placeholder in Free Photos. The company name 'Pexels' should always be written as it is. */ +"stockPhotos.subtitle" = "Pexels 提供的照片"; + +/* Title for placeholder in Free Photos */ +"stockPhotos.title" = "搜索查找要添加到您的媒体库中的免费照片!"; + /* Section title for prominent suggestions */ "suggestions.section.prominent" = "此会话中"; @@ -11331,6 +11452,9 @@ Example: given a notice format "Following %@" and empty site name, this will be /* View title for Help & Support page. */ "support.title" = "帮助"; +/* Title for placeholder in Tenor picker */ +"tenor.welcomeMessage" = "搜索查找要添加到您的媒体库中的 GIF!"; + /* Header of delete screen section listing things that will be deleted. */ "these items will be deleted:" = "即将删除以下项目:"; @@ -11346,9 +11470,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Displayed in the confirmation alert when marking unread notifications as read. */ "unread" = "未读"; -/* Label displayed on video media items. */ -"video" = "视频"; - /* Portion of a message for Jetpack users that have multisite WP installation, thus Restore is not available. This part is a link, colored with a different color. */ "visit our documentation page" = "访问我们的文档页面"; diff --git a/WordPress/Resources/zh-Hant.lproj/Localizable.strings b/WordPress/Resources/zh-Hant.lproj/Localizable.strings index 9cb5e39a2d35..0007f3b3cc7f 100644 --- a/WordPress/Resources/zh-Hant.lproj/Localizable.strings +++ b/WordPress/Resources/zh-Hant.lproj/Localizable.strings @@ -1,4 +1,4 @@ -/* Translation-Revision-Date: 2023-10-19 13:54:30+0000 */ +/* Translation-Revision-Date: 2024-01-04 10:54:08+0000 */ /* Plural-Forms: nplurals=1; plural=0; */ /* Generator: GlotPress/4.0.0-alpha.11 */ /* Language: zh_TW */ @@ -152,9 +152,6 @@ /* Difference label for Period Overview stat, indicating change from previous period. Ex: +99.9K (5%) */ "%@%@ (%@%%)" = "%1$@%2$@ (%3$@%%)"; -/* Accessibility label for a post in the post list. The parameters are the title, and date respectively. For example, \"Let it Go, 1 hour ago.\" */ -"%@, %@." = "%1$@,%2$@。"; - /* Accessibility value for a Stats' Posting Activity Month if the user selected a day with posts. The first parameter is day (e.g. November 2019). The second parameter is the number of posts. */ "%@. %d posts." = "%1$@。%2$d 篇文章。"; @@ -210,9 +207,6 @@ /* Age between dates over one year. */ "%d years" = "%d 年"; -/* Max image size in pixels (e.g. 300x300px) */ -"%dx%dpx" = "%1$dx%2$d 像素"; - /* One menu area available in the theme */ "%i menu area in this theme" = "此佈景主題中有 %i 個選單區域"; @@ -271,6 +265,9 @@ translators: Block name. %s: The localized block name */ /* translators: %s: social link name e.g: \"Instagram\". */ "%s social icon" = "%s 社交圖示"; +/* translators: displayed right after the classic block is converted to blocks. %s: The localized classic block name */ +"'%s' block converted to blocks" = "'%s' 區塊已轉換成區塊"; + /* translators: Missing block alert title. %s: The localized block name */ "'%s' is not fully-supported" = "系統不完全支援「%s」"; @@ -478,13 +475,6 @@ translators: Block name. %s: The localized block name */ /* Label for the Activity Type filter button when there are more than 1 activity type selected */ "Activity Type (%1$d)" = "活動類型 (%1$d)"; -/* Accessibility label for add button to add items to the user's media library - Remove asset from media picker list */ -"Add" = "新增"; - -/* Action for Media Picker to indicate selection of media. The argument in the string represents the number of elements (as numeric digits) selected */ -"Add %@" = "新增 %@"; - /* No comment provided by engineer. */ "Add Block After" = "在之後新增區塊"; @@ -581,9 +571,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Add menu item to children" = "將選單項目新增至子項目"; -/* Accessibility hint for add button to add items to the user's media library */ -"Add new media" = "新增媒體"; - /* Screen reader text for Menus button that adds a new menu to a site. */ "Add new menu" = "新增選單"; @@ -649,9 +636,6 @@ translators: Block name. %s: The localized block name */ /* Option to select the Airmail app when logging in with magic links */ "Airmail" = "Airmail"; -/* Description of albums in the photo libraries */ -"Albums" = "相簿"; - /* Image alignment option title. Title of the screen for choosing an image's alignment. */ "Alignment" = "對齊"; @@ -665,6 +649,9 @@ translators: Block name. %s: The localized block name */ Title of the drafts filter. This filter shows a list of draft posts. */ "All" = "全部"; +/* Information about redeeming domain credit on site dashboard. */ +"All WordPress.com annual plans include a custom domain name. Register your free domain now." = "所有 WordPress.com 年繳方案皆隨附一個自訂網域名稱。 立即註冊你的免費網域。"; + /* Footer of the free domain registration section for a paid plan. Information about redeeming domain credit on site dashboard. */ "All WordPress.com plans include a custom domain name. Register your free premium domain now." = "所有 WordPress.com 方案都包含一個自訂網域名稱。立即註冊你的免費進階網域。"; @@ -730,10 +717,13 @@ translators: Block name. %s: The localized block name */ "Alt Text" = "替代文字"; /* No comment provided by engineer. */ -"Alternatively, you can detach and edit these blocks separately by tapping “Detach patterns”." = "你可以改為點選「中斷連結版面配置」,然後中斷連結並單獨編輯這些區塊。"; +"Alternatively, you can convert the content to blocks." = "此外,您也可以將內容轉換成區塊。"; /* No comment provided by engineer. */ -"Alternatively, you can detach and edit this block separately by tapping “Detach pattern”." = "你可以改為點選「中斷連結版面配置」,然後中斷連結並單獨編輯此區塊。"; +"Alternatively, you can detach and edit this block separately by tapping “Detach”." = "你也可以改為點選「中斷連結」,然後中斷連結並單獨編輯此區塊。"; + +/* translators: Alternative option included in a warning related to having blocks deeply nested. */ +"Alternatively, you can flatten the content by ungrouping the block." = "或者,你可以取消區塊群組來簡化內容。"; /* Instruction text to explain to help users type their password instead of using magic link login option. */ "Alternatively, you may enter the password for this account." = "或者你也可以輸入此帳號的密碼。"; @@ -882,15 +872,9 @@ translators: Block name. %s: The localized block name */ /* Message prompting the user to confirm that they want to disconnect Jetpack from the site. */ "Are you sure you want to disconnect Jetpack from the site?" = "你確定要中斷Jetpack?"; -/* Message prompting the user to confirm that they want to permanently delete a group of media items. */ -"Are you sure you want to permanently delete these items?" = "確定要永久刪除選取的項目?"; - /* Message prompting the user to confirm that they want to permanently delete a media item. Should match Calypso. */ "Are you sure you want to permanently delete this item?" = "確定要永久刪除這些項目?"; -/* Message of the confirmation alert when deleting a page from the trash. */ -"Are you sure you want to permanently delete this page?" = "是否確定要永久刪除此頁面?"; - /* Message of the confirmation alert when deleting a post from the trash. */ "Are you sure you want to permanently delete this post?" = "你確定要永久刪除此文章?"; @@ -922,9 +906,6 @@ translators: Block name. %s: The localized block name */ /* Title of message shown when user taps submit for review. */ "Are you sure you want to submit for review?" = "是否確定要送交審查?"; -/* Message of the trash page confirmation alert. */ -"Are you sure you want to trash this page?" = "是否確定要清除這個頁面?"; - /* Message of the trash confirmation alert. */ "Are you sure you want to trash this post?" = "你確定要將這篇文章移至垃圾桶嗎?"; @@ -965,9 +946,6 @@ translators: Block name. %s: The localized block name */ /* translators: accessibility text. Empty Audio caption. */ "Audio caption. Empty" = "音訊字幕。 清空"; -/* Accessibility label for audio items in the media collection view. The parameter is the creation date of the audio. */ -"Audio, %@" = "音訊,%@"; - /* No comment provided by engineer. */ "Authenticating" = "驗證中"; @@ -1178,10 +1156,7 @@ translators: Block name. %s: The localized block name */ "Blocks menu" = "區塊選單"; /* translators: Warning related to having blocks deeply nested. %d: The deepest nesting level. */ -"Blocks nested deeper than %d levels may not render properly in the mobile editor. For this reason, we recommend flattening the content by ungrouping the block or editing the block using the web editor." = "向下內嵌超過 %d 層的區塊,在行動編輯器中可能無法正常轉譯。 因此我們建議藉由取消區塊群組,或使用你的網頁編輯器編輯區塊來減少內容。"; - -/* translators: Warning related to having blocks deeply nested. %d: The deepest nesting level. */ -"Blocks nested deeper than %d levels may not render properly in the mobile editor. For this reason, we recommend flattening the content by ungrouping the block or editing the block using your web browser." = "向下內嵌超過 %d 層的區塊,在行動編輯器中可能無法正常轉譯。 因此我們建議藉由取消區塊群組,或使用你的網頁瀏覽器編輯區塊來減少內容。"; +"Blocks nested deeper than %d levels may not render properly in the mobile editor." = "行動編輯器可能無法正確顯示內嵌超過 %d 層的區塊。"; /* Title of a button that displays the WordPress.com blog */ "Blog" = "網誌"; @@ -1259,9 +1234,6 @@ translators: Block name. %s: The localized block name */ /* Label for the post author in the post detail. */ "By " = "By "; -/* Accessibility label for the post author in the post list. The parameter is the author name. For example, \"By Elsa.\" */ -"By %@." = "作者:%@。"; - /* Legal disclaimer for logging in. The underscores _..._ denote underline. */ "By continuing, you agree to our _Terms of Service_." = "繼續操作即代表你同意我們的_服務條款_。"; @@ -1281,8 +1253,7 @@ translators: Block name. %s: The localized block name */ /* Label for size of media while it's being calculated. */ "Calculating..." = "正在計算…"; -/* Accessibility label for taking an image or video with the camera on formatting toolbar. - Accessibility label for the camera tile in the collection view */ +/* Accessibility label for taking an image or video with the camera on formatting toolbar. */ "Camera" = "相機"; /* Title of an alert informing the user the camera permission for the app is disabled and its needed to proceed */ @@ -1333,10 +1304,7 @@ translators: Block name. %s: The localized block name */ Cancels an Action Cancels an alert. Cancels the mark all as read action. - Dismiss the post action sheet Editing GIF alert cancel action button. - Label for a cancel button - Label for the auto-upload cancelation button in the post list. Tapping will prevent the app from auto-uploading the post. Menus cancel button for deleting a menu. Menus cancel button within text bar while editing items. Menus: Cancel button title for canceling an edited menu item. @@ -1353,10 +1321,6 @@ translators: Block name. %s: The localized block name */ Verb. Title for Jetpack Restore cancel button. */ "Cancel" = "取消"; -/* Label for the Post List option that cancels automatic uploading of a post. - Media Library option to cancel an in-progress or failed upload. */ -"Cancel Upload" = "取消上傳"; - /* Menus alert button title to cancel discarding changes and not createa a new menu. Menus alert button title to cancel discarding changes and not select a new menu Menus alert button title to cancel discarding changes and not select a new menu location */ @@ -1385,7 +1349,7 @@ translators: Block name. %s: The localized block name */ /* Image caption field label (for editing) Noun. Label for the caption for a media asset (image / video) */ -"Caption" = "說明文字"; +"Caption" = "媒體說明文字"; /* Alert title. Title for the warning shown to the user when he refuses to re-login when the authToken is missing. */ @@ -1415,9 +1379,6 @@ translators: Block name. %s: The localized block name */ Main title */ "Change Password" = "變更密碼"; -/* Title of button that takes user to the system Settings section for the app */ -"Change Settings" = "變更設定"; - /* Change Username title. */ "Change Username" = "變更使用者名稱"; @@ -1557,9 +1518,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Choose file" = "選擇檔案"; -/* Menu option for selecting media from the device's photo library. */ -"Choose from My Device" = "從「我的裝置」中選擇"; - /* Explanatory text for Homepage Settings homepage type selection. */ "Choose from a homepage that displays your latest posts (classic blog) or a fixed \/ static page." = "選擇讓首頁顯示你的最新文章 (傳統網誌),或顯示固定\/靜態頁面。"; @@ -1760,9 +1718,6 @@ translators: Block name. %s: The localized block name */ /* Community & Non-Profit site intent topic */ "Community & Non-Profit" = "社群和非營利"; -/* Accessibility indication that the current Post List style is currently Compact. */ -"Compact" = "簡潔有力"; - /* The action is completed */ "Completed" = "已完成"; @@ -1948,10 +1903,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Copied block" = "複製的區塊"; -/* Copy the post url and paste anywhere in phone - Label for page copy link. Tapping copy the url of page */ -"Copy Link" = "複製鏈結"; - /* Copy link to oomment button title */ "Copy Link to Comment" = "將連結複製到留言"; @@ -2060,9 +2011,6 @@ translators: Block name. %s: The localized block name */ /* Error title displayed when unable to close user account. */ "Couldn’t close account automatically" = "無法自動關閉帳號"; -/* Message to show while media data source is finding the number of items available. */ -"Counting media items..." = "正在計算媒體項目…"; - /* Period Stats 'Countries' header */ "Countries" = "國家\/地區"; @@ -2313,7 +2261,6 @@ translators: Block name. %s: The localized block name */ /* Delete Delete button title for the warning shown to the user when he refuses to re-login when the authToken is missing. Title for button that permanently deletes a media item (photo / video) - Title for button that permanently deletes one or more media items (photos / videos) Title of a delete button Title of the trash confirmation alert. */ "Delete" = "刪除"; @@ -2321,15 +2268,11 @@ translators: Block name. %s: The localized block name */ /* Menus confirmation button for deleting a menu. */ "Delete Menu" = "刪除選單"; -/* Delete option in the confirmation alert when deleting a page from the trash. - Delete option in the confirmation alert when deleting a post from the trash. - Label for a button permanently deletes a page. - Label for the delete post option. Tapping permanently deletes a post. +/* Delete option in the confirmation alert when deleting a post from the trash. Title for button on the comment details page that deletes the comment when tapped. */ "Delete Permanently" = "永久刪除"; -/* Title of the confirmation alert when deleting a page from the trash. - Title of the confirmation alert when deleting a post from the trash. */ +/* Title of the confirmation alert when deleting a post from the trash. */ "Delete Permanently?" = "是否要永久刪除?"; /* Button label for deleting the current site @@ -2448,7 +2391,6 @@ translators: Block name. %s: The localized block name */ /* Accessibility label for button to dismiss a bottom sheet Accessibility label for the transparent space above the login dialog which acts as a button to dismiss the dialog. Accessibility label for the transparent space above the signup dialog which acts as a button to dismiss the dialog. - Action to show on alert when view asset fails. Dismiss a view. Verb */ "Dismiss" = "關閉"; @@ -2466,12 +2408,6 @@ translators: Block name. %s: The localized block name */ User's Display Name */ "Display Name" = "顯示名稱"; -/* Accessibility label for other media items in the media collection view. The parameter is the creation date of the document. */ -"Document, %@" = "文件:%@"; - -/* Accessibility label for other media items in the media collection view. The parameter is the filename file. */ -"Document: %@" = "文件:%@"; - /* Message shown when all tours have been completed */ "Doesn't it feel good to cross things off a list?" = "清單中又少了一件事情,感覺不錯吧!"; @@ -2632,8 +2568,7 @@ translators: Block name. %s: The localized block name */ /* Description of a Quick Start Tour */ "Draft and publish a post." = "撰寫草稿並發表文章。"; -/* Title of the drafts filter. This filter shows a list of draft posts. - Title of the drafts header in search list. */ +/* Title of the drafts filter. This filter shows a list of draft posts. */ "Drafts" = "草稿"; /* No comment provided by engineer. */ @@ -2645,10 +2580,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Drag to adjust focal point" = "拖曳以調整焦點"; -/* Label for page duplicate option. Tapping creates a copy of the page. - Label for post duplicate option. Tapping creates a copy of the post. */ -"Duplicate" = "複製"; - /* No comment provided by engineer. */ "Duplicate block" = "重複區塊"; @@ -2661,13 +2592,9 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Each block has its own settings. To find them, tap on a block. Its settings will appear on the toolbar at the bottom of the screen." = "每個區塊都有其各自的設定。 若要找到這些設定,請點選區塊。 區塊設定會顯示在畫面底部的工具列。"; -/* Accessibility label for edit button to enable multi selection mode in the user's media library - Edit the post. - Editing GIF alert default action button. +/* Editing GIF alert default action button. Edits a Comment Edits the comment - Label for a button that opens the Edit Page view controller - Label for the edit post button. Tapping displays the editor. User action to edit media details. Verb, edit a comment */ "Edit" = "編輯"; @@ -2675,9 +2602,6 @@ translators: Block name. %s: The localized block name */ /* Title for the edit more button section */ "Edit \"More\" button" = "編輯「更多」按鈕"; -/* Button that displays the media editor to the user */ -"Edit %@" = "編輯 %@"; - /* Blocklist Keyword Edition Title */ "Edit Blocklist Word" = "編輯封鎖清單字詞"; @@ -2867,9 +2791,6 @@ translators: Block name. %s: The localized block name */ /* Secondary message shown when there are no domains that match the user entered text. */ "Enter different words above and we'll look for an address that matches it." = "請在上方輸入其他文字,我們會尋找與其相符的地址。"; -/* Accessibility hint for edit button to enable multi selection mode in the user's media library */ -"Enter edit mode to enable multi select to delete" = "若要刪除,請進入編輯模式以啟用多選功能"; - /* (placeholder) Help enter WordPress password Placeholder of a field to type a password to protect the post. */ "Enter password" = "輸入密碼"; @@ -3025,9 +2946,6 @@ translators: Block name. %s: The localized block name */ /* Short title telling the user they will receive a blogging reminder every day of the week. */ "Every day at %@" = "每天 %@"; -/* Label for the post author filter. This filter shows posts for all users on the blog. */ -"Everyone" = "所有人"; - /* Example story title description */ "Example story title" = "限時動態標題範例"; @@ -3037,9 +2955,6 @@ translators: Block name. %s: The localized block name */ /* No comment provided by engineer. */ "Excerpt length (words)" = "摘要長度 (字數)"; -/* Accessibility label for a post's excerpt in the post list. The parameter is the post excerpt. For example, \"Excerpt. This is the first paragraph.\" */ -"Excerpt. %@." = "文章摘要。%@。"; - /* Should be the same as the text displayed if the user clicks the (i) in Calypso. */ "Excerpts are optional hand-crafted summaries of your content." = "文章摘要是指選擇性手動摘錄的內容。"; @@ -3049,8 +2964,7 @@ translators: Block name. %s: The localized block name */ /* Accessibility Label for the exit full screen button on the full screen comment reply mode */ "Exit Full Screen" = "結束全螢幕"; -/* Accessibility indication that the current Post List style is currently Expanded. - Screen reader text to represent the expanded state of a UI control */ +/* Screen reader text to represent the expanded state of a UI control */ "Expanded" = "已展開"; /* Accessibility hint */ @@ -3100,9 +3014,6 @@ translators: Block name. %s: The localized block name */ The action failed */ "Failed" = "失敗"; -/* Error title when picked media cannot be imported into stories. */ -"Failed Media Export" = "無法匯出媒體"; - /* Message for mark all as read success notice */ "Failed marking Notifications as read" = "無法將全部通知標示為已讀"; @@ -3304,6 +3215,12 @@ translators: Block name. %s: The localized block name */ /* An example tag used in the login prologue screens. */ "Football" = "橄欖球"; +/* translators: Recommendation included in a warning related to having blocks deeply nested. */ +"For this reason, we recommend editing the block using the web editor." = "因此,我們建議使用網頁編輯器來編輯區塊。"; + +/* translators: Recommendation included in a warning related to having blocks deeply nested. */ +"For this reason, we recommend editing the block using your web browser." = "因此,我們建議使用網頁瀏覽器來編輯區塊。"; + /* Register Domain - Domain contact information section header description */ "For your convenience, we have pre-filled your WordPress.com contact information. Please review to be sure it’s the correct information you want to use for this domain." = "為了方便起見,我們已預先填入你的 WordPress.com 聯絡人資訊。請檢視各項資訊,以確認這是你想要用於此網域的正確資訊。"; @@ -3621,8 +3538,7 @@ translators: Block name. %s: The localized block name */ "Home" = "首頁"; /* Title for setting which shows the current page assigned as a site's homepage - Title for the homepage section in site settings screen - Title of the Homepage Badge */ + Title for the homepage section in site settings screen */ "Homepage" = "首頁"; /* Label for Homepage Settings site settings section @@ -3719,9 +3635,6 @@ translators: Block name. %s: The localized block name */ /* Hint for image title on image settings. */ "Image title" = "圖片標題"; -/* Accessibility label for image thumbnails in the media collection view. The parameter is the creation date of the image. */ -"Image, %@" = "圖片 (%@ 建立)"; - /* Undated post time label */ "Immediately" = "立即"; @@ -4207,9 +4120,6 @@ translators: Block name. %s: The localized block name */ Settings: Comments Approval settings */ "Links in comments" = "留言中的連結"; -/* The accessibility label for the list style button in the Post List. */ -"List style" = "清單樣式"; - /* Title of the screen that load selected the revisions. */ "Load" = "載入"; @@ -4225,18 +4135,12 @@ translators: Block name. %s: The localized block name */ /* Text displayed while loading the activity feed for a site */ "Loading Backups..." = "正在載入備份…"; -/* Phrase to show when the user has searched for GIFs and they are being loaded. */ -"Loading GIFs..." = "正在載入 GIF..."; - /* Menus label text displayed while menus are loading */ "Loading Menus..." = "正在載入選單..."; /* Text displayed while loading site People. */ "Loading People..." = "正在載入人員…"; -/* Phrase to show when the user has searched for images and they are being loaded. */ -"Loading Photos..." = "正在載入相片..."; - /* Text displayed while loading plans details */ "Loading Plan..." = "正在載入方案…"; @@ -4297,8 +4201,7 @@ translators: Block name. %s: The localized block name */ /* Local Services site intent topic */ "Local Services" = "當地服務"; -/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. - Title of the Local Changes Badge */ +/* A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog. */ "Local changes" = "本機變更"; /* Title for alert when a generic error happened when trying to find the location of the device */ @@ -4462,7 +4365,6 @@ translators: Block name. %s: The localized block name */ "Max Video Upload Size" = "最大影片上傳大小"; /* Accessibility label for the Me button in My Site. - Label for the post author filter. This filter shows posts only authored by the current user. Me page title Noun. Title. Links to the Me screen. The accessibility value of the me tab. @@ -4470,9 +4372,7 @@ translators: Block name. %s: The localized block name */ "Me" = "我"; /* Noun. Title. Links to the blog's Media library. - Tab bar title for the Media tab in Media Picker The menu item to select during a guided tour. - Title for Media Library section of the app. Title for the media section in site settings screen Title label for the media settings section in the app settings */ "Media" = "多媒體內容"; @@ -4484,13 +4384,6 @@ translators: Block name. %s: The localized block name */ /* Label for size of media cache in the app. */ "Media Cache Size" = "媒體快取大小"; -/* Title for alert when access to media capture is not granted */ -"Media Capture" = "媒體擷取"; - -/* Title for alert when a generic error happened when loading media - Title for alert when access to the media library is not granted by the user */ -"Media Library" = "媒體庫"; - /* Title for action sheet with media options. */ "Media Options" = "媒體選項"; @@ -4513,9 +4406,6 @@ translators: Block name. %s: The localized block name */ /* translators: %s: block title e.g: \"Paragraph\". */ "Media options" = "媒體選項"; -/* Alert title when there is issues loading an asset to preview. */ -"Media preview failed." = "媒體預覽失敗。"; - /* Alert displayed to the user when multiple media items have uploaded successfully. */ "Media uploaded (%ld files)" = "上傳的媒體 (%ld 個檔案)"; @@ -4553,9 +4443,6 @@ translators: Block name. %s: The localized block name */ Label for the share message field on the post settings. */ "Message" = "訊息"; -/* Title of section containing image / video metadata such as size and file type */ -"Metadata" = "中繼資料"; - /* Option to select the Microsft Outlook app when logging in with magic links */ "Microsoft Outlook" = "Microsoft Outlook"; @@ -4575,13 +4462,11 @@ translators: Block name. %s: The localized block name */ "Months and Years" = "月份與年份"; /* Accessibility label for more button in dashboard quick start card. - Accessibility label for the More button in Page List. Accessibility label for the More button in Post List (compact view). Accessibility label for the More button on formatting toolbar. Accessibility label for the More button on Reader Cell Accessibility label for the More button on Reader's post details - Action button to display more available options - Label for the more post button. Tapping displays an action sheet with post options. */ + Action button to display more available options */ "More" = "更多"; /* Action button to display more available options @@ -4639,15 +4524,8 @@ translators: Block name. %s: The localized block name */ /* Screen reader text for button that will move the menu item */ "Move menu item" = "移動選單項目"; -/* Label for a button that moves a page to the draft folder - Label for an option that moves a post to the draft folder */ -"Move to Draft" = "移至草稿"; - -/* Label for a button that moves a page to the trash folder - Label for a option that moves a post to the trash folder - Title for button on the comment details page that moves the comment to trash when tapped. +/* Title for button on the comment details page that moves the comment to trash when tapped. Trash option in the trash confirmation alert. - Trash option in the trash page confirmation alert. Trashes the comment */ "Move to Trash" = "移至垃圾桶"; @@ -4679,7 +4557,8 @@ translators: Block name. %s: The localized block name */ Title of My Site tab */ "My Site" = "我的網站"; -/* Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ +/* Title for site picker screen. + Title of the 'My Sites' tab - used for spotlight indexing on iOS. */ "My Sites" = "我的網站"; /* Siri Suggestion to open My Sites */ @@ -4857,7 +4736,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "No blocks found" = "找不到區塊"; /* An option in a list. Automatically approve no comments. */ -"No comments" = "無留言"; +"No comments" = "尚無留言"; /* Displayed in the Notifications Tab as a title, when the Comments Filter shows no notifications Displayed on the post details page when there are no post comments. @@ -4929,9 +4808,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title for the view when there aren't any Activities to display in the Activity Log for a given filter. */ "No matching events found." = "找不到符合條件的活動。"; -/* Message displayed when no results are returned from a media library search. Should match Calypso. - Phrase to show when the user search for images but there are no result to show. - Phrase to show when the user searches for GIFs but there are no result to show. */ +/* Message displayed when no results are returned from a media library search. Should match Calypso. */ "No media matching your search" = "沒有符合你搜尋條件的媒體"; /* Menus text shown when no menus were available for loading the Menus editor. */ @@ -4949,8 +4826,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Displayed in the Notifications Tab as a title, when there are no notifications */ "No notifications yet" = "尚無通知"; -/* Displayed when the user is searching the pages list and there are no matching pages - Text displayed when there's no matching with the text search */ +/* Text displayed when there's no matching with the text search */ "No pages matching your search" = "沒有頁面符合你的搜尋條件"; /* Text displayed when search for plugins returns no results */ @@ -4971,9 +4847,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Message shown whent the reader finds no posts for the chosen tag */ "No posts have been made recently with this tag." = "最近沒有發表包含此標籤的文章。"; -/* Displayed when the user is searching the posts list and there are no matching posts */ -"No posts matching your search" = "沒有文章符合你的搜尋條件"; - /* Accessibility value for a Stats' Posting Activity Month if there are no posts. */ "No posts." = "沒有文章。"; @@ -5074,9 +4947,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* A message title */ "Nothing liked yet" = "尚未有按讚的文章"; -/* Default message for empty media picker */ -"Nothing to show" = "沒有可顯示的內容"; - /* Notifications Details Accessibility Identifier */ "Notification Details Table" = "通知詳細資訊表"; @@ -5136,7 +5006,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Alert dismissal title Button title. Acknowledges a prompt. Button title. An acknowledgement of the message displayed in a prompt. - Confirmation of action Default action Dismisses the alert Marks all notifications as read. @@ -5198,9 +5067,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* No comment provided by engineer. */ "Only show excerpt" = "只顯示摘要"; -/* Message telling the user that they've only enabled limited photo library permissions for the app. */ -"Only the selected photos you've given access to are available." = "只能查看你已授權存取的選定照片。"; - /* An informal exclaimation that means `something went wrong`. Title for the view when there's an error loading a comment. Title for the view when there's an error loading Activity Log @@ -5235,9 +5101,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title of a button that opens the apps settings in the system Settings.app */ "Open Settings" = "開啟設定"; -/* Editor button to swich the media picker from quick mode to full picker */ -"Open full media picker" = "開啟所有媒體挑選器"; - /* No comment provided by engineer. */ "Open in Safari" = "在 Safari 中開啟"; @@ -5277,6 +5140,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Divider on initial auth view separating auth options. */ "Or" = "或者"; +/* Instruction text for other forms of two-factor auth methods. */ +"Or choose another form of authentication." = "或選擇其他驗證方式。"; + /* Label for button to log in using site address. Underscores _..._ denote underline. */ "Or log in by _entering your site address_." = "或輸入網站位址登入。"; @@ -5335,15 +5201,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title shown when selecting a post type of Page from the Share Extension. */ "Page" = "網頁"; -/* Prompts the user that a restored page was moved to the drafts list. */ -"Page Restored to Drafts" = "頁面已還原為草稿"; - -/* Prompts the user that a restored page was moved to the published list. */ -"Page Restored to Published" = "頁面已還原為已發表"; - -/* Prompts the user that a restored page was moved to the scheduled list. */ -"Page Restored to Scheduled" = "頁面已還原為已排程"; - /* Name of the button to open the page settings The title of the Page Settings screen. */ "Page Settings" = "頁面設定"; @@ -5360,9 +5217,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of notification displayed when a page has failed to upload. */ "Page failed to upload" = "頁面上傳失敗"; -/* A short message explaining that a page was moved to the trash bin. */ -"Page moved to trash." = "頁面已移至垃圾桶。"; - /* Title of notification displayed when a page has been successfully saved as a draft. */ "Page pending review" = "頁面待審中"; @@ -5434,8 +5288,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title of pending Comments filter. */ "Pending" = "待審核"; -/* Name for the status of a post pending review. - Title of the Pending Review Badge */ +/* Name for the status of a post pending review. */ "Pending review" = "等待複審"; /* Noun. Title of the people management feature. @@ -5464,12 +5317,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Photography site intent topic */ "Photography" = "攝影"; -/* Tab bar title for the Photos tab in Media Picker */ -"Photos" = "圖片"; - -/* Subtitle for placeholder in Free Photos. The company name 'Pexels' should always be written as it is. */ -"Photos provided by Pexels" = "由 Pexels 提供的相片"; - /* Title for selecting a new username in the site creation flow. */ "Pick username" = "挑選使用者名稱"; @@ -5562,7 +5409,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Please enter the password for your WordPress.com account to log in with your Apple ID." = "請輸入 WordPress.com 帳戶的密碼,以便使用 Apple ID 登入。"; /* Instruction text on the two-factor screen. */ -"Please enter the verification code from your authenticator app, or tap the link below to receive a code via SMS." = "請輸入驗證器應用程式上的驗證碼,或點選下方連結以透過簡訊接收驗證碼。"; +"Please enter the verification code from your authenticator app." = "請輸入驗證器應用程式上的驗證碼。"; /* Popup message to ask for user credentials (fields shown below). */ "Please enter your credentials" = "請輸入你的密碼"; @@ -5657,15 +5504,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ The post formats available for the post. Should be the same as in core WP. */ "Post Format" = "文章格式"; -/* Prompts the user that a restored post was moved to the drafts list. */ -"Post Restored to Drafts" = "文章已還原為草稿"; - -/* Prompts the user that a restored post was moved to the published list. */ -"Post Restored to Published" = "文章已還原為已發表"; - -/* Prompts the user that a restored post was moved to the scheduled list. */ -"Post Restored to Scheduled" = "文章已還原為已排程"; - /* Name of the button to open the post settings The title of the Post Settings screen. */ "Post Settings" = "文章設定"; @@ -5685,9 +5523,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of notification displayed when a post has failed to upload. */ "Post failed to upload" = "文章上傳失敗"; -/* A short message explaining that a post was moved to the trash bin. */ -"Post moved to trash." = "文章已移至垃圾桶。"; - /* Title of notification displayed when a post has been successfully saved as a draft. */ "Post pending review" = "文章待審中"; @@ -5746,9 +5581,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ Period Stats 'Posts and Pages' header */ "Posts and Pages" = "文章與頁面"; -/* Title of the Posts Page Badge */ -"Posts page" = "文章列表頁面"; - /* Message informing the user that their static homepage for posts was set successfully */ "Posts page successfully updated" = "已成功更新文章頁面"; @@ -5761,9 +5593,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* A message explaining the Posts I Like feature in the reader */ "Posts that you like will appear here." = "這裡會顯示你按讚的文章。"; -/* Subtitle for placeholder in Tenor picker. `The company name 'Tenor' should always be written as it is. */ -"Powered by Tenor" = "由 Tenor 支援"; - /* Browse premium themes selection title */ "Premium" = "進階版"; @@ -5782,18 +5611,12 @@ translators: %s: Select control button label e.g. \"Button width\" */ Title for screen to preview a static content. */ "Preview" = "預覽"; -/* Action for Media Picker to preview the selected media items. The argument in the string represents the number of elements (as numeric digits) selected */ -"Preview %@" = "預覽 %@"; - /* Title for web preview device switching button */ "Preview Device" = "預覽裝置"; /* Title on display preview error */ "Preview Unavailable" = "無法預覽"; -/* Accessibility label for media item preview for user's viewing an item in their media library */ -"Preview media" = "預覽媒體"; - /* No comment provided by engineer. */ "Preview page" = "閱覽頁面"; @@ -5840,8 +5663,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Privacy notice for California users" = "加州使用者的隱私權聲明"; /* Name for the status of a post that is marked private. - Privacy setting for posts set to 'Private'. Should be the same as in core WP. - Title of the Private Badge */ + Privacy setting for posts set to 'Private'. Should be the same as in core WP. */ "Private" = "私密"; /* No comment provided by engineer. */ @@ -5891,12 +5713,10 @@ translators: %s: Select control button label e.g. \"Button width\" */ Label for the publish date button. */ "Publish Date" = "發表日期"; -/* A short phrase indicating a post is due to be immedately published. - Label for a button that moves a page to the published folder, publishing with the current date/time. */ +/* A short phrase indicating a post is due to be immedately published. */ "Publish Immediately" = "立刻發佈"; /* Label for a button that publishes the post - Label for an option that moves a publishes a post immediately Title of button allowing the user to immediately publish the post they are editing. */ "Publish Now" = "立即發表"; @@ -5914,8 +5734,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Name for the status of a published post. Period Stats 'Published' header - Title of the published filter. This filter shows a list of posts that the user has published. - Title of the published header in search list. */ + Title of the published filter. This filter shows a list of posts that the user has published. */ "Published" = "已發佈"; /* Precedes the name of the blog just posted on */ @@ -6057,8 +5876,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of the completion screen of the Blogging Reminders Settings screen when the reminders are removed. */ "Reminders removed" = "提醒已移除"; -/* Add asset to media picker list - Alert button to confirm a plugin to be removed +/* Alert button to confirm a plugin to be removed Button label when removing a blog Label for the Remove Feature Image icon. Tapping will show a confirmation screen for removing the feature image from the post. Remove Action @@ -6071,7 +5889,7 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Remove %@" = "移除 %@"; /* Accessibility Label for the Remove Feature Image icon. Tapping will show a confirmation screen for removing the feature image from the post. */ -"Remove Featured Image" = "移除特色圖片"; +"Remove Featured Image" = "移除精選圖片"; /* Label action for removing a link from the editor */ "Remove Link" = "移除連結"; @@ -6211,9 +6029,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Title of secondary button on alert prompting verify their accounts while attempting to publish */ "Resend" = "重新傳送"; -/* Title of the reset button */ -"Reset" = "重設"; - /* Accessibility label for the reset activity type button */ "Reset Activity Type filter" = "重設活動類型篩選器"; @@ -6268,12 +6083,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ Button title, displayed when media has failed to upload. Allows the user to try the upload again. Button title. Retries uploading a post. If a user taps the button with this label, the action that evinced this error view will be retried. - Label for a button that attempts to re-upload a page that previously failed to upload. - Label for the retry post upload button. Tapping attempts to upload the post again. Opens the media library . Retries the upload of a user's post. Retry updating User's Role - Retry uploading the post. Retry. Action Retry. Verb – retry a failed media upload. The Jetpack view button title used when an error occurred @@ -6285,9 +6097,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Button title that triggers a scan */ "Retry Scan" = "再次掃瞄"; -/* User action to retry media upload. */ -"Retry Upload" = "重試上傳"; - /* User action to retry all failed media uploads. */ "Retry all" = "全部重試"; @@ -6385,9 +6194,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Accessibility label for the 'Save Post' button when a post has been saved. */ "Saved Post" = "已儲存文章"; -/* Text displayed in HUD when a media item's metadata (title, etc) is saved successfully. */ -"Saved!" = "已儲存!"; - /* Accessibility hint for the 'Save Post' button. */ "Saves this post for later." = "儲存此文章以供稍後使用。"; @@ -6398,7 +6204,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ "Saving post…" = "正在儲存文章…"; /* Menus save button title while it is saving a Menu. - Text displayed in HUD while a media item's metadata (title, etc) is being saved. Text displayed in HUD while a post is being saved as a draft. */ "Saving..." = "儲存中..."; @@ -6489,21 +6294,9 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* No comment provided by engineer. */ "Search or type URL" = "搜尋或輸入 URL"; -/* Text displayed when the search controller will be presented */ -"Search pages" = "搜尋頁面"; - -/* Text displayed when the search controller will be presented */ -"Search posts" = "搜尋文章"; - /* No comment provided by engineer. */ "Search settings" = "搜尋設定"; -/* Title for placeholder in Tenor picker */ -"Search to find GIFs to add to your Media Library!" = "搜尋免費 GIF 以新增至你的媒體庫!"; - -/* Title for placeholder in Free Photos */ -"Search to find free photos to add to your Media Library!" = "搜尋免費相片以新增至你的媒體庫!"; - /* Menus search bar placeholder text. */ "Search..." = "搜尋..."; @@ -6574,9 +6367,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Register Domain - Domain contact information field placeholder for Country */ "Select Country" = "選取國家\/地區"; -/* Title of button that allows the user to select more photos to access within the app */ -"Select More" = "選取更多"; - /* Blog Picker's Title */ "Select Site" = "選取網站"; @@ -6598,9 +6388,6 @@ translators: %s: Select control button label e.g. \"Button width\" */ /* Register domain - Title for the Choose domain button of Suggested domains screen */ "Select domain" = "選取網域"; -/* Accessibility hint for actions when displaying media items. */ -"Select media." = "選取媒體。"; - /* Accessibility label for selecting paragraph style button on formatting toolbar. */ "Select paragraph style" = "選取段落風格"; @@ -6704,19 +6491,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label for connected service in Publicize stat. */ "Service" = "服務"; -/* Label for a button that opens the Set Parent options view controller - Navigation title displayed on the navigation bar */ +/* Navigation title displayed on the navigation bar */ "Set Parent" = "設定上層項目"; /* No comment provided by engineer. */ "Set as Featured Image" = "設為特色圖片"; -/* Label for a button that sets the selected page as the site's Homepage */ -"Set as Homepage" = "設為首頁"; - -/* Label for a button that sets the selected page as the site's Posts page */ -"Set as Posts Page" = "設定為文章頁面"; - /* Notice confirming that an image has been set as the post's featured image. */ "Set as featured image" = "設為特色圖片"; @@ -6760,7 +6540,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Accessibility label for share buttons in nav bars Button label to share a post Button label to share a web page - Share the post. Shares the comment URL Spoken accessibility label Title for a button that allows the user to share their answer to the prompt. @@ -7146,8 +6925,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Name of setting configured when a site uses a static page as its homepage */ "Static Homepage" = "靜態首頁"; -/* Label for post stats option. Tapping displays statistics for a post. - Noun. Abbreviation of Statistics. Name of the Stats feature +/* Noun. Abbreviation of Statistics. Name of the Stats feature Noun. Abbv. of Statistics. Links to a blog's Stats screen. Stats 3D Touch Shortcut Stats window title @@ -7178,9 +6956,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label text that defines a post marked as sticky */ "Sticky" = "置頂"; -/* Accessibility label for a sticky post in the post list. */ -"Sticky." = "置頂文章。"; - /* User action to stop upload. */ "Stop upload" = "停止上傳"; @@ -7237,7 +7012,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ Theme Support action title */ "Support" = "支援"; -/* Button used to switch site */ +/* Title for back button that leads to the site picker screen. */ "Switch Site" = "切換網站"; /* Switches the Editor to HTML Mode */ @@ -7325,9 +7100,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Label explaining why users might want to add tags. */ "Tags help tell readers what a post is about. Separate different tags with commas." = "標籤可協助讀者瞭解文章內容。以逗號分隔不同標籤。"; -/* Menu option for taking an image or video with the device's camera. */ -"Take Photo or Video" = "拍攝圖片或影片"; - /* No comment provided by engineer. */ "Take a Photo" = "拍攝照片"; @@ -7398,12 +7170,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Accessibility hint */ "Tap to select the previous period" = "點選以選擇上個期間"; -/* Accessibility hint for button used to switch site */ -"Tap to switch to another site, or add a new site" = "點選即可切換到其他網站或新增網站"; - -/* Accessibility hint for media item preview for user's viewing an item in their media library */ -"Tap to view media in full screen" = "點選以全螢幕檢視媒體"; - /* Accessibility hint for a button that opens a new view with more details. */ "Tap to view more details." = "Tap to view more details."; @@ -7449,10 +7215,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* No comment provided by engineer. */ "Text formatting controls are located within the toolbar positioned above the keyboard while editing a text block" = "編輯文字區塊時,文字格式控制項就在鍵盤上方的工具列中"; -/* Button title - The button's title text to send a 2FA code via SMS text message. */ +/* Button title */ "Text me a code instead" = "請改用簡訊傳送驗證碼給我"; +/* The button's title text to send a 2FA code via SMS text message. */ +"Text me a code via SMS" = "透過簡訊向我傳送驗證碼"; + /* Message of alert when theme activation succeeds */ "Thanks for choosing %@ by %@" = "感謝你選擇 %2$@ 設計的 %1$@"; @@ -7480,9 +7248,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Error message shown to a user who is trying to share to Facebook but does not have any available Facebook Pages. */ "The Facebook connection cannot find any Pages. Publicize cannot connect to Facebook Profiles, only published Pages." = "Facebook 連結找不到任何頁面。Publicize 無法連結至 Facebook 個人檔案,只能連結至已發表的頁面。"; -/* Message shown when a GIF failed to load while trying to add it to the Media library. */ -"The GIF could not be added to the Media Library." = "無法將 GIF 新增到「媒體庫」。"; - /* Description shown when a user logs in with Google but no matching WordPress.com account is found */ "The Google account \"%@\" doesn't match any account on WordPress.com" = "沒有任何與 Google 帳號「%@」匹配的 WordPress.com 帳號"; @@ -7610,7 +7375,7 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Error message shown when user attempts to remove the site owner. */ "The user you are trying to remove is the owner of this site. Please contact support for assistance." = "你嘗試移除的使用者是此網站的擁有者。如需協助,請聯絡支援團隊。"; -/* Error message informing a user about an invalid password. */ +/* No comment provided by engineer. */ "The username or password stored in the app may be out of date. Please re-enter your password in the settings and try again." = "儲存在應用程式中的使用者名稱或密碼可能已過期。請在設定中重新輸入密碼,然後再試一次。"; /* Message shown when a video failed to load while trying to add it to the Media library. */ @@ -7678,9 +7443,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* A short error message letting the user know about a problem displaying a post. */ "There was a problem displaying this post." = "顯示此文章時發生問題。"; -/* Error message displayed when the Media Library is unable to load a full sized preview of an item. */ -"There was a problem loading the media item." = "載入媒體項目時發生問題。"; - /* The loading view subtitle displayed when an error occurred */ "There was a problem loading your data, refresh your page to try again." = "載入資料時發生問題,請重新整理網頁並重試。"; @@ -7693,9 +7455,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Explaining to the user there was an error trying to obtain the current location of the user. */ "There was a problem when trying to access your location. Please try again later." = "嘗試存取你的位置時發生問題。請稍後再試一次。"; -/* Explaining to the user there was an generic error accesing media. */ -"There was a problem when trying to access your media. Please try again later." = "嘗試存取你的媒體時發生問題。請稍後再試一次。"; - /* Message for stories unknown error. */ "There was a problem with the Stories editor. If the problem persists you can contact us via the Me > Help & Support screen." = "限時動態編輯器發生問題。 如果問題持續發生,請透過「我」> 「說明與支援」畫面聯絡我們。"; @@ -7766,9 +7525,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* A description informing the user in order to proceed with this feature we will need camera permissions, and how to enable it. */ "This app needs permission to access the Camera to scan login codes, tap on the Open Settings button to enable it." = "此應用程式需要相機存取權限才能掃描登入碼,請點選「開啟設定」按鈕以啟用權限。"; -/* Explaining to the user why the app needs access to the device media library. */ -"This app needs permission to access your device media library in order to add photos and\/or video to your posts. Please change the privacy settings if you wish to allow this." = "此應用程式需要權限才能存取你的裝置媒體庫,以便在你的文章中新增相片和\/或影片。若你希望允許此應用程式功能,請變更隱私設定。"; - /* No comment provided by engineer. */ "This color combination may be hard for people to read. Try using a brighter background color and\/or a darker text color." = "此色彩組合可能讓人難以閱讀。 請嘗試更亮的背景顏色和\/或較深的文字顏色。"; @@ -7878,6 +7634,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Description of a Quick Start Tour */ "Time to finish setting up your site! Our checklist walks you through the next steps." = "完成你的網站設定的時候到了!我們的檢查清單會引導你完成後續步驟。"; +/* Error when the uses takes more than 1 minute to submit a security key. */ +"Time's up, but don't worry, your security is our priority. Please try again!" = "優惠已到期,但別擔心,你的安全是我們的第一要務。 請再試一次!"; + /* WordPress.com Marketing Footer Text */ "Tips for getting the most out of WordPress.com." = "讓 WordPress.com 發揮最大功效的秘訣。"; @@ -8001,24 +7760,20 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title for the traffic section in site settings screen */ "Traffic" = "流量"; +/* Describes a domain that was transferred from elsewhere to wordpress.com */ +"Transferred Domain" = "已轉移的網域"; + /* translators: %s: block title e.g: \"Paragraph\". */ "Transform %s to" = "將 %s 轉換為"; /* No comment provided by engineer. */ "Transform block…" = "轉換區塊…"; -/* Accessibility label for trash button to delete items from the user's media library - Accessibility label for trash buttons in nav bars +/* Accessibility label for trash buttons in nav bars Trashes a comment Trashes the comment */ "Trash" = "移至回收桶"; -/* Accessibility hint for trash button to delete items from the user's media library */ -"Trash selected media" = "將選取的媒體移至垃圾桶"; - -/* Title of the trash page confirmation alert. */ -"Trash this page?" = "是否要清除此頁面?"; - /* Title of the trash confirmation alert. */ "Trash this post?" = "是否將此文章移至垃圾桶?"; @@ -8136,9 +7891,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Shown when a user logs in with Google but it subsequently fails to work as login to WordPress.com */ "Unable To Connect" = "無法連線"; -/* An error message. */ -"Unable to Connect" = "無法連結"; - /* Title for stories unknown error. */ "Unable to Create Stories Editor" = "無法建立限時動態編輯器"; @@ -8154,9 +7906,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* An error message shown when there is an issue creating new invite links. */ "Unable to create new invite links." = "無法新建邀請連結。"; -/* Text displayed in HUD if there was an error attempting to delete a group of media items. */ -"Unable to delete all media items." = "無法刪除所有媒體項目。"; - /* Text displayed in HUD if there was an error attempting to delete a media item. */ "Unable to delete media item." = "無法刪除媒體項目。"; @@ -8220,12 +7969,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Message displayed when sharing a link to the downloadable backup fails. */ "Unable to share link" = "無法分享連結"; -/* Message that appears when a user tries to trash a page while their device is offline. */ -"Unable to trash pages while offline. Please try again later." = "離線時無法清除頁面。 請稍後再試一次。"; - -/* Message that appears when a user tries to trash a post while their device is offline. */ -"Unable to trash posts while offline. Please try again later." = "離線時無法刪除文章。請稍後再試。"; - /* Notice title when turning site notifications off fails. */ "Unable to turn off site notifications" = "無法關閉網站通知"; @@ -8298,8 +8041,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ Button title. Reverts the previous notification operation Revert an operation Revert enabling notification after successfully subcribing to the comments for the post. - The title of an 'undo' button. Tapping the button moves a trashed page out of the trash folder. - The title of an 'undo' button. Tapping the button moves a trashed post out of the trash folder. Undo action */ "Undo" = "復原"; @@ -8342,9 +8083,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title for Unknown HTML Editor */ "Unknown HTML" = "不明 HTML"; -/* Label to use when creation date from media asset is not know. */ -"Unknown creation date" = "建立日期未知"; - /* No comment provided by engineer. */ "Unknown error" = "發生未知的錯誤"; @@ -8510,6 +8248,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of a row displayed on the debug screen used to configure the sandbox store use in the App. */ "Use Sandbox Store" = "使用沙盒商店"; +/* The button's title text to use a security key. */ +"Use a security key" = "使用安全性金鑰"; + /* Option to enable the block editor for new posts */ "Use block editor" = "使用區塊編輯器"; @@ -8585,15 +8326,10 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of an alert informing users that the video they are trying to select is not allowed. */ "Video not uploaded" = "影片未上傳"; -/* Accessibility label for video thumbnails in the media collection view. The parameter is the creation date of the video. */ -"Video, %@" = "影片 (%@ 建立)"; - /* Period Stats 'Videos' header */ "Videos" = "影片"; /* Button title. Displays a summary / sharing screen for a specific post. - Label for a button that opens the page when tapped. - Label for the view post button. Tapping displays the post as it appears on the web. Opens the post epilogue screen to allow sharing / viewing of a post. Theme View action title Verb. The screen title shown when viewing a post inside the app. */ @@ -8708,6 +8444,10 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Message shown on screen while waiting for Google to finish its signup process. */ "Waiting for Google to complete…" = "正在等待 Google 完成…"; +/* Text while the webauthn signature is being verified + Text while waiting for a security key challenge */ +"Waiting for security key" = "正在等候安全性金鑰"; + /* View title during the Google auth process. */ "Waiting..." = "等待中…"; @@ -9079,6 +8819,12 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* An error message shown when a wpcom user provides the wrong password. */ "Whoops, something went wrong and we couldn't log you in. Please try again!" = "糟糕,發生了點錯誤,我們無法將你登入。請再試一次!"; +/* Generic error on the 2FA screen */ +"Whoops, something went wrong. Please try again!" = "糟糕,出狀況了。 請再試一次!"; + +/* Error when the uses chooses an invalid security key on the 2FA screen. */ +"Whoops, that security key does not seem valid. Please try again with another one" = "糟糕,安全性金鑰似乎不是有效的。 請使用另一組金鑰再試一次"; + /* Error message shown when an incorrect two factor code is provided. */ "Whoops, that's not a valid two-factor verification code. Double-check your code and try again!" = "糟糕,此雙重驗證碼無效。請再次檢查你的驗證碼,然後再試一次!"; @@ -9106,9 +8852,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Siri Suggestion to open Support */ "WordPress Help" = "WordPress 說明"; -/* Name for the WordPress Media Library */ -"WordPress Media" = "WordPress 媒體"; - /* Accessibility label for selecting an image or video from the user's WordPress media library on formatting toolbar. */ "WordPress Media Library" = "WordPress 媒體庫"; @@ -9423,9 +9166,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Share extension error dialog text. */ "Your account does not have permission to upload media to this site. The Site Administrator can change these permissions." = "你的帳號沒有上傳媒體至這個網站的權限。網站管理員可變更這些權限。"; -/* Explaining to the user why the app needs access to the device media library. */ -"Your app is not authorized to access media library due to active restrictions such as parental controls. Please check your parental control settings in this device." = "你的應用程式受到家長監護之類的設定主動限制,因此無權存取媒體庫。請檢查此裝置的家長監護設定。"; - /* Title for the Jetpack Backup Complete message. */ "Your backup is now available for download" = "你的備份現在已可下載"; @@ -9444,9 +9184,6 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Title of the site address section in the Domains Dashboard. */ "Your free WordPress.com address is" = "你的 WordPress.com 免費位址是"; -/* Error message when picked media cannot be imported into stories. */ -"Your media could not be exported. If the problem persists you can contact us via the Me > Help & Support screen." = "無法匯出你的媒體. If the problem persists you can contact us via the Me >「說明與支援」畫面。"; - /* Details about recently acquired domain on domain credit redemption success screen */ "Your new domain %@ is being set up. It may take up to 30 minutes for your domain to start working." = "正在設定你的新網域 %@。 你的網域最多可能需要 30 分鐘才能開始運作。"; @@ -9570,8 +9307,38 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* This is the string we display when prompting the user to review the WordPress app */ "appRatings.wordpress.prompt" = "你覺得 WordPress 如何?"; -/* Label displayed on audio media items. */ -"audio" = "音訊"; +/* Option to enable the optimization of images when uploading. */ +"appSettings.media.imageOptimizationRow" = "將圖片最佳化"; + +/* Indicates an image will use high quality when uploaded. */ +"appSettings.media.imageQuality.high" = "高"; + +/* Indicates an image will use low quality when uploaded. */ +"appSettings.media.imageQuality.low" = "低"; + +/* Indicates an image will use maximum quality when uploaded. */ +"appSettings.media.imageQuality.maximum" = "最高"; + +/* Indicates an image will use medium quality when uploaded. */ +"appSettings.media.imageQuality.medium" = "中等"; + +/* The quality of image used when uploading */ +"appSettings.media.imageQuality.title" = "畫質"; + +/* Title for the image quality settings option. */ +"appSettings.media.imageQualityRow" = "圖片畫質"; + +/* Message of an alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.message" = "圖片最佳化會壓縮圖片,以加快上傳速度。\n\n系統預設啟用此選項,但你可以隨時在應用程式設定中變更。"; + +/* Title of an alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.title" = "要繼續最佳化圖片嗎?"; + +/* Title of button for turning off image optimization, displayed in the alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.turnOff" = "不,關閉設定"; + +/* Title of button for leaving on image optimization, displayed in the alert informing users to enable image optimization in uploads. */ +"appSettings.optimizeImagesPopup.turnOn" = "是,繼續使用"; /* translators: displays audio file extension. e.g. MP3 audio file */ "audio file" = "音訊檔案"; @@ -9682,7 +9449,44 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ "blogHeader.actionCopyURL" = "複製網址"; /* Context menu button title */ -"blogHeader.actionOpenInBrowser" = "在瀏覽器中開啟"; +"blogHeader.actionVisitSite" = "造訪網站"; + +/* Title for a button that, when tapped, shows more info about participating in Bloganuary. */ +"bloganuary.dashboard.card.button.learnMore" = "深入瞭解"; + +/* Short description for the Bloganuary event, shown right below the title. */ +"bloganuary.dashboard.card.description" = "Bloganuary 會在 1 月送上網誌提示:這個社群挑戰的目的,是為了協助你在新的一年養成寫網誌的習慣。"; + +/* Title for the Bloganuary dashboard card while Bloganuary is running. */ +"bloganuary.dashboard.card.runningTitle" = "Bloganuary 正式開跑!"; + +/* Title for the Bloganuary dashboard card. */ +"bloganuary.dashboard.card.title" = "Bloganuary 挑戰即將來臨!"; + +/* Title of a button that calls the user to enable the Blogging Prompts feature. */ +"bloganuary.learnMore.modal.button.promptsDisabled" = "開啟網誌提示"; + +/* Title of a button that will dismiss the Bloganuary modal when tapped. +Note that the word 'go' here should have a closer meaning to 'start' rather than 'move forward'. */ +"bloganuary.learnMore.modal.button.promptsEnabled" = "開始行動!"; + +/* The second line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.description.second" = "張貼回覆。"; + +/* The third line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.description.third" = "閱讀其他部落客的回覆,汲取靈感並建立新人脈。"; + +/* The first line of the description shown in the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.descriptions.first" = "每天接收新提示,激發靈感。"; + +/* An additional piece of information shown in case the user has the Blogging Prompts feature disabled. */ +"bloganuary.learnMore.modal.footer.addition" = "需啟用「網誌提示」才能參加 Bloganuary。"; + +/* An informative excerpt shown in a subtler tone. */ +"bloganuary.learnMore.modal.footer.text" = "Bloganuary 會透過「每日網誌提示」傳送 1 月的主題。"; + +/* The headline text of the Bloganuary modal sheet. */ +"bloganuary.learnMore.modal.headline" = "參加為期一個月的寫作挑戰"; /* Verb. Dismisses the blogging prompt notification. */ "bloggingPrompt.pushNotification.customActionDescription.dismiss" = "關閉"; @@ -9711,6 +9515,12 @@ Example: Comment on Example: Reply to Pamela Nguyen */ "comment.header.subText.reply" = "回覆 %1$@"; +/* Error message informing a user about an invalid password. */ +"common.reEnterPasswordMessage" = "儲存在應用程式中的使用者名稱或密碼可能已過期。 請在設定中重新輸入密碼,然後再試一次。"; + +/* An error message. */ +"common.unableToConnect" = "無法連線"; + /* Footnote for the privacy compliance popover. */ "compliance.analytics.popover.footnote" = "這些 Cookie 讓我們得以收集使用者與網站互動的相關資訊,進而最佳化效能。"; @@ -9861,50 +9671,92 @@ Example: Reply to Pamela Nguyen */ /* Title for a menu action in the context menu on the Jetpack install card. */ "domain.dashboard.card.menu.hide" = "隱藏此訊息"; -/* Domain Purchase Completion footer */ -"domain.purchase.preview.footer" = "你的自訂網域最多可能需要 30 分鐘才能開始運作。"; +/* Search domain - Title for the Suggested domains screen */ +"domain.management.addDomain.search.title" = "搜尋網域"; -/* Domain Purchase Completion description (only for FREE domains). */ -"domain.purchase.preview.free.description" = "接下來,我們會協助你讓網站可供瀏覽。"; +/* Domain management buy domain card button title */ +"domain.management.buy.card.button.title" = "取得網域"; -/* Domain Purchase Completion description (only for PAID domains). */ -"domain.purchase.preview.paid.description" = "我們已將收據透過電子郵件傳送給你了。 接下來,我們會協助你準備好向所有人推出網站。"; +/* Domain management buy domain card subtitle */ +"domain.management.buy.card.subtitle" = "稍後新增網站。"; -/* Reflects that site is live when domain purchase feature flag is ON. */ -"domain.purchase.preview.title" = "恭喜,你的網站已上線!"; +/* Domain management buy domain card title */ +"domain.management.buy.card.title" = "單純購買網域"; + +/* The expired label of the domain card in All Domains screen. */ +"domain.management.card.expired.label" = "已到期"; + +/* The renews label of the domain card in All Domains screen. */ +"domain.management.card.renews.label" = "續訂"; + +/* The empty state button title in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.button.title" = "尋找網域"; + +/* The empty state description in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.description" = "點選下方,找到完美的網域名稱。"; + +/* The empty state title in All Domains screen when the user doesn't have any domains */ +"domain.management.default.empty.state.title" = "你沒有任何網域"; + +/* The empty state description in All Domains screen when an error occurs */ +"domain.management.error.empty.state.description" = "載入你的網域時發生錯誤。 如果問題持續發生,請聯絡支援人員。"; + +/* The empty state title in All Domains screen when an error occurs */ +"domain.management.error.empty.state.title" = "發生錯誤"; + +/* The empty state button title in All Domains screen when an error occurs */ +"domain.management.error.state.button.title" = "再試一次"; + +/* The empty state description in All Domains screen when the user is offline */ +"domain.management.offline.empty.state.description" = "請檢查你的網路連線並再試一次。"; + +/* The empty state title in All Domains screen when the user is offline */ +"domain.management.offline.empty.state.title" = "沒有網際網路連線"; + +/* Domain management choose site card button title */ +"domain.management.purchase.footer" = "*所有付費年繳方案皆隨附一年免費網域"; -/* Status of a domain in `Action Required` state */ -"domain.status.action.required" = "須採取行動"; +/* Domain management purchase domain screen title */ +"domain.management.purchase.subtitle" = "別擔心,你之後也能輕鬆新增網站。"; -/* Status of a domain in `Active` state */ -"domain.status.active" = "已啟用"; +/* Domain management purchase domain screen title. */ +"domain.management.purchase.title" = "選擇網域的使用方式"; -/* Status of a domain in `Complete Setup` state */ -"domain.status.complete.setup" = "完成設定手續"; +/* The search bar title in All Domains screen. */ +"domain.management.search-bar.title" = "搜尋網域"; -/* Status of a domain in `Error` state */ -"domain.status.error" = "錯誤"; +/* The empty state description in All Domains screen when the are no domains matching the search criteria */ +"domain.management.search.empty.state.description" = "找不到符合「%@」的網域搜尋結果"; -/* Status of a domain in `Expired` state */ -"domain.status.expired" = "已到期"; +/* The empty state title in All Domains screen when the are no domains matching the search criteria */ +"domain.management.search.empty.state.title" = "找不到相符的網域"; -/* Status of a domain in `Expiring Soon` state */ -"domain.status.expiring.soon" = "即將到期"; +/* Domain management choose site card button title */ +"domain.management.site.card.button.title" = "選擇網站"; -/* Status of a domain in `Failed` state */ -"domain.status.failed" = "失敗"; +/* Domain management choose site card subtitle */ +"domain.management.site.card.footer" = "第一年可享免費網域*"; -/* Status of a domain in `In Progress` state */ -"domain.status.in.progress" = "進行中"; +/* Domain management choose site card subtitle */ +"domain.management.site.card.subtitle" = "使用你已開始經營的網站。"; -/* Status of a domain in `Renew` state */ -"domain.status.renew" = "續訂"; +/* Domain management choose site card title */ +"domain.management.site.card.title" = "現有的 WordPress.com 網站"; -/* Status of a domain in `Verify Email` state */ -"domain.status.verify.email" = "驗證電子郵件"; +/* Domain Management Screen Title */ +"domain.management.title" = "所有網域"; -/* Status of a domain in `Verifying` state */ -"domain.status.verifying" = "正在驗證"; +/* Domain Purchase Completion footer */ +"domain.purchase.preview.footer" = "你的自訂網域最多可能需要 30 分鐘才能開始運作。"; + +/* Domain Purchase Completion description (only for FREE domains). */ +"domain.purchase.preview.free.description" = "接下來,我們會協助你讓網站可供瀏覽。"; + +/* Domain Purchase Completion description (only for PAID domains). */ +"domain.purchase.preview.paid.description" = "我們已將收據透過電子郵件傳送給你了。 接下來,我們會協助你準備好向所有人推出網站。"; + +/* Reflects that site is live when domain purchase feature flag is ON. */ +"domain.purchase.preview.title" = "恭喜,你的網站已上線!"; /* The 'Best Alternative' label under the domain name in 'Choose a domain' screen */ "domain.suggestions.row.best-alternative" = "最佳替代選項"; @@ -9927,12 +9779,24 @@ Example: Reply to Pamela Nguyen */ /* The text to display for paid domains in 'Site Creation > Choose a domain' screen */ "domain.suggestions.row.yearly" = "每年"; +/* Title for the checkout screen. */ +"domains.checkout.title" = "結帳"; + /* Action shown in a bottom notice to dismiss it. */ "domains.failure.dismiss" = "關閉"; /* Content show when the domain selection action fails. */ "domains.failure.title" = "抱歉,你嘗試新增的網域目前無法透過 Jetpack 應用程式購買。"; +/* Title for the screen where the user can choose how to use the domain they're end up purchasing. */ +"domains.purchase.choice.title" = "購買網域"; + +/* Back button title that navigates back to the search domains screen. */ +"domains.search.backButton.title" = "搜尋"; + +/* Title of screen where user chooses a site to connect to their selected domain */ +"domains.sitePicker.title" = "選擇網站"; + /* No comment provided by engineer. */ "double-tap to change unit" = "點兩下即可變更單位"; @@ -9950,6 +9814,15 @@ Example: Reply to Pamela Nguyen */ Site Address placeholder */ "example.com" = "example.com"; +/* Title for confirmation navigation bar button item */ +"externalMediaPicker.add" = "新增"; + +/* Bottom toolbar title in the selection mode */ +"externalMediaPicker.toolbarSelectItemsPrompt" = "選取圖片"; + +/* Bottom toolbar title in the selection mode */ +"externalMediaPicker.toolbarViewSelected" = "檢視選擇項目 (%@)"; + /* Title of screen the displays the details of an advertisement campaign. */ "feature.blaze.campaignDetails.title" = "行銷活動詳細資訊"; @@ -10049,9 +9922,6 @@ Example: Reply to Pamela Nguyen */ /* (placeholder) Help the user enter a URL into the field */ "http:\/\/my-site-address (URL)" = "http:\/\/my-site-address (網址)"; -/* Label displayed on image media items. */ -"image" = "圖片"; - /* Sentence to justify why the app is asking permission from the user to use their camera. */ "infoplist.NSCameraUsageDescription" = "拍些相片或影片供文章使用。"; @@ -10352,6 +10222,9 @@ Example: Reply to Pamela Nguyen */ /* Indicating that referrer was marked as spam */ "marked as spam" = "已標示為垃圾訊息"; +/* Products header text in Me Screen. */ +"me.products.header" = "產品"; + /* Title of error prompt shown when a sync fails. */ "media.syncFailed" = "無法同步媒體"; @@ -10364,18 +10237,24 @@ Example: Reply to Pamela Nguyen */ /* Message of an alert informing users that the video they are trying to select is not allowed. */ "mediaExporter.videoLimitExceededError" = "須購買付費方案才能上傳長度超過 5 分鐘的影片。"; -/* Verb. User action to dismiss error alert when failing to load media item. */ -"mediaItemTable.errorAlert.dismissButton" = "關閉"; - /* Accessibility hint for add button to add items to the user's media library */ "mediaLibrary.addButtonAccessibilityHint" = "新增媒體"; /* Accessibility label for add button to add items to the user's media library */ "mediaLibrary.addButtonAccessibilityLabel" = "新增"; +/* Button name in the more menu */ +"mediaLibrary.aspectRatioGrid" = "長寬比網格"; + +/* Context menu button */ +"mediaLibrary.buttonDelete" = "刪除"; + /* Media screen navigation bar button Select title */ "mediaLibrary.buttonSelect" = "選取"; +/* Context menu button */ +"mediaLibrary.buttonShare" = "分享"; + /* Verb. Button title. Tapping cancels an action. */ "mediaLibrary.deleteConfirmationCancel" = "取消"; @@ -10397,6 +10276,21 @@ Example: Reply to Pamela Nguyen */ /* Text displayed in HUD after successfully deleting a media item */ "mediaLibrary.deletionSuccessMessage" = "已刪除!"; +/* The name of the media filter */ +"mediaLibrary.filterAll" = "全部"; + +/* The name of the media filter */ +"mediaLibrary.filterAudio" = "音訊"; + +/* The name of the media filter */ +"mediaLibrary.filterDocuments" = "文件"; + +/* The name of the media filter */ +"mediaLibrary.filterImages" = "圖片"; + +/* The name of the media filter */ +"mediaLibrary.filterVideos" = "影片"; + /* User action to delete un-uploaded media. */ "mediaLibrary.retryOptionsAlert.delete" = "刪除"; @@ -10409,6 +10303,12 @@ Example: Reply to Pamela Nguyen */ /* Message displayed when no results are returned from a media library search. Should match Calypso. */ "mediaLibrary.searchResultsEmptyTitle" = "沒有符合你搜尋條件的媒體"; +/* Text displayed in HUD if there was an error attempting to share a group of media items. */ +"mediaLibrary.sharingFailureMessage" = "無法分享選取的項目。"; + +/* Button name in the more menu */ +"mediaLibrary.squareGrid" = "正方形網格"; + /* Media screen navigation title */ "mediaLibrary.title" = "媒體"; @@ -10430,6 +10330,12 @@ Example: Reply to Pamela Nguyen */ /* The title of the button to dismiss the alert shown when the picked media cannot be imported into stories. */ "mediaPicker.failedMediaExportAlert.dismissButton" = "關閉"; +/* Error message when picked media cannot be imported into stories. */ +"mediaPicker.failedMediaExportAlert.message" = "無法匯出你的媒體。 如果問題持續發生,請透過「我」> 「說明與支援」畫面聯絡我們。"; + +/* Error title when picked media cannot be imported into stories. */ +"mediaPicker.failedMediaExportAlert.title" = "無法匯出媒體"; + /* Message for alert when access to camera is not granted */ "mediaPicker.noCameraAccessMessage" = "此應用程式需要取得權限才能存取相機,以擷取新的媒體。若你願意允許此應用程式功能,請變更隱私設定。"; @@ -10463,6 +10369,12 @@ Example: Reply to Pamela Nguyen */ /* The name of the action in the context menu */ "mediaPicker.takeVideo" = "拍攝影片"; +/* Navigation title for media preview. Example: 1 of 3 */ +"mediaPreview.NofM" = "第 %1$@ 步,共 %2$@ 步"; + +/* Max image size in pixels (e.g. 300x300px) */ +"mediaSizeSlider.valueFormat" = "%1$d × %2$d 像素"; + /* The description in the Delete WordPress screen */ "migration.deleteWordpress.description" = "你似乎仍有安裝 WordPress 應用程式。"; @@ -10475,9 +10387,6 @@ Example: Reply to Pamela Nguyen */ /* The title in the Delete WordPress screen */ "migration.deleteWordpress.title" = "你的裝置不再需要 WordPress 應用程式了"; -/* Primary button title in the migration done screen. */ -"migration.done.actions.primary.title" = "完成"; - /* Footer for the migration done screen. */ "migration.done.footer" = "建議解除安裝裝置上的 WordPress 應用程式以避免資料衝突。"; @@ -10487,6 +10396,9 @@ Example: Reply to Pamela Nguyen */ /* Primary description in the migration done screen. */ "migration.done.primaryDescription" = "我們已轉移你的所有資料和設定。 一切全部順暢銜接。"; +/* Secondary description (second paragraph) in the migration done screen. */ +"migration.done.secondaryDescription" = "現在就在 Jetpack 應用程式上繼續你的 WordPress 旅程!"; + /* Title of the migration done screen. */ "migration.done.title" = "感謝改用 Jetpack!"; @@ -10535,6 +10447,9 @@ Example: Reply to Pamela Nguyen */ /* The title in the migration welcome screen */ "migration.welcome.title" = "歡迎使用 Jetpack!"; +/* Primary button title in the migration done screen. */ +"migrationDone.actions.primaryTitle" = "我們開始吧"; + /* Description for the static screen displayed prompting users to switch the Jetpack app. */ "movedToJetpack.description" = "Jetpack 應用程式具備 WordPress 應用程式所有功能,現在還可獨家使用統計資料、閱讀器、通知等功能。"; @@ -10610,6 +10525,30 @@ Example: Reply to Pamela Nguyen */ /* Message title for when a user has no sites. */ "mySite.noSites.title" = "你沒有任何網站"; +/* Menu title for the add site option */ +"mySite.siteActions.addSite" = "新增網站"; + +/* Button that reveals more site actions */ +"mySite.siteActions.button" = "網站動作"; + +/* Accessibility hint for button used to show more site actions */ +"mySite.siteActions.hint" = "點選以顯示更多網站動作"; + +/* Menu title for the personalize home option */ +"mySite.siteActions.personalizeHome" = "打造個人版首頁"; + +/* Menu title for the change site icon option */ +"mySite.siteActions.siteIcon" = "變更網站圖示"; + +/* Menu title for the change site title option */ +"mySite.siteActions.siteTitle" = "變更網站標題"; + +/* Menu title for the switch site option */ +"mySite.siteActions.switchSite" = "切換網站"; + +/* Menu title for the visit site option */ +"mySite.siteActions.visitSite" = "造訪網站"; + /* Dismiss button title. */ "noResultsViewController.dismissButton" = "關閉"; @@ -10625,14 +10564,20 @@ Example: Reply to Pamela Nguyen */ /* This is one of the buttons we display when prompting the user for a review */ "notifications.appRatings.sendFeedback.yes.buttonTitle" = "傳送意見回饋"; -/* Word separating the current index from the total amount. I.e.: 7 of 9 */ -"of" = "的"; +/* Badge for page cells */ +"pageList.badgeHomepage" = "首頁"; -/* Label displayed on media items that are not video, image, or audio. */ -"other" = "其它"; +/* Badge for page cells */ +"pageList.badgeLocalChanges" = "本機變更"; -/* Promote the page with Blaze. */ -"pages.blaze.actionTitle" = "使用 Blaze 進行宣傳"; +/* Badge for page cells */ +"pageList.badgePendingReview" = "待審中"; + +/* Badge for page cells */ +"pageList.badgePosts" = "文章頁面"; + +/* Badge for page cells */ +"pageList.badgePrivate" = "私人"; /* Subtitle of the theme template homepage cell */ "pages.template.subtitle" = "你的首頁正在使用佈景主題範本,且將於網頁編輯器開啟。"; @@ -10640,6 +10585,36 @@ Example: Reply to Pamela Nguyen */ /* Title of the theme template homepage cell */ "pages.template.title" = "首頁"; +/* Message informing the user that their static homepage page was set successfully */ +"pages.updatePage.successTitle" = "頁面已成功更新"; + +/* Delete option in the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.actionTitle" = "永久刪除"; + +/* Message of the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.alertMessage" = "確定要永久刪除此頁面嗎?"; + +/* Title of the confirmation alert when deleting a page from the trash. */ +"pagesList.deletePermanently.alertTitle" = "是否要永久刪除?"; + +/* Menu option for filtering posts by everyone */ +"pagesList.pagesByEveryone" = "所有人的頁面"; + +/* Menu option for filtering posts by me */ +"pagesList.pagesByMe" = "我的頁面"; + +/* Trash option in the trash page confirmation alert. */ +"pagesList.trash.actionTitle" = "移至垃圾桶"; + +/* Message of the trash page confirmation alert. */ +"pagesList.trash.alertMessage" = "確定要將此頁面移至垃圾桶嗎?"; + +/* Title of the trash page confirmation alert. */ +"pagesList.trash.alertTitle" = "是否要將此頁面移至垃圾桶?"; + +/* Cancels an Action */ +"pagesList.trash.cancel" = "取消"; + /* No comment provided by engineer. */ "password" = "密碼"; @@ -10679,6 +10654,51 @@ Example: Reply to Pamela Nguyen */ /* Register Domain - Domain contact information field Phone */ "phone number" = "電話號碼"; +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.createdTimeAgo" = "已建立:%@"; + +/* Status mesasge for post cells */ +"post.deletingPostPermanentlyStatusMessage" = "正在刪除文章..."; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.editedTimeAgo" = "已編輯:%@"; + +/* Status mesasge for post cells */ +"post.movingToTrashStatusMessage" = "正在將文章移至垃圾桶..."; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.publishedTimeAgo" = "已發表:%@"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.scheduledForDate" = "已排程:%@"; + +/* Post status and date for list cells with %@ a placeholder for the date. */ +"post.trashedTimeAgo" = "已移至垃圾桶:%@"; + +/* Accessibility label for the post author in the post list. The parameter is the author name. For example, \"By Elsa.\" */ +"postList.a11y.authorChunkFormat" = "作者:%@。"; + +/* Accessibility label for a post's excerpt in the post list. The parameter is the post excerpt. For example, \"Excerpt. This is the first paragraph.\" */ +"postList.a11y.exerptChunkFormat" = "文章摘要。%@。"; + +/* Accessibility label for a sticky post in the post list. */ +"postList.a11y.sticky" = "置頂文章。"; + +/* Accessibility label for a post in the post list. The first placeholder is the post title. The second placeholder is the date. */ +"postList.a11y.titleAndDateChunkFormat" = "%1$@,%2$@。"; + +/* Title for the 'Trash' post list row swipe action */ +"postList.swipeActionDelete" = "垃圾桶"; + +/* Title for the 'Delete' post list row swipe action */ +"postList.swipeActionDeletePermanently" = "刪除"; + +/* Title for the 'Share' post list row swipe action */ +"postList.swipeActionShare" = "分享"; + +/* Title for the 'View' post list row swipe action */ +"postList.swipeActionView" = "檢視"; + /* User action to dismiss featured media options. */ "postSettings.featuredImageUploadActionSheet.dismiss" = "關閉"; @@ -10697,9 +10717,84 @@ Example: Reply to Pamela Nguyen */ /* Button in Post Settings */ "postSettings.setFeaturedImageButton" = "設定精選圖片"; +/* Error message on post/page settings screen */ +"postSettings.updateFailedMessage" = "無法更新文章設定"; + /* Promote the post with Blaze. */ "posts.blaze.actionTitle" = "使用 Blaze 進行宣傳"; +/* Label for the Post List option that cancels automatic uploading of a post. */ +"posts.cancelUpload.actionTitle" = "取消上傳"; + +/* Label for post comments option. Tapping displays comments for a post. */ +"posts.comments.actionTitle" = "留言"; + +/* Label for the delete post option. Tapping permanently deletes a post. */ +"posts.delete.actionTitle" = "永久刪除"; + +/* Label for an option that moves a post to the draft folder */ +"posts.draft.actionTitle" = "移至草稿"; + +/* Label for post duplicate option. Tapping creates a copy of the post. */ +"posts.duplicate.actionTitle" = "複製"; + +/* Opens a submenu for page attributes. */ +"posts.pageAttributes.actionTitle" = "頁面屬性"; + +/* Label for the preview post button. Tapping displays the post as it appears on the web. */ +"posts.preview.actionTitle" = "預覽"; + +/* Label for an option that moves a publishes a post immediately */ +"posts.publish.actionTitle" = "立即發表"; + +/* Retry uploading the post. */ +"posts.retry.actionTitle" = "重試"; + +/* Set the selected page as the homepage. */ +"posts.setHomepage.actionTitle" = "設定為首頁"; + +/* Set the parent page for the selected page. */ +"posts.setParent.actionTitle" = "設定上層項目"; + +/* Set the selected page as a posts page. */ +"posts.setPostsPage.actionTitle" = "設定為文章頁面"; + +/* Set the selected page as a regular page. */ +"posts.setRegularPage.actionTitle" = "設定為一般頁面"; + +/* Label for post settings option. Tapping displays settings for a post. */ +"posts.settings.actionTitle" = "設定"; + +/* Share the post. */ +"posts.share.actionTitle" = "分享"; + +/* Label for post stats option. Tapping displays statistics for a post. */ +"posts.stats.actionTitle" = "統計資料"; + +/* Label for a option that moves a post to the trash folder */ +"posts.trash.actionTitle" = "移至垃圾桶"; + +/* Label for the view post button. Tapping displays the post as it appears on the web. */ +"posts.view.actionTitle" = "檢視"; + +/* A short message explaining that a page was deleted permanently. */ +"postsList.deletePage.message" = "永久刪除的頁面"; + +/* A short message explaining that a post was deleted permanently. */ +"postsList.deletePost.message" = "永久刪除的文章"; + +/* A short message explaining that a page was moved to the trash bin. */ +"postsList.movePageToTrash.message" = "頁面已移至垃圾桶。"; + +/* A short message explaining that a post was moved to the trash bin. */ +"postsList.movePostToTrash.message" = "文章已移至垃圾桶。"; + +/* Menu option for filtering posts by everyone */ +"postsList.postsByEveryone" = "所有人的文章"; + +/* Menu option for filtering posts by me */ +"postsList.postsByMe" = "我的文章"; + /* Title for the button to subscribe to Jetpack Social on the remaining shares view */ "postsettings.social.remainingshares.subscribe" = "立即訂購以提高分享次數"; @@ -10727,6 +10822,12 @@ This string is displayed when some of the social accounts are turned off for aut Example: Sharing to 2 of 3 accounts */ "prepublishing.social.label.partialConnections" = "分享至 %1$d 個帳號 (總額度為 %2$d 個帳號)"; +/* The primary label for the auto-sharing row on the pre-publishing sheet. +Indicates the blog post will be shared to a social media account. +%1$@ is a placeholder for the account name. +Example: Sharing to @wordpress */ +"prepublishing.social.label.singleConnection" = "分享至 %1$@"; + /* A subtext that's shown below the primary label in the auto-sharing row on the pre-publishing sheet. Informs the remaining limit for post auto-sharing. %1$d is a placeholder for the remaining shares. @@ -10838,13 +10939,15 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Text for the 'Like' button on the reader post card cell. */ "reader.post.button.like" = "讚"; -/* Accessibility hint for the like button on the reader post card cell - Accessibility hint for the liked button on the reader post card cell */ +/* Accessibility hint for the like button on the reader post card cell */ "reader.post.button.like.accessibility.hint" = "對文章按讚。"; /* Text for the 'Liked' button on the reader post card cell. */ "reader.post.button.liked" = "已按讚"; +/* Accessibility hint for the liked button on the reader post card cell */ +"reader.post.button.liked.accessibility.hint" = "取消按讚此文章。"; + /* Accessibility hint for the site header on the reader post card cell */ "reader.post.button.menu.accessibility.hint" = "開啟選單,查看更多動作。"; @@ -10908,6 +11011,12 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Title for the tooltip anchor. */ "readerDetail.tooltipAnchorTitle.accessibilityLabel" = "新增"; +/* The button title for the transfer footer view in Register Domain screen */ +"register.domain.transfer.button.title" = "轉移網域"; + +/* The title for the transfer footer view in Register Domain screen */ +"register.domain.transfer.title" = "想轉移你擁有的網域嗎?"; + /* Information of what related post are and how they are presented */ "relatedPostsSettings.optionsFooter" = "「相關文章」會在你的文章下方顯示你網站上的相關內容。"; @@ -11007,6 +11116,21 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Accessibility hint for actions when displaying media items. */ "siteMedia.cellAccessibilityHint" = "選取媒體。"; +/* Accessibility hint for media item preview for user's viewing an item in their media library */ +"siteMediaItem.contentViewAccessibilityHint" = "點選即可在全螢幕中檢視媒體"; + +/* Accessibility label for media item preview for user's viewing an item in their media library */ +"siteMediaItem.contentViewAccessibilityLabel" = "預覽媒體"; + +/* Title for confirmation navigation bar button item */ +"siteMediaPicker.add" = "新增"; + +/* Button selection media in media picker */ +"siteMediaPicker.deselect" = "取消選取"; + +/* Button selection media in media picker */ +"siteMediaPicker.select" = "選取"; + /* Media screen navigation title */ "siteMediaPicker.title" = "媒體"; @@ -11014,7 +11138,7 @@ Example: given a notice format "Following %@" and empty site name, this will be "siteSettings.privacy.title" = "隱私權"; /* Hint for users when hidden privacy setting is set */ -"siteVisibility.hidden.hint" = "所有人都能看見你的網站,但系統會要求搜尋引擎不要將你的網站加入索引。"; +"siteVisibility.hidden.hint" = "除非網站準備好供訪客瀏覽,否則訪客看不到網站,只看得到「即將推出」通知。"; /* Text for privacy settings: Hidden */ "siteVisibility.hidden.title" = "已隱藏"; @@ -11175,6 +11299,12 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Dismiss the AlertView */ "stockPhotos.strings.dismiss" = "關閉"; +/* Subtitle for placeholder in Free Photos. The company name 'Pexels' should always be written as it is. */ +"stockPhotos.subtitle" = "由 Pexels 提供的相片"; + +/* Title for placeholder in Free Photos */ +"stockPhotos.title" = "搜尋免費相片以新增至你的媒體庫!"; + /* Section title for prominent suggestions */ "suggestions.section.prominent" = "在此討論中"; @@ -11322,6 +11452,9 @@ Example: given a notice format "Following %@" and empty site name, this will be /* View title for Help & Support page. */ "support.title" = "說明"; +/* Title for placeholder in Tenor picker */ +"tenor.welcomeMessage" = "搜尋 GIF 以新增至你的媒體庫!"; + /* Header of delete screen section listing things that will be deleted. */ "these items will be deleted:" = "這些項目將會刪除:"; @@ -11337,9 +11470,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Displayed in the confirmation alert when marking unread notifications as read. */ "unread" = "未讀"; -/* Label displayed on video media items. */ -"video" = "影片"; - /* Portion of a message for Jetpack users that have multisite WP installation, thus Restore is not available. This part is a link, colored with a different color. */ "visit our documentation page" = "請造訪我們的文件頁面"; diff --git a/WordPress/UITests/JetpackUITests.xctestplan b/WordPress/UITests/JetpackUITests.xctestplan index 320a39381980..036ebc079947 100644 --- a/WordPress/UITests/JetpackUITests.xctestplan +++ b/WordPress/UITests/JetpackUITests.xctestplan @@ -16,11 +16,11 @@ "identifier" : "FABB1F8F2602FC2C00C8785C", "name" : "Jetpack" }, - "testRepetitionMode" : "retryOnFailure" + "testRepetitionMode" : "retryOnFailure", + "uiTestingScreenshotsLifetime" : "keepAlways" }, "testTargets" : [ { - "parallelizable" : true, "skippedTests" : [ "EditorAztecTests", "LoginTests\/testEmailMagicLinkLogin()", diff --git a/WordPress/UITests/Tests/AppSettingsTests.swift b/WordPress/UITests/Tests/AppSettingsTests.swift new file mode 100644 index 000000000000..2589f0477553 --- /dev/null +++ b/WordPress/UITests/Tests/AppSettingsTests.swift @@ -0,0 +1,60 @@ +import UITestsFoundation +import XCTest + +final class AppSettingsTests: XCTestCase { + + let testsRequiringAppDeletion = [ + "testImageOptimizationEnabledByDefault", + "testImageOptimizationIsTurnedOnEditor", + "testImageOptimizationIsTurnedOffEditor" + ] + + @MainActor + override func setUpWithError() throws { + try super.setUpWithError() + + let removeBeforeLaunching = testsRequiringAppDeletion.contains { testName in + self.name.contains(testName) + } + setUpTestSuite(removeBeforeLaunching: removeBeforeLaunching) + + try LoginFlow + .login(email: WPUITestCredentials.testWPcomUserEmail) + } + + override func tearDownWithError() throws { + try super.tearDownWithError() + takeScreenshotOfFailedTest() + } + + func testImageOptimizationEnabledByDefault() throws { + try TabNavComponent() + .goToMeScreen() + .goToAppSettings() + .verifyImageOptimizationSwitch(enabled: true) + } + + func testImageOptimizationIsTurnedOnEditor() throws { + try TabNavComponent() + .goToBlockEditorScreen() + .addImage() + .chooseOptimizeImages(option: true) + .closeEditor() + try TabNavComponent() + .goToMeScreen() + .goToAppSettings() + .verifyImageOptimizationSwitch(enabled: true) + } + + func testImageOptimizationIsTurnedOffEditor() throws { + try TabNavComponent() + .goToBlockEditorScreen() + .addImage() + .chooseOptimizeImages(option: false) + .closeEditor() + try TabNavComponent() + .goToMeScreen() + .goToAppSettings() + .verifyImageOptimizationSwitch(enabled: false) + } +} diff --git a/WordPress/UITests/Tests/DashboardTests.swift b/WordPress/UITests/Tests/DashboardTests.swift index 5f6d89225a8b..58aefaec71f0 100644 --- a/WordPress/UITests/Tests/DashboardTests.swift +++ b/WordPress/UITests/Tests/DashboardTests.swift @@ -25,6 +25,7 @@ class DashboardTests: XCTestCase { .verifyFreeToPaidPlansCard() .tapFreeToPaidPlansCard() .assertScreenIsLoaded() + .searchDomain() .selectDomain() .goToPlanSelection() .assertScreenIsLoaded() diff --git a/WordPress/UITests/Tests/LoginTests.swift b/WordPress/UITests/Tests/LoginTests.swift index 822dc50e0fb5..7da16cd8226d 100644 --- a/WordPress/UITests/Tests/LoginTests.swift +++ b/WordPress/UITests/Tests/LoginTests.swift @@ -24,7 +24,6 @@ class LoginTests: XCTestCase { siteUrl: WPUITestCredentials.testWPcomPaidSite ) .continueWithSelectedSite() - .dismissNotificationAlertIfNeeded() try TabNavComponent() .goToMeScreen() .logoutToPrologue() diff --git a/WordPress/UITests/Tests/StatsTests.swift b/WordPress/UITests/Tests/StatsTests.swift index a56ee15e3774..479da2a2de05 100644 --- a/WordPress/UITests/Tests/StatsTests.swift +++ b/WordPress/UITests/Tests/StatsTests.swift @@ -22,42 +22,43 @@ class StatsTests: XCTestCase { takeScreenshotOfFailedTest() } - let insightsStats: [String] = [ - "Your views in the last 7-days are -9 (-82%) lower than the previous 7-days. ", - "Thursday", - "34% of views", - "Best Hour", - "4 AM", - "25% of views" - ] - - let yearsStats: [String] = [ - "9,148", - "+7,933 (653%)", - "United States, 60", - "Canada, 44", - "Germany, 15", - "France, 14", - "United Kingdom, 12", - "India, 121" - ] - - let yearsChartBars: [String] = [ - "Views, 2019: 9148", - "Visitors, 2019: 4216", - "Views, 2018: 1215", - "Visitors, 2018: 632", - "Views, 2017: 788", - "Visitors, 2017: 465" - ] - func testInsightsStatsLoadProperly() throws { + let insightsStats: [String] = [ + "Your views in the last 7-days are -9 (-82%) lower than the previous 7-days. ", + "Thursday", + "34% of views", + "Best Hour", + "4 AM", + "25% of views" + ] + try StatsScreen() .switchTo(mode: "insights") .assertStatsAreLoaded(insightsStats) } func testYearsStatsLoadProperly() throws { + let yearsStats: [String] = [ + "9,148", + "+7,933 (653%)", + "United States, 60", + "Canada, 44", + "Germany, 15", + "France, 14", + "United Kingdom, 12", + "India, 121" + ] + + let currentYear = Calendar.current.component(.year, from: Date()) + let yearsChartBars: [String] = [ + "Views, \(currentYear): 9148", + "Visitors, \(currentYear): 4216", + "Views, \(currentYear - 1): 1215", + "Visitors, \(currentYear - 1): 632", + "Views, \(currentYear - 2): 788", + "Visitors, \(currentYear - 2): 465" + ] + try StatsScreen() .switchTo(mode: "years") .assertStatsAreLoaded(yearsStats) diff --git a/WordPress/UITests/WordPressUITests.xctestplan b/WordPress/UITests/WordPressUITests.xctestplan index c46d842a07ba..47e4390b8467 100644 --- a/WordPress/UITests/WordPressUITests.xctestplan +++ b/WordPress/UITests/WordPressUITests.xctestplan @@ -16,7 +16,8 @@ "identifier" : "1D6058900D05DD3D006BFB54", "name" : "WordPress" }, - "testRepetitionMode" : "retryOnFailure" + "testRepetitionMode" : "retryOnFailure", + "uiTestingScreenshotsLifetime" : "keepAlways" }, "testTargets" : [ { diff --git a/WordPress/UITestsFoundation/Globals.swift b/WordPress/UITestsFoundation/Globals.swift index 4d211e0ec376..cac30515d342 100644 --- a/WordPress/UITestsFoundation/Globals.swift +++ b/WordPress/UITestsFoundation/Globals.swift @@ -45,7 +45,7 @@ public func waitForExistenceAndTap(_ element: XCUIElement, timeout: TimeInterval element.tap() } -public func waitAndTap( _ element: XCUIElement, maxRetries: Int = 10) { +public func waitAndTap( _ element: XCUIElement, maxRetries: Int = 20) { var retries = 0 while retries < maxRetries { if element.isHittable { @@ -53,7 +53,7 @@ public func waitAndTap( _ element: XCUIElement, maxRetries: Int = 10) { break } - usleep(500000) // a 0.5 second delay before retrying + usleep(250000) // a 0.25 second delay before retrying retries += 1 } @@ -62,23 +62,6 @@ public func waitAndTap( _ element: XCUIElement, maxRetries: Int = 10) { } } -public func tap(element: XCUIElement, untilAppears elementToAppear: XCUIElement, maxRetries: Int = 10) { - var retries = 0 - while retries < maxRetries { - if !elementToAppear.exists { - element.tap() - break - } - - usleep(500000) // a 0.5 second delay before retrying - retries += 1 - } - - if retries == maxRetries { - XCTFail("Expected element (\(elementToAppear)) still does not exist after \(maxRetries) tries.") - } -} - public func waitForElementToDisappear( _ element: XCUIElement, maxRetries: Int = 10) { var retries = 0 while retries < maxRetries { diff --git a/WordPress/UITestsFoundation/Screens/ActivityLogScreen.swift b/WordPress/UITestsFoundation/Screens/ActivityLogScreen.swift index 7377f1fc96b0..ee75731d1367 100644 --- a/WordPress/UITestsFoundation/Screens/ActivityLogScreen.swift +++ b/WordPress/UITestsFoundation/Screens/ActivityLogScreen.swift @@ -12,8 +12,11 @@ public class ActivityLogScreen: ScreenObject { $0.buttons["Activity Type"].firstMatch } - var dateRangeButton: XCUIElement { dateRangeButtonGetter(app) } var activityTypeButton: XCUIElement { activityTypeButtonGetter(app) } + var dateRangeButton: XCUIElement { dateRangeButtonGetter(app) } + + // Timeout duration to overwrite value defined in XCUITestHelpers + var duration: TimeInterval = 10.0 public init(app: XCUIApplication = XCUIApplication()) throws { tabBar = try TabNavComponent() @@ -31,7 +34,7 @@ public class ActivityLogScreen: ScreenObject { @discardableResult public func verifyActivityLogScreen(hasActivityPartial activityTitle: String) -> Self { XCTAssertTrue( - app.staticTexts.matching(NSPredicate(format: "label CONTAINS[c] %@", activityTitle)).firstMatch.waitForIsHittable(), + app.staticTexts.matching(NSPredicate(format: "label CONTAINS[c] %@", activityTitle)).firstMatch.waitForIsHittable(timeout: duration), "Activity Log Screen: \"\(activityTitle)\" activity not displayed.") return self } diff --git a/WordPress/UITestsFoundation/Screens/DomainsSuggestionsScreen.swift b/WordPress/UITestsFoundation/Screens/DomainsSelectionScreen.swift similarity index 67% rename from WordPress/UITestsFoundation/Screens/DomainsSuggestionsScreen.swift rename to WordPress/UITestsFoundation/Screens/DomainsSelectionScreen.swift index 14220db43bc7..5a98de44d7ed 100644 --- a/WordPress/UITestsFoundation/Screens/DomainsSuggestionsScreen.swift +++ b/WordPress/UITestsFoundation/Screens/DomainsSelectionScreen.swift @@ -1,10 +1,10 @@ import ScreenObject import XCTest -public class DomainsSuggestionsScreen: ScreenObject { +public class DomainsSelectionScreen: ScreenObject { private let domainSuggestionsTableGetter: (XCUIApplication) -> XCUIElement = { - $0.tables["DomainSuggestionsTable"] + $0.tables["DomainSelectionTable"] } private let selectDomainButtonGetter: (XCUIApplication) -> XCUIElement = { @@ -15,6 +15,11 @@ public class DomainsSuggestionsScreen: ScreenObject { $0.staticTexts["Search domains"] } + private let searchTextFieldGetter: (XCUIApplication) -> XCUIElement = { + $0.searchFields.firstMatch + } + + var searchTextField: XCUIElement { searchTextFieldGetter(app) } var domainSuggestionsTable: XCUIElement { domainSuggestionsTableGetter(app) } var selectDomainButton: XCUIElement { selectDomainButtonGetter(app) } var siteDomainsNavbarHeader: XCUIElement { siteDomainsNavbarHeaderGetter(app) } @@ -27,12 +32,18 @@ public class DomainsSuggestionsScreen: ScreenObject { } public static func isLoaded() -> Bool { - (try? DomainsSuggestionsScreen().isLoaded) ?? false + (try? DomainsSelectionScreen().isLoaded) ?? false + } + + public func searchDomain() throws -> Self { + searchTextField.tap() + searchTextField.typeText("domainexample.blog") + return self } @discardableResult public func selectDomain() throws -> Self { - domainSuggestionsTable.cells.lastMatch?.tap() + domainSuggestionsTable.cells.firstMatch.tap() return self } diff --git a/WordPress/UITestsFoundation/Screens/Editor/BlockEditorScreen.swift b/WordPress/UITestsFoundation/Screens/Editor/BlockEditorScreen.swift index 538bab40667b..4def7eedb799 100644 --- a/WordPress/UITestsFoundation/Screens/Editor/BlockEditorScreen.swift +++ b/WordPress/UITestsFoundation/Screens/Editor/BlockEditorScreen.swift @@ -96,6 +96,14 @@ public class BlockEditorScreen: ScreenObject { $0.staticTexts["You have unsaved changes."] } + private let leaveOnImageOptimizationButtonGetter: (XCUIApplication) -> XCUIElement = { + $0.buttons["Yes, leave on"] + } + + private let turnOffImageOptimizationButtonGetter: (XCUIApplication) -> XCUIElement = { + $0.buttons["No, turn off"] + } + var addBlockButton: XCUIElement { addBlockButtonGetter(app) } var chooseFromDeviceButton: XCUIElement { chooseFromDeviceButtonGetter(app) } var closeButton: XCUIElement { closeButtonGetter(app) } @@ -119,6 +127,8 @@ public class BlockEditorScreen: ScreenObject { var switchToHTMLModeButton: XCUIElement { switchToHTMLModeButtonGetter(app) } var undoButton: XCUIElement { undoButtonGetter(app) } var unsavedChangesLabel: XCUIElement { unsavedChangesLabelGetter(app) } + var leaveOnImageOptimizationButton: XCUIElement { leaveOnImageOptimizationButtonGetter(app) } + var turnOffImageOptimizationButton: XCUIElement { turnOffImageOptimizationButtonGetter(app) } public init(app: XCUIApplication = XCUIApplication()) throws { // The block editor has _many_ elements but most are loaded on-demand. To verify the screen @@ -187,10 +197,32 @@ public class BlockEditorScreen: ScreenObject { public func addImageGallery() throws -> BlockEditorScreen { addBlock("Gallery block") try addMultipleImages(numberOfImages: 3) + try dismissImageOptimizationPopupIfNeeded() + + return self + } + + /** + Chooses the option of Optimize Images popup. + */ + @discardableResult + public func chooseOptimizeImages(option: Bool) throws -> BlockEditorScreen { + if option { + leaveOnImageOptimizationButton.tap() + } + else { + turnOffImageOptimizationButton.tap() + } return self } + public func dismissImageOptimizationPopupIfNeeded() throws { + if leaveOnImageOptimizationButton.waitForIsHittable() { + try chooseOptimizeImages(option: true) + } + } + public func addVideoFromUrl(urlPath: String) -> Self { addMediaBlockFromUrl( blockType: "Video block", @@ -430,6 +462,7 @@ public class BlockEditorScreen: ScreenObject { private func addMultipleImages(numberOfImages: Int) throws { try chooseFromDevice() .selectMultipleImages(numberOfImages) + try dismissImageOptimizationPopupIfNeeded() } private func chooseFromDevice() throws -> PHPickerScreen { diff --git a/WordPress/UITestsFoundation/Screens/Editor/EditorPostSettings.swift b/WordPress/UITestsFoundation/Screens/Editor/EditorPostSettings.swift index bececdaab531..d05a37823b64 100644 --- a/WordPress/UITestsFoundation/Screens/Editor/EditorPostSettings.swift +++ b/WordPress/UITestsFoundation/Screens/Editor/EditorPostSettings.swift @@ -39,6 +39,10 @@ public class EditorPostSettings: ScreenObject { $0.buttons["Next Month"] } + private let monthLabelGetter: (XCUIApplication) -> XCUIElement = { + $0.buttons["Month"] + } + private let firstCalendarDayButtonGetter: (XCUIApplication) -> XCUIElement = { $0.buttons.containing(.staticText, identifier: "1").element } @@ -54,6 +58,7 @@ public class EditorPostSettings: ScreenObject { var doneButton: XCUIElement { doneButtonGetter(app) } var featuredImageButton: XCUIElement { featuredImageButtonGetter(app) } var firstCalendarDayButton: XCUIElement { firstCalendarDayButtonGetter(app) } + var monthLabel: XCUIElement { monthLabelGetter(app) } var nextMonthButton: XCUIElement { nextMonthButtonGetter(app) } var publishDateButton: XCUIElement { publishDateButtonGetter(app) } var settingsTable: XCUIElement { settingsTableGetter(app) } @@ -139,10 +144,16 @@ public class EditorPostSettings: ScreenObject { public func updatePublishDateToFutureDate() -> Self { publishDateButton.tap() dateSelector.tap() + let currentMonth = monthLabel.value as! String // Selects the first day of the next month nextMonthButton.tap() - firstCalendarDayButton.tap() + + // To ensure that the day tap happens on the correct month + let nextMonth = monthLabel.value as! String + if nextMonth != currentMonth { + firstCalendarDayButton.tapUntil(.selected, failureMessage: "First Day button not selected!") + } doneButton.tap() return self diff --git a/WordPress/UITestsFoundation/Screens/Login/LoginEpilogueScreen.swift b/WordPress/UITestsFoundation/Screens/Login/LoginEpilogueScreen.swift index 712511a23599..7c17365c564d 100644 --- a/WordPress/UITestsFoundation/Screens/Login/LoginEpilogueScreen.swift +++ b/WordPress/UITestsFoundation/Screens/Login/LoginEpilogueScreen.swift @@ -20,7 +20,7 @@ public class LoginEpilogueScreen: ScreenObject { } private let siteNameSearchFieldGetter: (XCUIApplication) -> XCUIElement = { - $0.searchFields["Type a name for your site"] + $0.searchFields.firstMatch } private let createSiteButtonGetter: (XCUIApplication) -> XCUIElement = { @@ -112,7 +112,7 @@ public class LoginEpilogueScreen: ScreenObject { public func chooseDomainName(_ domainName: String) -> Self { siteNameSearchField.tap() siteNameSearchField.typeText(domainName) - app.staticTexts[domainName] .tap() + app.staticTexts[domainName].tap() createSiteButton.tap() return self diff --git a/WordPress/UITestsFoundation/Screens/Login/LoginUsernamePasswordScreen.swift b/WordPress/UITestsFoundation/Screens/Login/LoginUsernamePasswordScreen.swift index a83320603dfb..fedaffef865d 100644 --- a/WordPress/UITestsFoundation/Screens/Login/LoginUsernamePasswordScreen.swift +++ b/WordPress/UITestsFoundation/Screens/Login/LoginUsernamePasswordScreen.swift @@ -40,13 +40,11 @@ public class LoginUsernamePasswordScreen: ScreenObject { public func proceedWithSelfHostedSiteAddedFromSitesList(username: String, password: String) throws -> MySitesScreen { fill(username: username, password: password) - return try MySitesScreen() } public func proceedWithSelfHosted(username: String, password: String) throws -> MySiteScreen { fill(username: username, password: password) - return try MySiteScreen() } @@ -66,6 +64,10 @@ public class LoginUsernamePasswordScreen: ScreenObject { passwordTextField.typeText(password) } nextButton.tap() + + if #available(iOS 17.2, *) { + app.dismissSavePasswordPrompt() + } } private func dismissQuickStartPromptIfNeeded() throws { diff --git a/WordPress/UITestsFoundation/Screens/Login/Unified/PasswordScreen.swift b/WordPress/UITestsFoundation/Screens/Login/Unified/PasswordScreen.swift index a16961a7d098..3cd51366cb9e 100644 --- a/WordPress/UITestsFoundation/Screens/Login/Unified/PasswordScreen.swift +++ b/WordPress/UITestsFoundation/Screens/Login/Unified/PasswordScreen.swift @@ -29,7 +29,6 @@ public class PasswordScreen: ScreenObject { @discardableResult public func proceedWithValidPassword() throws -> LoginEpilogueScreen { try tryProceed(password: "pw") - return try LoginEpilogueScreen() } @@ -59,6 +58,15 @@ public class PasswordScreen: ScreenObject { passwordTextField.typeText(password) continueButton.tap() + + // iOS 16.4 introduced a prompt to save passwords in the keychain. + // Prior to iOS 17.2, we used a test observer (see TestObserver.swift) to disable storing passwords before the tests started. + // Xcode 15.1 and iOS 17.2 have what at the time of writing looks like a bug in the Settings app which breaks that approach. + // As soon as the passwords screen is pushed in the Settings navigation stack, it's immediately popped back. + // For the time being, let's manually dismiss the prompt on demand. + if #available(iOS 17.2, *) { + app.dismissSavePasswordPrompt() + } } @discardableResult diff --git a/WordPress/UITestsFoundation/Screens/Me/AppSettingsScreen.swift b/WordPress/UITestsFoundation/Screens/Me/AppSettingsScreen.swift new file mode 100644 index 000000000000..36c2be8f2eb2 --- /dev/null +++ b/WordPress/UITestsFoundation/Screens/Me/AppSettingsScreen.swift @@ -0,0 +1,45 @@ +import ScreenObject +import XCTest + +public class AppSettingsScreen: ScreenObject { + + private let backButtonGetter: (XCUIApplication) -> XCUIElement = { + $0.navigationBars.buttons.element(boundBy: 0) + } + + private let imageOptimizationSwitchGetter: (XCUIApplication) -> XCUIElement = { + $0.switches["imageOptimizationSwitch"] + } + + var backButton: XCUIElement { backButtonGetter(app) } + var imageOptimizationSwitch: XCUIElement { imageOptimizationSwitchGetter(app) } + + init(app: XCUIApplication = XCUIApplication()) throws { + try super.init( + expectedElementGetters: [ + imageOptimizationSwitchGetter, + ], + app: app + ) + } + + public func tapImageOptimizationSwitch() throws -> Self { + imageOptimizationSwitch.tap() + return self + } + + @discardableResult + public func verifyImageOptimizationSwitch(enabled: Bool) -> Self { + XCTAssertEqual(imageOptimizationSwitch.value as? String, enabled ? "1" : "0") + return self + } + + public func dismiss() throws -> MeTabScreen { + backButton.tap() + return try MeTabScreen() + } + + static func isLoaded() -> Bool { + (try? AppSettingsScreen().isLoaded) ?? false + } +} diff --git a/WordPress/UITestsFoundation/Screens/MeTabScreen.swift b/WordPress/UITestsFoundation/Screens/MeTabScreen.swift index 05cf55cebc8e..12d39413ac2e 100644 --- a/WordPress/UITestsFoundation/Screens/MeTabScreen.swift +++ b/WordPress/UITestsFoundation/Screens/MeTabScreen.swift @@ -96,4 +96,10 @@ public class MeTabScreen: ScreenObject { return try MySiteScreen() } + + public func goToAppSettings() throws -> AppSettingsScreen { + appSettingsButton.tap() + + return try AppSettingsScreen() + } } diff --git a/WordPress/UITestsFoundation/Screens/MySiteScreen.swift b/WordPress/UITestsFoundation/Screens/MySiteScreen.swift index 7d30074d5ea0..f64833667779 100644 --- a/WordPress/UITestsFoundation/Screens/MySiteScreen.swift +++ b/WordPress/UITestsFoundation/Screens/MySiteScreen.swift @@ -96,10 +96,13 @@ public class MySiteScreen: ScreenObject { $0.buttons["site-url-button"] } - private let switchSiteButtonGetter: (XCUIApplication) -> XCUIElement = { - $0.buttons["switch-site-button"] + private let siteActionButtonGetter: (XCUIApplication) -> XCUIElement = { + $0.buttons["site-action-button"] } + private let switchSiteButtonGetter: (XCUIApplication) -> XCUIElement = { + $0.buttons["Switch site"] + } var activityLogCard: XCUIElement { activityLogCardGetter(app) } var activityLogCardHeaderButton: XCUIElement { activityLogCardHeaderButtonGetter(app) } var blogDetailsRemoveSiteButton: XCUIElement { blogDetailsRemoveSiteButtonGetter(app) } @@ -121,15 +124,16 @@ public class MySiteScreen: ScreenObject { var segmentedControlMenuButton: XCUIElement { segmentedControlMenuButtonGetter(app) } var siteTitleButton: XCUIElement { siteTitleButtonGetter(app) } var siteUrlButton: XCUIElement { siteUrlButtonGetter(app) } + var siteActionButton: XCUIElement { siteActionButtonGetter(app) } var switchSiteButton: XCUIElement { switchSiteButtonGetter(app) } // Timeout duration to overwrite value defined in XCUITestHelpers - var duration: TimeInterval = 5.0 + var duration: TimeInterval = 10.0 public init(app: XCUIApplication = XCUIApplication()) throws { try super.init( expectedElementGetters: [ - switchSiteButtonGetter, + siteActionButtonGetter, createButtonGetter ], app: app @@ -137,6 +141,7 @@ public class MySiteScreen: ScreenObject { } public func showSiteSwitcher() throws -> MySitesScreen { + siteActionButton.tap() switchSiteButton.tap() return try MySitesScreen() } @@ -224,9 +229,9 @@ public class MySiteScreen: ScreenObject { } @discardableResult - public func tapFreeToPaidPlansCard() throws -> DomainsSuggestionsScreen { + public func tapFreeToPaidPlansCard() throws -> DomainsSelectionScreen { freeToPaidPlansCardButton.tap() - return try DomainsSuggestionsScreen() + return try DomainsSelectionScreen() } @discardableResult diff --git a/WordPress/UITestsFoundation/Screens/NotificationsScreen.swift b/WordPress/UITestsFoundation/Screens/NotificationsScreen.swift index 301273b55e6c..56ede9a43f83 100644 --- a/WordPress/UITestsFoundation/Screens/NotificationsScreen.swift +++ b/WordPress/UITestsFoundation/Screens/NotificationsScreen.swift @@ -84,7 +84,12 @@ public class NotificationsScreen: ScreenObject { } public func replyToComment(withText text: String) -> Self { - tap(element: replyCommentButton, untilAppears: replyTextView) + replyCommentButton.tapUntil( + element: replyTextView, + matches: .exists, + failureMessage: "Reply Text View does not exists!" + ) + replyTextView.typeText(text) replyButton.tap() @@ -114,9 +119,9 @@ public class NotificationsScreen: ScreenObject { public func likeComment() -> Self { - let isCommentTextDisplayed = app.webViews.staticTexts.firstMatch.waitForExistence(timeout: 5) + let isCommentOnTextDisplayed = app.staticTexts["Comment on"].firstMatch.waitForExistence(timeout: 5) - if isCommentTextDisplayed { + if isCommentOnTextDisplayed { likeCommentButton.tap() } diff --git a/WordPress/UITestsFoundation/Screens/PostsScreen.swift b/WordPress/UITestsFoundation/Screens/PostsScreen.swift index 7be5a09d6949..6fc61a52f002 100644 --- a/WordPress/UITestsFoundation/Screens/PostsScreen.swift +++ b/WordPress/UITestsFoundation/Screens/PostsScreen.swift @@ -94,7 +94,9 @@ public class PostsScreen: ScreenObject { } public func verifyPostExists(withTitle title: String) { - let expectedPost = app.cells.containing(.staticText, identifier: title).element + let postsTable = app.tables["PostsTable"] + let predicate = NSPredicate(format: "label BEGINSWITH %@", title) + let expectedPost = postsTable.cells.element(matching: predicate) XCTAssertTrue(expectedPost.exists) } diff --git a/WordPress/UITestsFoundation/Screens/StatsScreen.swift b/WordPress/UITestsFoundation/Screens/StatsScreen.swift index 5bd93c4b4a68..e9c1b64bccea 100644 --- a/WordPress/UITestsFoundation/Screens/StatsScreen.swift +++ b/WordPress/UITestsFoundation/Screens/StatsScreen.swift @@ -23,7 +23,7 @@ public class StatsScreen: ScreenObject { public func verifyStatsLoaded(_ stats: [String]) -> Bool { for stat in stats { - guard app.staticTexts[stat].waitForExistence(timeout: 3) else { + guard app.staticTexts[stat].waitForExistence(timeout: 10) else { Logger.log(message: "Element not found: \(stat)", event: LogEvent.e) return false } @@ -33,7 +33,7 @@ public class StatsScreen: ScreenObject { public func verifyChartLoaded(_ chartElements: [String]) -> Bool { for chartElement in chartElements { - guard app.otherElements[chartElement].waitForExistence(timeout: 3) else { + guard app.otherElements[chartElement].waitForExistence(timeout: 10) else { Logger.log(message: "Element not found: \(chartElement)", event: LogEvent.e) return false } diff --git a/WordPress/UITestsFoundation/TestObserver.swift b/WordPress/UITestsFoundation/TestObserver.swift index 4cc036b0e496..a9c1695b14ae 100644 --- a/WordPress/UITestsFoundation/TestObserver.swift +++ b/WordPress/UITestsFoundation/TestObserver.swift @@ -2,8 +2,8 @@ import XCTest class TestObserver: NSObject, XCTestObservation { override init() { - super.init() - XCTestObservationCenter.shared.addTestObserver(self) + super.init() + XCTestObservationCenter.shared.addTestObserver(self) } func testBundleWillStart(_ testBundle: Bundle) { diff --git a/WordPress/UITestsFoundation/XCTestCase+Utils.swift b/WordPress/UITestsFoundation/XCTestCase+Utils.swift index f4ca321069d4..7bc341b42648 100644 --- a/WordPress/UITestsFoundation/XCTestCase+Utils.swift +++ b/WordPress/UITestsFoundation/XCTestCase+Utils.swift @@ -27,7 +27,7 @@ public extension XCTestCase { appToRemove.firstMatch.press(forDuration: 1) waitAndTap(Apps.springboard.buttons["Remove App"]) - waitAndTap(Apps.springboard.alerts.buttons["Delete App"]) + waitForExistenceAndTap(Apps.springboard.alerts.buttons["Delete App"]) waitAndTap(Apps.springboard.alerts.buttons["Delete"]) } diff --git a/WordPress/UITestsFoundation/XCUIApplication+SavePassword.swift b/WordPress/UITestsFoundation/XCUIApplication+SavePassword.swift index dac04c35873d..c8d4b32ebcfe 100644 --- a/WordPress/UITestsFoundation/XCUIApplication+SavePassword.swift +++ b/WordPress/UITestsFoundation/XCUIApplication+SavePassword.swift @@ -6,11 +6,15 @@ extension XCUIApplication { // This method encapsulates the logic to dimiss the prompt. func dismissSavePasswordPrompt() { XCTContext.runActivity(named: "Dismiss save password prompt if needed.") { _ in - guard buttons["Save Password"].waitForExistence(timeout: 10) else { return } + guard buttons["Save Password"].waitForExistence(timeout: 20) else { return } // There should be no need to wait for this button to exist since it's part of the same - // alert where "Save Password" is. - buttons["Not Now"].tap() + // alert where "Save Password" is... + let notNowButton = XCUIApplication().buttons["Not Now"] + // ...but we've seen failures in CI where this cannot be found so let's check first + XCTAssertTrue(notNowButton.waitForExistence(timeout: 5)) + + notNowButton.tapUntil(.dismissed, failureMessage: "Save Password Prompt not dismissed!") } } } diff --git a/WordPress/UITestsFoundation/XCUIElement+TapUntil.swift b/WordPress/UITestsFoundation/XCUIElement+TapUntil.swift new file mode 100644 index 000000000000..010e12e1fa5b --- /dev/null +++ b/WordPress/UITestsFoundation/XCUIElement+TapUntil.swift @@ -0,0 +1,93 @@ +import XCTest + +public extension XCUIElement { + + /// Abstraction do describe possible "states" an `XCUIElement` can be in. + /// + /// The goal of this `enum` is to make checking against the possible states a safe operation thanks to the compiler enforcing all and only the states represented by the `enum` `case`s are handled. + enum State { + case exists + case dismissed + case selected + } + + /// Attempt to tap `self` until the given `XCUIElement` is in the given `State` or the `maxRetries` number of retries has been reached. + /// + /// Useful to make tests robusts against UI changes that may have some lag. + func tapUntil( + element: XCUIElement, + matches state: State, + failureMessage: String, + maxRetries: Int = 10, + retryInterval: TimeInterval = 1 + ) { + tapUntil( + Condition(element: element, state: state), + retriedCount: 0, + failureMessage: failureMessage, + maxRetries: maxRetries, + retryInterval: retryInterval + ) + } + + /// Attempt to tap `self` until its "state" matches `Condition.State` or the `maxRetries` number of retries has been reached. + /// + /// Useful to make tests robusts against UI changes that may have some lag. + func tapUntil( + _ state: State, + failureMessage: String, + maxRetries: Int = 10, + retryInterval: TimeInterval = 1 + ) { + tapUntil( + Condition(element: self, state: state), + retriedCount: 0, + failureMessage: failureMessage, + maxRetries: maxRetries, + retryInterval: retryInterval + ) + } + + /// Describe the expectation for a given `XCUIElement` to be in a certain `Condition.State`. + /// + /// Example: `Condition(element: myButton, state: .selected)`. + struct Condition { + + let element: XCUIElement + let state: XCUIElement.State + + fileprivate func isMet() -> Bool { + switch state { + case .exists: return element.exists + case .dismissed: return element.isHittable == false + case .selected: return element.isSelected + } + } + } + + private func tapUntil( + _ condition: Condition, + retriedCount: Int, + failureMessage: String, + maxRetries: Int, + retryInterval: TimeInterval + ) { + guard retriedCount < maxRetries else { + return XCTFail("\(failureMessage) after \(retriedCount) tries.") + } + + tap() + + guard condition.isMet() else { + sleep(UInt32(retryInterval)) + + return tapUntil( + condition, + retriedCount: retriedCount + 1, + failureMessage: failureMessage, + maxRetries: maxRetries, + retryInterval: retryInterval + ) + } + } +} diff --git a/WordPress/UITestsFoundation/XCUIElement+Utils.swift b/WordPress/UITestsFoundation/XCUIElement+Utils.swift index 6cf8f0ae2829..985b2bb48c10 100644 --- a/WordPress/UITestsFoundation/XCUIElement+Utils.swift +++ b/WordPress/UITestsFoundation/XCUIElement+Utils.swift @@ -20,7 +20,14 @@ public extension XCUIElement { let deviceScreenFrame = app.windows.element(boundBy: 0).frame let deviceScreenWidth = deviceScreenFrame.size.width - let visibleAreaTop = topElement.frame.origin.y + topElement.frame.size.height + let visibleAreaTop: CGFloat + + if topElement.exists { + visibleAreaTop = topElement.frame.origin.y + topElement.frame.size.height + } else { + visibleAreaTop = deviceScreenFrame.origin.y + } + let visibleAreaHeight = bottomElement.frame.origin.y - visibleAreaTop let visibleAreaFrame = CGRect(x: 0, y: visibleAreaTop, width: deviceScreenWidth, height: visibleAreaHeight) diff --git a/WordPress/WordPress-Alpha.entitlements b/WordPress/WordPress-Alpha.entitlements index 0e3350cf3abe..c95dbafba6ed 100644 --- a/WordPress/WordPress-Alpha.entitlements +++ b/WordPress/WordPress-Alpha.entitlements @@ -5,10 +5,11 @@ com.apple.developer.associated-domains webcredentials:wordpress.com + webcredentials:*.wordpress.com applinks:wordpress.com + applinks:*.wordpress.com applinks:apps.wordpress.com applinks:links.wp.a8cmail.com - applinks:*.wordpress.com com.apple.security.application-groups diff --git a/WordPress/WordPress-Internal.entitlements b/WordPress/WordPress-Internal.entitlements index e40c5eddc1a6..0c0d7121ca56 100644 --- a/WordPress/WordPress-Internal.entitlements +++ b/WordPress/WordPress-Internal.entitlements @@ -7,10 +7,11 @@ com.apple.developer.associated-domains webcredentials:wordpress.com + webcredentials:*.wordpress.com applinks:wordpress.com + applinks:*.wordpress.com applinks:apps.wordpress.com applinks:links.wp.a8cmail.com - applinks:*.wordpress.com com.apple.security.application-groups diff --git a/WordPress/WordPress.entitlements b/WordPress/WordPress.entitlements index 88743e39627f..2fd4f24ae0b5 100644 --- a/WordPress/WordPress.entitlements +++ b/WordPress/WordPress.entitlements @@ -11,11 +11,12 @@ com.apple.developer.associated-domains webcredentials:wordpress.com + webcredentials:*.wordpress.com applinks:wordpress.com + applinks:*.wordpress.com applinks:apps.wordpress.com applinks:public-api.wordpress.com applinks:links.wp.a8cmail.com - applinks:*.wordpress.com com.apple.developer.icloud-container-identifiers diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index 940fd0ff0774..042088b33b09 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -72,7 +72,6 @@ 0107E0C028F97D5000DE87DB /* HomeWidgetToday.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F526C522538CF2A0069706C /* HomeWidgetToday.swift */; }; 0107E0C128F97D5000DE87DB /* FlexibleCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F568A2E254216550048A9E4 /* FlexibleCard.swift */; }; 0107E0C228F97D5000DE87DB /* VerticalCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F568A1E254213B60048A9E4 /* VerticalCard.swift */; }; - 0107E0C328F97D5000DE87DB /* ThisWeekWidgetStats.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98F93181239AF64800E4E96E /* ThisWeekWidgetStats.swift */; }; 0107E0C428F97D5000DE87DB /* HomeWidgetAllTimeData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F5C861925C9EA2500BABE64 /* HomeWidgetAllTimeData.swift */; }; 0107E0C528F97D5000DE87DB /* GroupedViewData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FE20C1425CF165700A15525 /* GroupedViewData.swift */; }; 0107E0C628F97D5000DE87DB /* FeatureFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D690151F828FF000200E30 /* FeatureFlag.swift */; }; @@ -87,7 +86,6 @@ 0107E0D028F97D5000DE87DB /* HomeWidgetThisWeek.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F8B138E25D09AA5004FAC0A /* HomeWidgetThisWeek.swift */; }; 0107E0D128F97D5000DE87DB /* SingleStatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F5689EF254209790048A9E4 /* SingleStatView.swift */; }; 0107E0D228F97D5000DE87DB /* UnconfiguredView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FAA18CB25797B85002B1911 /* UnconfiguredView.swift */; }; - 0107E0D328F97D5000DE87DB /* Tracks+StatsWidgets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98390AC2254C984700868F0A /* Tracks+StatsWidgets.swift */; }; 0107E0D428F97D5000DE87DB /* HomeWidgetData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F6DA04025646F96002AB88F /* HomeWidgetData.swift */; }; 0107E0D528F97D5000DE87DB /* HomeWidgetTodayData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FB34ACA25672A90001A74A6 /* HomeWidgetTodayData.swift */; }; 0107E0D628F97D5000DE87DB /* AllTimeWidgetStats.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98BFF57D23984344008A1DCB /* AllTimeWidgetStats.swift */; }; @@ -108,7 +106,6 @@ 0107E11528FD7FE500DE87DB /* AppConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA25FA332609AAAA0005E08F /* AppConfiguration.swift */; }; 0107E11628FD7FE800DE87DB /* AppConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA25FA332609AAAA0005E08F /* AppConfiguration.swift */; }; 0107E13B28FE9DB200DE87DB /* Sites.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 3F46AB0225BF5D6300CE2E98 /* Sites.intentdefinition */; settings = {ATTRIBUTES = (codegen, ); }; }; - 0107E13C28FE9DB200DE87DB /* ThisWeekWidgetStats.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98F93181239AF64800E4E96E /* ThisWeekWidgetStats.swift */; }; 0107E13D28FE9DB200DE87DB /* HomeWidgetAllTimeData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F5C861925C9EA2500BABE64 /* HomeWidgetAllTimeData.swift */; }; 0107E13E28FE9DB200DE87DB /* SitesDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1482CDF2575BDA4007E4DD6 /* SitesDataProvider.swift */; }; 0107E13F28FE9DB200DE87DB /* HomeWidgetTodayData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FB34ACA25672A90001A74A6 /* HomeWidgetTodayData.swift */; }; @@ -152,7 +149,7 @@ 011F52DB2A1CA53300B04114 /* CheckoutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 011F52D92A1CA53300B04114 /* CheckoutViewController.swift */; }; 012041032AAAFE3A00E7C707 /* WidgetCenter+JetpackWidgets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012041022AAAFE3900E7C707 /* WidgetCenter+JetpackWidgets.swift */; }; 012041042AAAFE3A00E7C707 /* WidgetCenter+JetpackWidgets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012041022AAAFE3900E7C707 /* WidgetCenter+JetpackWidgets.swift */; }; - 01281E9A2A0456CB00464F8F /* DomainsSuggestionsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01281E992A0456CB00464F8F /* DomainsSuggestionsScreen.swift */; }; + 01281E9A2A0456CB00464F8F /* DomainsSelectionScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01281E992A0456CB00464F8F /* DomainsSelectionScreen.swift */; }; 01281E9C2A051EEA00464F8F /* MenuNavigationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01281E9B2A051EEA00464F8F /* MenuNavigationTests.swift */; }; 01281E9D2A051EEA00464F8F /* MenuNavigationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01281E9B2A051EEA00464F8F /* MenuNavigationTests.swift */; }; 0133A7BE2A8CEADD00B36E58 /* SupportCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0133A7BD2A8CEADD00B36E58 /* SupportCoordinator.swift */; }; @@ -173,7 +170,13 @@ 014ACD152A1E5034008A706C /* WebKitViewController+SandboxStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 014ACD132A1E5033008A706C /* WebKitViewController+SandboxStore.swift */; }; 014D7E8F2AA9FBDE00F8C9E3 /* WidgetConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0107E15C28FFE99300DE87DB /* WidgetConfiguration.swift */; }; 015BA4EB29A788A300920F4B /* StatsTotalInsightsCellTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 015BA4EA29A788A300920F4B /* StatsTotalInsightsCellTests.swift */; }; + 016231502B3B3CAD0010E377 /* PrimaryDomainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0162314F2B3B3CAD0010E377 /* PrimaryDomainView.swift */; }; + 016231512B3B3CAD0010E377 /* PrimaryDomainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0162314F2B3B3CAD0010E377 /* PrimaryDomainView.swift */; }; 0167F4B62AAA0342005B9E42 /* WidgetConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0107E15C28FFE99300DE87DB /* WidgetConfiguration.swift */; }; + 017008452B35C25C00C80490 /* SiteDomainsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 017008442B35C25C00C80490 /* SiteDomainsViewModel.swift */; }; + 017008462B35C25C00C80490 /* SiteDomainsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 017008442B35C25C00C80490 /* SiteDomainsViewModel.swift */; }; + 017C57BB2B2B5555001E7687 /* DomainSelectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 017C57BA2B2B5555001E7687 /* DomainSelectionViewController.swift */; }; + 017C57BC2B2B5555001E7687 /* DomainSelectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 017C57BA2B2B5555001E7687 /* DomainSelectionViewController.swift */; }; 018635842A8109DE00915532 /* SupportChatBotViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 018635832A8109DE00915532 /* SupportChatBotViewController.swift */; }; 018635852A8109DE00915532 /* SupportChatBotViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 018635832A8109DE00915532 /* SupportChatBotViewController.swift */; }; 018635872A8109F900915532 /* SupportChatBotViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 018635862A8109F900915532 /* SupportChatBotViewModel.swift */; }; @@ -190,11 +193,23 @@ 0188FE4C2AA62F800093EDA5 /* LockScreenTodayLikesCommentsStatWidgetConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0188FE4A2AA62F800093EDA5 /* LockScreenTodayLikesCommentsStatWidgetConfig.swift */; }; 0189AF052ACAD89700F63393 /* ShoppingCartService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0189AF042ACAD89700F63393 /* ShoppingCartService.swift */; }; 0189AF062ACAD89700F63393 /* ShoppingCartService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0189AF042ACAD89700F63393 /* ShoppingCartService.swift */; }; + 018FF1352AE6771A00F301C3 /* LockScreenVerticalCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 018FF1342AE6771A00F301C3 /* LockScreenVerticalCard.swift */; }; + 018FF1372AE67C2600F301C3 /* LockScreenFlexibleCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 018FF1362AE67C2600F301C3 /* LockScreenFlexibleCard.swift */; }; 019D699E2A5EA963003B676D /* RootViewCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 019D699D2A5EA963003B676D /* RootViewCoordinatorTests.swift */; }; 019D69A02A5EBF47003B676D /* WordPressAuthenticatorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 019D699F2A5EBF47003B676D /* WordPressAuthenticatorProtocol.swift */; }; 019D69A12A5EBF47003B676D /* WordPressAuthenticatorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 019D699F2A5EBF47003B676D /* WordPressAuthenticatorProtocol.swift */; }; 01A8508B2A8A126400BD8A97 /* support_chat_widget.css in Resources */ = {isa = PBXBuildFile; fileRef = 01A8508A2A8A126400BD8A97 /* support_chat_widget.css */; }; 01A8508C2A8A126400BD8A97 /* support_chat_widget.css in Resources */ = {isa = PBXBuildFile; fileRef = 01A8508A2A8A126400BD8A97 /* support_chat_widget.css */; }; + 01ABF1702AD578B3004331BD /* WidgetAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01ABF16F2AD578B3004331BD /* WidgetAnalytics.swift */; }; + 01ABF1712AD578B3004331BD /* WidgetAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01ABF16F2AD578B3004331BD /* WidgetAnalytics.swift */; }; + 01B5C3C72AE7FC61007055BB /* UITestConfigurator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01B5C3C62AE7FC61007055BB /* UITestConfigurator.swift */; }; + 01B5C3C82AE7FC61007055BB /* UITestConfigurator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01B5C3C62AE7FC61007055BB /* UITestConfigurator.swift */; }; + 01B759062B3ECA7300179AE6 /* DomainsStateViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F46546302AF2F8D20017E3D1 /* DomainsStateViewModel.swift */; }; + 01B759082B3ECAF300179AE6 /* DomainsStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01B759072B3ECAF300179AE6 /* DomainsStateView.swift */; }; + 01B759092B3ECAF300179AE6 /* DomainsStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01B759072B3ECAF300179AE6 /* DomainsStateView.swift */; }; + 01B7590B2B3ED63B00179AE6 /* DomainDetailsWebViewControllerWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01B7590A2B3ED63B00179AE6 /* DomainDetailsWebViewControllerWrapper.swift */; }; + 01B7590C2B3ED63B00179AE6 /* DomainDetailsWebViewControllerWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01B7590A2B3ED63B00179AE6 /* DomainDetailsWebViewControllerWrapper.swift */; }; + 01B7590E2B3EEEA400179AE6 /* SiteDomainsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01B7590D2B3EEEA400179AE6 /* SiteDomainsViewModelTests.swift */; }; 01CE5007290A889F00A9C2E0 /* TracksConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01CE5006290A889F00A9C2E0 /* TracksConfiguration.swift */; }; 01CE5008290A88BD00A9C2E0 /* TracksConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01CE5006290A889F00A9C2E0 /* TracksConfiguration.swift */; }; 01CE500E290A88C100A9C2E0 /* TracksConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01CE5006290A889F00A9C2E0 /* TracksConfiguration.swift */; }; @@ -214,6 +229,7 @@ 01E258032ACC36FA00F09666 /* PlanStep.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01E258012ACC36FA00F09666 /* PlanStep.swift */; }; 01E258052ACC373800F09666 /* PlanWizardContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01E258042ACC373800F09666 /* PlanWizardContent.swift */; }; 01E258062ACC373800F09666 /* PlanWizardContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01E258042ACC373800F09666 /* PlanWizardContent.swift */; }; + 01E258092ACC3AA000F09666 /* iOS17WidgetAPIs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01E258082ACC3AA000F09666 /* iOS17WidgetAPIs.swift */; }; 01E2580B2ACDC72C00F09666 /* PlanWizardContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01E2580A2ACDC72C00F09666 /* PlanWizardContentViewModel.swift */; }; 01E2580C2ACDC72C00F09666 /* PlanWizardContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01E2580A2ACDC72C00F09666 /* PlanWizardContentViewModel.swift */; }; 01E2580E2ACDC88100F09666 /* PlanWizardContentViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01E2580D2ACDC88100F09666 /* PlanWizardContentViewModelTests.swift */; }; @@ -223,7 +239,6 @@ 02761EC4227010BC009BAF0F /* BlogDetailsSectionIndexTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02761EC3227010BC009BAF0F /* BlogDetailsSectionIndexTests.swift */; }; 027AC51D227896540033E56E /* DomainCreditEligibilityChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027AC51C227896540033E56E /* DomainCreditEligibilityChecker.swift */; }; 027AC5212278983F0033E56E /* DomainCreditEligibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027AC5202278983F0033E56E /* DomainCreditEligibilityTests.swift */; }; - 02AC3092226FFFAA0018D23B /* BlogDetailsViewController+DomainCredit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02AC3091226FFFAA0018D23B /* BlogDetailsViewController+DomainCredit.swift */; }; 02BE5CC02281B53F00E351BA /* RegisterDomainDetailsViewModelLoadingStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02BE5CBF2281B53F00E351BA /* RegisterDomainDetailsViewModelLoadingStateTests.swift */; }; 02BF30532271D7F000616558 /* DomainCreditRedemptionSuccessViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02BF30522271D7F000616558 /* DomainCreditRedemptionSuccessViewController.swift */; }; 02D75D9922793EA2003FF09A /* BlogDetailsSectionFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02D75D9822793EA2003FF09A /* BlogDetailsSectionFooterView.swift */; }; @@ -233,7 +248,6 @@ 03216ECD27995F3500D444CA /* SchedulingViewControllerPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03216ECB27995F3500D444CA /* SchedulingViewControllerPresenter.swift */; }; 069A4AA62664448F00413FA9 /* GutenbergFeaturedImageHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 069A4AA52664448F00413FA9 /* GutenbergFeaturedImageHelper.swift */; }; 069A4AA72664448F00413FA9 /* GutenbergFeaturedImageHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 069A4AA52664448F00413FA9 /* GutenbergFeaturedImageHelper.swift */; }; - 0807CB721CE670A800CDBDAC /* WPContentSearchHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0807CB711CE670A800CDBDAC /* WPContentSearchHelper.swift */; }; 080C44A91CE14A9F00B3A02F /* MenuDetailsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 080C449E1CE14A9F00B3A02F /* MenuDetailsViewController.m */; }; 0815CF461E96F22600069916 /* MediaImportService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0815CF451E96F22600069916 /* MediaImportService.swift */; }; 081E4B4C281C019A0085E89C /* TooltipAnchor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081E4B4B281C019A0085E89C /* TooltipAnchor.swift */; }; @@ -253,14 +267,15 @@ 08216FD31CDBF96000304BA7 /* MenuItemTagsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 08216FC31CDBF96000304BA7 /* MenuItemTagsViewController.m */; }; 08216FD41CDBF96000304BA7 /* MenuItemTypeSelectionView.m in Sources */ = {isa = PBXBuildFile; fileRef = 08216FC51CDBF96000304BA7 /* MenuItemTypeSelectionView.m */; }; 08216FD51CDBF96000304BA7 /* MenuItemTypeViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 08216FC71CDBF96000304BA7 /* MenuItemTypeViewController.m */; }; - 08240C2E2AB8A2DD00E7AEA8 /* DomainListCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08240C2D2AB8A2DD00E7AEA8 /* DomainListCard.swift */; }; - 08240C2F2AB8A2DD00E7AEA8 /* DomainListCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08240C2D2AB8A2DD00E7AEA8 /* DomainListCard.swift */; }; + 08240C2F2AB8A2DD00E7AEA8 /* AllDomainsListCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08240C2D2AB8A2DD00E7AEA8 /* AllDomainsListCardView.swift */; }; 082635BB1CEA69280088030C /* MenuItemsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 082635BA1CEA69280088030C /* MenuItemsViewController.m */; }; 0828D7FA1E6E09AE00C7C7D4 /* WPAppAnalytics+Media.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0828D7F91E6E09AE00C7C7D4 /* WPAppAnalytics+Media.swift */; }; 082A645B291C2DD700668D2C /* Routes+Jetpack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 082A645A291C2DD700668D2C /* Routes+Jetpack.swift */; }; 082A645C291C2DD700668D2C /* Routes+Jetpack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 082A645A291C2DD700668D2C /* Routes+Jetpack.swift */; }; 082AB9D91C4EEEF4000CA523 /* PostTagService.m in Sources */ = {isa = PBXBuildFile; fileRef = 082AB9D81C4EEEF4000CA523 /* PostTagService.m */; }; 082AB9DD1C4F035E000CA523 /* PostTag.m in Sources */ = {isa = PBXBuildFile; fileRef = 082AB9DC1C4F035E000CA523 /* PostTag.m */; }; + 0830538C2B2732E400B889FE /* DynamicDashboardCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0830538B2B2732E400B889FE /* DynamicDashboardCard.swift */; }; + 0830538D2B2732E400B889FE /* DynamicDashboardCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0830538B2B2732E400B889FE /* DynamicDashboardCard.swift */; }; 0839F88B2993C0C000415038 /* JetpackDefaultOverlayCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0857BB3F299275760011CBD1 /* JetpackDefaultOverlayCoordinator.swift */; }; 0839F88C2993C1B500415038 /* JetpackPluginOverlayCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 084FC3BA29914C7F00A17BCF /* JetpackPluginOverlayCoordinator.swift */; }; 0839F88D2993C1B600415038 /* JetpackPluginOverlayCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 084FC3BA29914C7F00A17BCF /* JetpackPluginOverlayCoordinator.swift */; }; @@ -293,29 +308,19 @@ 086F2484284F52E100032F39 /* FeatureHighlightStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 084A07052848E1820054508A /* FeatureHighlightStore.swift */; }; 0878580328B4CF950069F96C /* UserPersistentRepositoryUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0878580228B4CF950069F96C /* UserPersistentRepositoryUtility.swift */; }; 0878580428B4CF950069F96C /* UserPersistentRepositoryUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0878580228B4CF950069F96C /* UserPersistentRepositoryUtility.swift */; }; - 08799C252A334645005317F7 /* Spacing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08799C242A334645005317F7 /* Spacing.swift */; }; - 08799C262A334645005317F7 /* Spacing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08799C242A334645005317F7 /* Spacing.swift */; }; 0879FC161E9301DD00E1EFC8 /* MediaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0879FC151E9301DD00E1EFC8 /* MediaTests.swift */; }; - 087EBFA81F02313E001F7ACE /* MediaThumbnailService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 087EBFA71F02313E001F7ACE /* MediaThumbnailService.swift */; }; - 0880BADC29ED6FF3002D3AB0 /* UIColor+DesignSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0880BADB29ED6FF3002D3AB0 /* UIColor+DesignSystem.swift */; }; - 0880BADD29ED6FF3002D3AB0 /* UIColor+DesignSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0880BADB29ED6FF3002D3AB0 /* UIColor+DesignSystem.swift */; }; 088134FF2A56C5240027C086 /* CompliancePopoverViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 088134FE2A56C5240027C086 /* CompliancePopoverViewModelTests.swift */; }; 0885A3671E837AFE00619B4D /* URLIncrementalFilenameTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0885A3661E837AFE00619B4D /* URLIncrementalFilenameTests.swift */; }; 088B89891DA6F93B000E8DEF /* ReaderPostCardContentLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 088B89881DA6F93B000E8DEF /* ReaderPostCardContentLabel.swift */; }; 088CC594282BEC41007B9421 /* TooltipPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 088CC593282BEC41007B9421 /* TooltipPresenter.swift */; }; - 088D58A529E724F300E6C0F4 /* ColorGallery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 088D58A429E724F300E6C0F4 /* ColorGallery.swift */; }; - 088D58A629E724F300E6C0F4 /* ColorGallery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 088D58A429E724F300E6C0F4 /* ColorGallery.swift */; }; 08A250F828D9E87600F50420 /* CommentDetailInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08A250F728D9E87600F50420 /* CommentDetailInfoViewController.swift */; }; 08A250F928D9E87600F50420 /* CommentDetailInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08A250F728D9E87600F50420 /* CommentDetailInfoViewController.swift */; }; 08A250FC28D9F0E200F50420 /* CommentDetailInfoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08A250FB28D9F0E200F50420 /* CommentDetailInfoViewModel.swift */; }; 08A250FD28D9F0E200F50420 /* CommentDetailInfoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08A250FB28D9F0E200F50420 /* CommentDetailInfoViewModel.swift */; }; 08A2AD791CCED2A800E84454 /* PostTagServiceTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 08A2AD781CCED2A800E84454 /* PostTagServiceTests.m */; }; 08A2AD7B1CCED8E500E84454 /* PostCategoryServiceTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 08A2AD7A1CCED8E500E84454 /* PostCategoryServiceTests.m */; }; - 08A4E129289D202F001D9EC7 /* UserPersistentStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08A4E128289D202F001D9EC7 /* UserPersistentStore.swift */; }; - 08A4E12A289D202F001D9EC7 /* UserPersistentStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08A4E128289D202F001D9EC7 /* UserPersistentStore.swift */; }; 08A4E12C289D2337001D9EC7 /* UserPersistentRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08A4E12B289D2337001D9EC7 /* UserPersistentRepository.swift */; }; 08A4E12D289D2337001D9EC7 /* UserPersistentRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08A4E12B289D2337001D9EC7 /* UserPersistentRepository.swift */; }; - 08A4E12F289D2795001D9EC7 /* UserPersistentStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08A4E12E289D2795001D9EC7 /* UserPersistentStoreTests.swift */; }; 08A7343F298AB68000F925C7 /* JetpackPluginOverlayViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08A7343E298AB68000F925C7 /* JetpackPluginOverlayViewModel.swift */; }; 08A73440298AB68000F925C7 /* JetpackPluginOverlayViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08A7343E298AB68000F925C7 /* JetpackPluginOverlayViewModel.swift */; }; 08AA64052A84FFF40076E38D /* DashboardGoogleDomainsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08AA64042A84FFF40076E38D /* DashboardGoogleDomainsViewModel.swift */; }; @@ -335,8 +340,7 @@ 08BA4BCA298A9AD500015BD2 /* JetpackInstallPluginLogoAnimation_ltr.json in Resources */ = {isa = PBXBuildFile; fileRef = 08BA4BC6298A9AD400015BD2 /* JetpackInstallPluginLogoAnimation_ltr.json */; }; 08BBA3502A792B4B00BDCF32 /* DashboardGoogleDomainsCardCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08BBA34F2A792B4B00BDCF32 /* DashboardGoogleDomainsCardCell.swift */; }; 08BBA3512A792B4B00BDCF32 /* DashboardGoogleDomainsCardCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08BBA34F2A792B4B00BDCF32 /* DashboardGoogleDomainsCardCell.swift */; }; - 08C388661ED7705E0057BE49 /* MediaAssetExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08C388651ED7705E0057BE49 /* MediaAssetExporter.swift */; }; - 08C3886A1ED78EE70057BE49 /* Media+WPMediaAsset.m in Sources */ = {isa = PBXBuildFile; fileRef = 08C388691ED78EE70057BE49 /* Media+WPMediaAsset.m */; }; + 08C3886A1ED78EE70057BE49 /* Media+Extensions.m in Sources */ = {isa = PBXBuildFile; fileRef = 08C388691ED78EE70057BE49 /* Media+Extensions.m */; }; 08C42C31281807880034720B /* ReaderSubscribeCommentsActionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08C42C30281807880034720B /* ReaderSubscribeCommentsActionTests.swift */; }; 08CBC77929AE6CC4000026E7 /* SiteCreationEmptySiteTemplate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08CBC77829AE6CC4000026E7 /* SiteCreationEmptySiteTemplate.swift */; }; 08CBC77A29AE6CC4000026E7 /* SiteCreationEmptySiteTemplate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08CBC77829AE6CC4000026E7 /* SiteCreationEmptySiteTemplate.swift */; }; @@ -358,16 +362,14 @@ 08DF9C441E8475530058678C /* test-image-portrait.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 08DF9C431E8475530058678C /* test-image-portrait.jpg */; }; 08E39B4528A3DEB200874CB8 /* UserPersistentStoreFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08E39B4428A3DEB200874CB8 /* UserPersistentStoreFactory.swift */; }; 08E39B4628A3DEB200874CB8 /* UserPersistentStoreFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08E39B4428A3DEB200874CB8 /* UserPersistentStoreFactory.swift */; }; + 08E63FCD2B28E52B00747E21 /* DesignSystem in Frameworks */ = {isa = PBXBuildFile; productRef = 08E63FCC2B28E52B00747E21 /* DesignSystem */; }; + 08E63FCF2B28E53400747E21 /* DesignSystem in Frameworks */ = {isa = PBXBuildFile; productRef = 08E63FCE2B28E53400747E21 /* DesignSystem */; }; 08E6E07B2A4C3E3A00B807B0 /* CompliancePopoverViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08E6E07A2A4C3E3A00B807B0 /* CompliancePopoverViewController.swift */; }; 08E6E07C2A4C3E3A00B807B0 /* CompliancePopoverViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08E6E07A2A4C3E3A00B807B0 /* CompliancePopoverViewController.swift */; }; 08E6E07E2A4C405500B807B0 /* CompliancePopoverViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08E6E07D2A4C405500B807B0 /* CompliancePopoverViewModel.swift */; }; 08E6E07F2A4C405500B807B0 /* CompliancePopoverViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08E6E07D2A4C405500B807B0 /* CompliancePopoverViewModel.swift */; }; 08E77F451EE87FCF006F9515 /* MediaThumbnailExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08E77F441EE87FCF006F9515 /* MediaThumbnailExporter.swift */; }; 08E77F471EE9D72F006F9515 /* MediaThumbnailExporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08E77F461EE9D72F006F9515 /* MediaThumbnailExporterTests.swift */; }; - 08EA036729C9B51200B72A87 /* Color+DesignSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08EA036629C9B51200B72A87 /* Color+DesignSystem.swift */; }; - 08EA036929C9B53000B72A87 /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 08EA036829C9B53000B72A87 /* Colors.xcassets */; }; - 08EA036A29C9C39A00B72A87 /* Color+DesignSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08EA036629C9B51200B72A87 /* Color+DesignSystem.swift */; }; - 08EA036B29C9C3A000B72A87 /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 08EA036829C9B53000B72A87 /* Colors.xcassets */; }; 08F8CD2A1EBD22EF0049D0C0 /* MediaExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08F8CD291EBD22EF0049D0C0 /* MediaExporter.swift */; }; 08F8CD2D1EBD24600049D0C0 /* MediaExporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08F8CD2C1EBD245F0049D0C0 /* MediaExporterTests.swift */; }; 08F8CD2F1EBD29440049D0C0 /* MediaImageExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08F8CD2E1EBD29440049D0C0 /* MediaImageExporter.swift */; }; @@ -388,16 +390,29 @@ 0A9610F928B2E56300076EBA /* UserSuggestion+Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A9610F828B2E56300076EBA /* UserSuggestion+Comparable.swift */; }; 0A9610FA28B2E56300076EBA /* UserSuggestion+Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A9610F828B2E56300076EBA /* UserSuggestion+Comparable.swift */; }; 0A9687BC28B40771009DCD2F /* FullScreenCommentReplyViewModelMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A9687BB28B40771009DCD2F /* FullScreenCommentReplyViewModelMock.swift */; }; - 0C01A6EA2AB37F0F009F7145 /* SiteMediaCollectionCellBadgeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C01A6E92AB37F0F009F7145 /* SiteMediaCollectionCellBadgeView.swift */; }; - 0C01A6EB2AB37F0F009F7145 /* SiteMediaCollectionCellBadgeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C01A6E92AB37F0F009F7145 /* SiteMediaCollectionCellBadgeView.swift */; }; + 0C01A6EA2AB37F0F009F7145 /* SiteMediaCollectionCellSelectionOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C01A6E92AB37F0F009F7145 /* SiteMediaCollectionCellSelectionOverlayView.swift */; }; + 0C01A6EB2AB37F0F009F7145 /* SiteMediaCollectionCellSelectionOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C01A6E92AB37F0F009F7145 /* SiteMediaCollectionCellSelectionOverlayView.swift */; }; 0C0453282AC73343003079C8 /* SiteMediaVideoDurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0453272AC73343003079C8 /* SiteMediaVideoDurationView.swift */; }; 0C0453292AC73343003079C8 /* SiteMediaVideoDurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0453272AC73343003079C8 /* SiteMediaVideoDurationView.swift */; }; 0C04532B2AC77245003079C8 /* SiteMediaDocumentInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C04532A2AC77245003079C8 /* SiteMediaDocumentInfoView.swift */; }; 0C04532C2AC77245003079C8 /* SiteMediaDocumentInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C04532A2AC77245003079C8 /* SiteMediaDocumentInfoView.swift */; }; + 0C0AD1062B0C483F00EC06E6 /* ExternalMediaSelectionTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0AD1052B0C483F00EC06E6 /* ExternalMediaSelectionTitleView.swift */; }; + 0C0AD1072B0C483F00EC06E6 /* ExternalMediaSelectionTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0AD1052B0C483F00EC06E6 /* ExternalMediaSelectionTitleView.swift */; }; + 0C0AD10A2B0CCFA400EC06E6 /* MediaPreviewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0AD1092B0CCFA400EC06E6 /* MediaPreviewController.swift */; }; + 0C0AD10B2B0CCFA400EC06E6 /* MediaPreviewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0AD1092B0CCFA400EC06E6 /* MediaPreviewController.swift */; }; 0C0AE7592A8FAD6A007D9D6C /* MediaPickerMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0AE7582A8FAD6A007D9D6C /* MediaPickerMenu.swift */; }; 0C0AE75A2A8FAD6A007D9D6C /* MediaPickerMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0AE7582A8FAD6A007D9D6C /* MediaPickerMenu.swift */; }; 0C0D3B0D2A4C79DE0050A00D /* BlazeCampaignsStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D3B0C2A4C79DE0050A00D /* BlazeCampaignsStream.swift */; }; 0C0D3B0E2A4C79DE0050A00D /* BlazeCampaignsStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D3B0C2A4C79DE0050A00D /* BlazeCampaignsStream.swift */; }; + 0C1531FE2AE17140003CDE13 /* PostSearchViewModel+Highlighter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1531FD2AE17140003CDE13 /* PostSearchViewModel+Highlighter.swift */; }; + 0C1531FF2AE17140003CDE13 /* PostSearchViewModel+Highlighter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1531FD2AE17140003CDE13 /* PostSearchViewModel+Highlighter.swift */; }; + 0C1DB5FF2B095DA50028F200 /* ImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1DB5FE2B095DA50028F200 /* ImageView.swift */; }; + 0C1DB6002B095DA50028F200 /* ImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1DB5FE2B095DA50028F200 /* ImageView.swift */; }; + 0C1DB6082B0A419B0028F200 /* ImageDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1DB6072B0A419B0028F200 /* ImageDecoder.swift */; }; + 0C1DB6092B0A419B0028F200 /* ImageDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1DB6072B0A419B0028F200 /* ImageDecoder.swift */; }; + 0C1DB60B2B0A9A570028F200 /* ImageDownloaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1DB60A2B0A9A570028F200 /* ImageDownloaderTests.swift */; }; + 0C1DB60D2B0BDA740028F200 /* TenorWelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1DB60C2B0BDA740028F200 /* TenorWelcomeView.swift */; }; + 0C1DB60E2B0BDA740028F200 /* TenorWelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1DB60C2B0BDA740028F200 /* TenorWelcomeView.swift */; }; 0C23F3362AC4AD3400EE6117 /* SiteMediaSelectionTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C23F3352AC4AD3400EE6117 /* SiteMediaSelectionTitleView.swift */; }; 0C23F3372AC4AD3400EE6117 /* SiteMediaSelectionTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C23F3352AC4AD3400EE6117 /* SiteMediaSelectionTitleView.swift */; }; 0C23F33E2AC4AEF600EE6117 /* SiteMediaPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C23F33D2AC4AEF600EE6117 /* SiteMediaPickerViewController.swift */; }; @@ -407,6 +422,10 @@ 0C2C83FB2A6EABF300A3ACD9 /* StatsPeriodCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2C83F92A6EABF300A3ACD9 /* StatsPeriodCache.swift */; }; 0C2C83FD2A6EBD3F00A3ACD9 /* StatsInsightsCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2C83FC2A6EBD3F00A3ACD9 /* StatsInsightsCache.swift */; }; 0C2C83FE2A6EBD3F00A3ACD9 /* StatsInsightsCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2C83FC2A6EBD3F00A3ACD9 /* StatsInsightsCache.swift */; }; + 0C308FFE2B1234E70071C551 /* SiteMediaFilterButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C308FFD2B1234E70071C551 /* SiteMediaFilterButtonView.swift */; }; + 0C308FFF2B1234E70071C551 /* SiteMediaFilterButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C308FFD2B1234E70071C551 /* SiteMediaFilterButtonView.swift */; }; + 0C3090222B12A5C90071C551 /* UIButton+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3090212B12A5C90071C551 /* UIButton+Extensions.swift */; }; + 0C3090232B12A5C90071C551 /* UIButton+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3090212B12A5C90071C551 /* UIButton+Extensions.swift */; }; 0C35FFF129CB81F700D224EB /* BlogDashboardHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C35FFF029CB81F700D224EB /* BlogDashboardHelpers.swift */; }; 0C35FFF229CB81F700D224EB /* BlogDashboardHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C35FFF029CB81F700D224EB /* BlogDashboardHelpers.swift */; }; 0C35FFF429CBA6DA00D224EB /* BlogDashboardPersonalizationViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C35FFF329CBA6DA00D224EB /* BlogDashboardPersonalizationViewModelTests.swift */; }; @@ -417,17 +436,25 @@ 0C391E612A3002950040EA91 /* BlazeCampaignStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C391E602A3002950040EA91 /* BlazeCampaignStatusView.swift */; }; 0C391E622A3002950040EA91 /* BlazeCampaignStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C391E602A3002950040EA91 /* BlazeCampaignStatusView.swift */; }; 0C391E642A312DB20040EA91 /* BlazeCampaignViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C391E632A312DB20040EA91 /* BlazeCampaignViewModelTests.swift */; }; + 0C5751102B011468001074E5 /* RemoteConfigDebugView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C57510F2B011468001074E5 /* RemoteConfigDebugView.swift */; }; + 0C5751112B011468001074E5 /* RemoteConfigDebugView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C57510F2B011468001074E5 /* RemoteConfigDebugView.swift */; }; 0C63266F2A3D1305000B8C57 /* GutenbergFilesAppMediaSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C63266E2A3D1305000B8C57 /* GutenbergFilesAppMediaSourceTests.swift */; }; 0C6C4CD02A4F0A000049E762 /* BlazeCampaignsStreamTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C6C4CCF2A4F0A000049E762 /* BlazeCampaignsStreamTests.swift */; }; 0C6C4CD42A4F0AD90049E762 /* blaze-search-page-1.json in Resources */ = {isa = PBXBuildFile; fileRef = 0C6C4CD32A4F0AD80049E762 /* blaze-search-page-1.json */; }; 0C6C4CD62A4F0AEE0049E762 /* blaze-search-page-2.json in Resources */ = {isa = PBXBuildFile; fileRef = 0C6C4CD52A4F0AEE0049E762 /* blaze-search-page-2.json */; }; 0C6C4CD82A4F0F2C0049E762 /* Bundle+TestExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C6C4CD72A4F0F2C0049E762 /* Bundle+TestExtensions.swift */; }; + 0C700B862AE1E1300085C2EE /* PageListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C700B852AE1E1300085C2EE /* PageListCell.swift */; }; + 0C700B872AE1E1300085C2EE /* PageListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C700B852AE1E1300085C2EE /* PageListCell.swift */; }; + 0C700B892AE1E1940085C2EE /* PageListItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C700B882AE1E1940085C2EE /* PageListItemViewModel.swift */; }; + 0C700B8A2AE1E1940085C2EE /* PageListItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C700B882AE1E1940085C2EE /* PageListItemViewModel.swift */; }; 0C7073952A65CB2E00F325CE /* MemoryCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C7073942A65CB2E00F325CE /* MemoryCache.swift */; }; 0C7073962A65CB2E00F325CE /* MemoryCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C7073942A65CB2E00F325CE /* MemoryCache.swift */; }; 0C71959B2A3CA582002EA18C /* SiteSettingsRelatedPostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C71959A2A3CA582002EA18C /* SiteSettingsRelatedPostsView.swift */; }; 0C71959C2A3CA582002EA18C /* SiteSettingsRelatedPostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C71959A2A3CA582002EA18C /* SiteSettingsRelatedPostsView.swift */; }; 0C748B4B2A9D71A100809E1A /* SiteMediaCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C748B4A2A9D71A000809E1A /* SiteMediaCollectionViewController.swift */; }; 0C748B4C2A9D71A100809E1A /* SiteMediaCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C748B4A2A9D71A000809E1A /* SiteMediaCollectionViewController.swift */; }; + 0C749D7A2B0543D0004CB468 /* WPImageViewController+Swift.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C749D792B0543D0004CB468 /* WPImageViewController+Swift.swift */; }; + 0C749D7B2B0543D0004CB468 /* WPImageViewController+Swift.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C749D792B0543D0004CB468 /* WPImageViewController+Swift.swift */; }; 0C75E26E2A9F63CB00B784E5 /* MediaImageService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C75E26D2A9F63CB00B784E5 /* MediaImageService.swift */; }; 0C75E26F2A9F63CB00B784E5 /* MediaImageService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C75E26D2A9F63CB00B784E5 /* MediaImageService.swift */; }; 0C7762232AAFD39700E07A88 /* SiteMediaAddMediaMenuController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C7762222AAFD39700E07A88 /* SiteMediaAddMediaMenuController.swift */; }; @@ -446,8 +473,6 @@ 0C896DE42A3A7BDC00D7D4E7 /* SettingsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C896DDF2A3A763400D7D4E7 /* SettingsCell.swift */; }; 0C896DE52A3A7C1F00D7D4E7 /* SiteVisibility+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C896DE12A3A767200D7D4E7 /* SiteVisibility+Extensions.swift */; }; 0C896DE72A3A832B00D7D4E7 /* SiteVisibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C896DE62A3A832B00D7D4E7 /* SiteVisibilityTests.swift */; }; - 0C8B8C0F2ACDBE1900CCE50F /* DisabledVideoOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8B8C0E2ACDBE1900CCE50F /* DisabledVideoOverlay.swift */; }; - 0C8B8C102ACDBE1900CCE50F /* DisabledVideoOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8B8C0E2ACDBE1900CCE50F /* DisabledVideoOverlay.swift */; }; 0C8E2F2D2AC4722F0023F9D6 /* SiteMediaViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8E2F2C2AC4722F0023F9D6 /* SiteMediaViewController.swift */; }; 0C8E2F2E2AC4722F0023F9D6 /* SiteMediaViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8E2F2C2AC4722F0023F9D6 /* SiteMediaViewController.swift */; }; 0C8FC9A12A8BC8630059DCE4 /* PHPickerController+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8FC9A02A8BC8630059DCE4 /* PHPickerController+Extensions.swift */; }; @@ -458,6 +483,13 @@ 0C8FC9A82A8BFAAE0059DCE4 /* NSItemProvider+Exportable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8FC9A62A8BFAAD0059DCE4 /* NSItemProvider+Exportable.swift */; }; 0C8FC9AA2A8C57000059DCE4 /* ItemProviderMediaExporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8FC9A92A8C57000059DCE4 /* ItemProviderMediaExporterTests.swift */; }; 0C8FC9AC2A8C57930059DCE4 /* test-webp.webp in Resources */ = {isa = PBXBuildFile; fileRef = 0C8FC9AB2A8C57930059DCE4 /* test-webp.webp */; }; + 0CA10F6D2ADAE86D00CE75AC /* PostSearchSuggestionsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA10F6C2ADAE86D00CE75AC /* PostSearchSuggestionsService.swift */; }; + 0CA10F6E2ADAE86E00CE75AC /* PostSearchSuggestionsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA10F6C2ADAE86D00CE75AC /* PostSearchSuggestionsService.swift */; }; + 0CA10F732ADB014C00CE75AC /* StringRankedSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA10F722ADB014C00CE75AC /* StringRankedSearch.swift */; }; + 0CA10F742ADB014C00CE75AC /* StringRankedSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA10F722ADB014C00CE75AC /* StringRankedSearch.swift */; }; + 0CA10FA52ADB286300CE75AC /* StringRankedSearchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA10FA42ADB286300CE75AC /* StringRankedSearchTests.swift */; }; + 0CA10FA82ADB7C5200CE75AC /* PostSearchService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA10FA62ADB76ED00CE75AC /* PostSearchService.swift */; }; + 0CA10FA92ADB7C5300CE75AC /* PostSearchService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA10FA62ADB76ED00CE75AC /* PostSearchService.swift */; }; 0CA1C8C12A940EE300F691EE /* AvatarMenuController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA1C8C02A940EE300F691EE /* AvatarMenuController.swift */; }; 0CA1C8C22A940EE300F691EE /* AvatarMenuController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA1C8C02A940EE300F691EE /* AvatarMenuController.swift */; }; 0CAE8EF22A9E9E8D0073EEB9 /* SiteMediaCollectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAE8EF12A9E9E8D0073EEB9 /* SiteMediaCollectionCell.swift */; }; @@ -473,16 +505,49 @@ 0CB4057A29C8DDEE008EED0A /* BlogDashboardPersonalizationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB4057029C8DCF4008EED0A /* BlogDashboardPersonalizationViewModel.swift */; }; 0CB4057D29C8DF83008EED0A /* BlogDashboardPersonalizeCardCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB4057B29C8DEE1008EED0A /* BlogDashboardPersonalizeCardCell.swift */; }; 0CB4057E29C8DF84008EED0A /* BlogDashboardPersonalizeCardCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB4057B29C8DEE1008EED0A /* BlogDashboardPersonalizeCardCell.swift */; }; + 0CB424EE2ADEE3CD0080B807 /* PostSearchTokenTableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB424ED2ADEE3CD0080B807 /* PostSearchTokenTableCell.swift */; }; + 0CB424EF2ADEE3CD0080B807 /* PostSearchTokenTableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB424ED2ADEE3CD0080B807 /* PostSearchTokenTableCell.swift */; }; + 0CB424F12ADEE52A0080B807 /* PostSearchToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB424F02ADEE52A0080B807 /* PostSearchToken.swift */; }; + 0CB424F22ADEE52A0080B807 /* PostSearchToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB424F02ADEE52A0080B807 /* PostSearchToken.swift */; }; + 0CB424F42ADF3CBE0080B807 /* PostSearchViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB424F32ADF3CBE0080B807 /* PostSearchViewModelTests.swift */; }; + 0CB424F62AE0416D0080B807 /* SolidColorActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB424F52AE0416D0080B807 /* SolidColorActivityIndicator.swift */; }; + 0CB424F72AE0416D0080B807 /* SolidColorActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB424F52AE0416D0080B807 /* SolidColorActivityIndicator.swift */; }; + 0CB54F572AEC320700582080 /* WordPressAppDelegate+PostCoordinatorDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB54F562AEC320700582080 /* WordPressAppDelegate+PostCoordinatorDelegate.swift */; }; + 0CB54F582AEC320700582080 /* WordPressAppDelegate+PostCoordinatorDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB54F562AEC320700582080 /* WordPressAppDelegate+PostCoordinatorDelegate.swift */; }; 0CD223DF2AA8ADFD002BD761 /* DashboardQuickActionsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD223DE2AA8ADFD002BD761 /* DashboardQuickActionsViewModel.swift */; }; 0CD223E02AA8ADFD002BD761 /* DashboardQuickActionsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD223DE2AA8ADFD002BD761 /* DashboardQuickActionsViewModel.swift */; }; 0CD382832A4B699E00612173 /* DashboardBlazeCardCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD382822A4B699E00612173 /* DashboardBlazeCardCellViewModel.swift */; }; 0CD382842A4B699E00612173 /* DashboardBlazeCardCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD382822A4B699E00612173 /* DashboardBlazeCardCellViewModel.swift */; }; 0CD382862A4B6FCF00612173 /* DashboardBlazeCardCellViewModelTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD382852A4B6FCE00612173 /* DashboardBlazeCardCellViewModelTest.swift */; }; + 0CD9CC9F2AD73A560044A33C /* PostSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD9CC9E2AD73A560044A33C /* PostSearchViewController.swift */; }; + 0CD9CCA02AD73A560044A33C /* PostSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD9CC9E2AD73A560044A33C /* PostSearchViewController.swift */; }; + 0CD9CCA32AD831590044A33C /* PostSearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD9CCA22AD831590044A33C /* PostSearchViewModel.swift */; }; + 0CD9CCA42AD831590044A33C /* PostSearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD9CCA22AD831590044A33C /* PostSearchViewModel.swift */; }; + 0CD9FB7E2AF9C4DB009D9C7A /* UIBarButtonItem+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD9FB7D2AF9C4DB009D9C7A /* UIBarButtonItem+Extensions.swift */; }; + 0CD9FB7F2AF9C4DB009D9C7A /* UIBarButtonItem+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD9FB7D2AF9C4DB009D9C7A /* UIBarButtonItem+Extensions.swift */; }; + 0CD9FB872AFA71B9009D9C7A /* DGCharts in Frameworks */ = {isa = PBXBuildFile; productRef = 0CD9FB862AFA71B9009D9C7A /* DGCharts */; }; + 0CD9FB892AFA71C2009D9C7A /* DGCharts in Frameworks */ = {isa = PBXBuildFile; productRef = 0CD9FB882AFA71C2009D9C7A /* DGCharts */; }; + 0CD9FB8B2AFADAFE009D9C7A /* SiteMediaPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD9FB8A2AFADAFE009D9C7A /* SiteMediaPageViewController.swift */; }; + 0CD9FB8C2AFADAFE009D9C7A /* SiteMediaPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD9FB8A2AFADAFE009D9C7A /* SiteMediaPageViewController.swift */; }; 0CDEC40C2A2FAF0500BB3A91 /* DashboardBlazeCampaignsCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CDEC40B2A2FAF0500BB3A91 /* DashboardBlazeCampaignsCardView.swift */; }; 0CDEC40D2A2FAF0500BB3A91 /* DashboardBlazeCampaignsCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CDEC40B2A2FAF0500BB3A91 /* DashboardBlazeCampaignsCardView.swift */; }; + 0CE538CA2B0D6E0000834BA2 /* ExternalMediaDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE538C92B0D6E0000834BA2 /* ExternalMediaDataSource.swift */; }; + 0CE538CB2B0D6E0000834BA2 /* ExternalMediaDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE538C92B0D6E0000834BA2 /* ExternalMediaDataSource.swift */; }; + 0CE538D02B0E317000834BA2 /* StockPhotosWelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE538CF2B0E317000834BA2 /* StockPhotosWelcomeView.swift */; }; + 0CE538D12B0E317000834BA2 /* StockPhotosWelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE538CF2B0E317000834BA2 /* StockPhotosWelcomeView.swift */; }; + 0CE7833D2B08F3C300B114EB /* ExternalMediaPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE7833C2B08F3C300B114EB /* ExternalMediaPickerViewController.swift */; }; + 0CE7833E2B08F3C300B114EB /* ExternalMediaPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE7833C2B08F3C300B114EB /* ExternalMediaPickerViewController.swift */; }; + 0CE783412B08FB2E00B114EB /* ExternalMediaPickerCollectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE783402B08FB2E00B114EB /* ExternalMediaPickerCollectionCell.swift */; }; + 0CE783422B08FB2E00B114EB /* ExternalMediaPickerCollectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE783402B08FB2E00B114EB /* ExternalMediaPickerCollectionCell.swift */; }; 0CED95602A460F4B0020F420 /* DebugFeatureFlagsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CED955F2A460F4B0020F420 /* DebugFeatureFlagsView.swift */; }; 0CED95612A460F4B0020F420 /* DebugFeatureFlagsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CED955F2A460F4B0020F420 /* DebugFeatureFlagsView.swift */; }; + 0CF0C4232AE98C13006FFAB4 /* AbstractPostHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF0C4222AE98C13006FFAB4 /* AbstractPostHelper.swift */; }; + 0CF0C4242AE98C13006FFAB4 /* AbstractPostHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF0C4222AE98C13006FFAB4 /* AbstractPostHelper.swift */; }; 0CF7D6C32ABB753A006D1E89 /* MediaImageServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF7D6C22ABB753A006D1E89 /* MediaImageServiceTests.swift */; }; + 0CFE9AC62AF44A9F00B8F659 /* AbstractPostHelper+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CFE9AC52AF44A9F00B8F659 /* AbstractPostHelper+Actions.swift */; }; + 0CFE9AC72AF44A9F00B8F659 /* AbstractPostHelper+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CFE9AC52AF44A9F00B8F659 /* AbstractPostHelper+Actions.swift */; }; + 0CFE9AC92AF52D3B00B8F659 /* PostSettingsViewController+Swift.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CFE9AC82AF52D3B00B8F659 /* PostSettingsViewController+Swift.swift */; }; + 0CFE9ACA2AF52D3B00B8F659 /* PostSettingsViewController+Swift.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CFE9AC82AF52D3B00B8F659 /* PostSettingsViewController+Swift.swift */; }; 1702BBDC1CEDEA6B00766A33 /* BadgeLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1702BBDB1CEDEA6B00766A33 /* BadgeLabel.swift */; }; 1702BBE01CF3034E00766A33 /* DomainsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1702BBDF1CF3034E00766A33 /* DomainsService.swift */; }; 17039225282E6D2800F602E9 /* ViewsVisitorsLineChartCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC772B0728201F5300664C02 /* ViewsVisitorsLineChartCell.swift */; }; @@ -496,8 +561,6 @@ 170BEC8B239153160017AEC1 /* FeatureFlagOverrideStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17A09B98238FE13B0022AE0D /* FeatureFlagOverrideStore.swift */; }; 170BEC8C2391533D0017AEC1 /* KeyValueDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1751E5901CE0E552000CA08D /* KeyValueDatabase.swift */; }; 170CE7402064478600A48191 /* PostNoticeNavigationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 170CE73F2064478600A48191 /* PostNoticeNavigationCoordinator.swift */; }; - 171096CB270F01EA001BCDD6 /* DomainSuggestionsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 171096CA270F01EA001BCDD6 /* DomainSuggestionsTableViewController.swift */; }; - 171096CC270F01EA001BCDD6 /* DomainSuggestionsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 171096CA270F01EA001BCDD6 /* DomainSuggestionsTableViewController.swift */; }; 1714F8D020E6DA8900226DCB /* RouteMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1714F8CF20E6DA8900226DCB /* RouteMatcher.swift */; }; 1715179220F4B2EB002C4A38 /* Routes+Stats.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1715179120F4B2EB002C4A38 /* Routes+Stats.swift */; }; 1715179420F4B5CD002C4A38 /* MySitesCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1715179320F4B5CD002C4A38 /* MySitesCoordinator.swift */; }; @@ -507,7 +570,6 @@ 1717139F265FE59700F3A022 /* ButtonStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1717139E265FE59700F3A022 /* ButtonStyles.swift */; }; 171713A0265FE59700F3A022 /* ButtonStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1717139E265FE59700F3A022 /* ButtonStyles.swift */; }; 171963401D378D5100898E8B /* SearchWrapperView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1719633F1D378D5100898E8B /* SearchWrapperView.swift */; }; - 171CC15824FCEBF7008B7180 /* UINavigationBar+Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 171CC15724FCEBF7008B7180 /* UINavigationBar+Appearance.swift */; }; 17222D80261DDDF90047B163 /* celadon-classic-icon-app-76x76.png in Resources */ = {isa = PBXBuildFile; fileRef = 17222D45261DDDF10047B163 /* celadon-classic-icon-app-76x76.png */; }; 17222D81261DDDF90047B163 /* celadon-classic-icon-app-76x76@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 17222D46261DDDF10047B163 /* celadon-classic-icon-app-76x76@2x.png */; }; 17222D82261DDDF90047B163 /* celadon-classic-icon-app-83.5x83.5@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 17222D47261DDDF10047B163 /* celadon-classic-icon-app-83.5x83.5@2x.png */; }; @@ -564,9 +626,7 @@ 172F06BB2865C04F00C78FD4 /* spectrum-'22-icon-app-76x76@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 172F06B62865C04E00C78FD4 /* spectrum-'22-icon-app-76x76@2x.png */; }; 172F06BC2865C04F00C78FD4 /* spectrum-'22-icon-app-83.5x83.5@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 172F06B72865C04F00C78FD4 /* spectrum-'22-icon-app-83.5x83.5@2x.png */; }; 172F06BD2865C04F00C78FD4 /* spectrum-'22-icon-app-60x60@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 172F06B82865C04F00C78FD4 /* spectrum-'22-icon-app-60x60@2x.png */; }; - 1730D4A31E97E3E400326B7C /* MediaItemTableViewCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1730D4A21E97E3E400326B7C /* MediaItemTableViewCells.swift */; }; - 173B215527875E0600D4DD6B /* DeviceMediaPermissionsHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173B215427875E0600D4DD6B /* DeviceMediaPermissionsHeader.swift */; }; - 173B215627875E0600D4DD6B /* DeviceMediaPermissionsHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173B215427875E0600D4DD6B /* DeviceMediaPermissionsHeader.swift */; }; + 1730D4A31E97E3E400326B7C /* MediaItemHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1730D4A21E97E3E400326B7C /* MediaItemHeaderView.swift */; }; 173BCE791CEB780800AE8817 /* Domain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173BCE781CEB780800AE8817 /* Domain.swift */; }; 173D82E7238EE2A7008432DA /* FeatureFlagTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173D82E6238EE2A7008432DA /* FeatureFlagTests.swift */; }; 173DF291274522A1007C64B5 /* AppAboutScreenConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173DF290274522A1007C64B5 /* AppAboutScreenConfiguration.swift */; }; @@ -596,9 +656,6 @@ 1759F1721FE017F20003EC81 /* QueueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1759F1711FE017F20003EC81 /* QueueTests.swift */; }; 1759F1801FE1460C0003EC81 /* NoticeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1759F17F1FE1460C0003EC81 /* NoticeView.swift */; }; 175A650C20B6F7280023E71B /* ReaderSaveForLater+Analytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 175A650B20B6F7280023E71B /* ReaderSaveForLater+Analytics.swift */; }; - 175CC1702720548700622FB4 /* DomainExpiryDateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 175CC16F2720548700622FB4 /* DomainExpiryDateFormatter.swift */; }; - 175CC1712720548700622FB4 /* DomainExpiryDateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 175CC16F2720548700622FB4 /* DomainExpiryDateFormatter.swift */; }; - 175CC17527205BFB00622FB4 /* DomainExpiryDateFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 175CC17427205BFB00622FB4 /* DomainExpiryDateFormatterTests.swift */; }; 175CC1772721814C00622FB4 /* domain-service-updated-domains.json in Resources */ = {isa = PBXBuildFile; fileRef = 175CC1762721814B00622FB4 /* domain-service-updated-domains.json */; }; 175CC17927230DC900622FB4 /* Bool+StringRepresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 175CC17827230DC900622FB4 /* Bool+StringRepresentation.swift */; }; 175CC17A27230DC900622FB4 /* Bool+StringRepresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 175CC17827230DC900622FB4 /* Bool+StringRepresentation.swift */; }; @@ -685,7 +742,6 @@ 17B7C89E20EC1D0D0042E260 /* UniversalLinkRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17B7C89D20EC1D0D0042E260 /* UniversalLinkRouter.swift */; }; 17B7C8A020EC1D6A0042E260 /* Route.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17B7C89F20EC1D6A0042E260 /* Route.swift */; }; 17B7C8C120EE2A870042E260 /* Routes+Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17B7C8C020EE2A870042E260 /* Routes+Notifications.swift */; }; - 17BB26AE1E6D8321008CD031 /* MediaLibraryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17BB26AD1E6D8321008CD031 /* MediaLibraryViewController.swift */; }; 17BD4A0820F76A4700975AC3 /* Routes+Banners.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17BD4A0720F76A4700975AC3 /* Routes+Banners.swift */; }; 17BD4A192101D31B00975AC3 /* NavigationActionHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17BD4A182101D31B00975AC3 /* NavigationActionHelpers.swift */; }; 17C1D67C2670E3DC006C8970 /* SiteIconPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17C1D67B2670E3DC006C8970 /* SiteIconPickerView.swift */; }; @@ -722,6 +778,9 @@ 17FCA6811FD84B4600DBA9C8 /* NoticeStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17FCA6801FD84B4600DBA9C8 /* NoticeStore.swift */; }; 1A433B1D2254CBEE00AE7910 /* WordPressComRestApi+Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A433B1C2254CBEE00AE7910 /* WordPressComRestApi+Defaults.swift */; }; 1ABA150822AE5F870039311A /* WordPressUIBundleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ABA150722AE5F870039311A /* WordPressUIBundleTests.swift */; }; + 1D0402732B10FA9100888C30 /* AppSettingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D0402722B10FA9100888C30 /* AppSettingsTests.swift */; }; + 1D0402742B10FA9100888C30 /* AppSettingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D0402722B10FA9100888C30 /* AppSettingsTests.swift */; }; + 1D0402762B10FB9E00888C30 /* AppSettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D0402752B10FB9E00888C30 /* AppSettingsScreen.swift */; }; 1D19C56329C9D9A700FB0087 /* GutenbergVideoPressUploadProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D19C56229C9D9A700FB0087 /* GutenbergVideoPressUploadProcessor.swift */; }; 1D19C56429C9D9A700FB0087 /* GutenbergVideoPressUploadProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D19C56229C9D9A700FB0087 /* GutenbergVideoPressUploadProcessor.swift */; }; 1D19C56629C9DB0A00FB0087 /* GutenbergVideoPressUploadProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D19C56529C9DB0A00FB0087 /* GutenbergVideoPressUploadProcessorTests.swift */; }; @@ -824,6 +883,7 @@ 37022D931981C19000F322B7 /* VerticallyStackedButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 37022D901981BF9200F322B7 /* VerticallyStackedButton.m */; }; 374CB16215B93C0800DD0EBC /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 374CB16115B93C0800DD0EBC /* AudioToolbox.framework */; }; 37EAAF4D1A11799A006D6306 /* CircularImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EAAF4C1A11799A006D6306 /* CircularImageView.swift */; }; + 3F03F2BD2B45041E00A9CE99 /* XCUIElement+TapUntil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F03F2BC2B45041E00A9CE99 /* XCUIElement+TapUntil.swift */; }; 3F09CCA82428FF3300D00A8C /* ReaderTabViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F09CCA72428FF3300D00A8C /* ReaderTabViewController.swift */; }; 3F09CCAA2428FF8300D00A8C /* ReaderTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F09CCA92428FF8300D00A8C /* ReaderTabView.swift */; }; 3F09CCAE24292EFD00D00A8C /* ReaderTabItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F09CCAD24292EFD00D00A8C /* ReaderTabItem.swift */; }; @@ -840,8 +900,6 @@ 3F2ABE1A2770EF3E005D8916 /* Blog+VideoLimits.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F2ABE192770EF3E005D8916 /* Blog+VideoLimits.swift */; }; 3F2ABE1B277118C4005D8916 /* Blog+VideoLimits.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F2ABE192770EF3E005D8916 /* Blog+VideoLimits.swift */; }; 3F2ABE1C277118C9005D8916 /* VideoLimitsAlertPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F2ABE15277037A9005D8916 /* VideoLimitsAlertPresenter.swift */; }; - 3F2B62DC284F4E0B0008CD59 /* Charts in Frameworks */ = {isa = PBXBuildFile; productRef = 3F2B62DB284F4E0B0008CD59 /* Charts */; }; - 3F2B62DE284F4E310008CD59 /* Charts in Frameworks */ = {isa = PBXBuildFile; productRef = 3F2B62DD284F4E310008CD59 /* Charts */; }; 3F2F854026FAE9DC000FCDA5 /* BlockEditorScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC2BB0C92289CC3B0034F9AB /* BlockEditorScreen.swift */; }; 3F2F854226FAEA50000FCDA5 /* JetpackScanScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA4104732639393700E90EBF /* JetpackScanScreen.swift */; }; 3F2F854326FAEA50000FCDA5 /* JetpackBackupScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA4104BE26393F1A00E90EBF /* JetpackBackupScreen.swift */; }; @@ -880,10 +938,8 @@ 3F3B23C42858A1D800CACE60 /* BuildkiteTestCollector in Frameworks */ = {isa = PBXBuildFile; productRef = 3F3B23C32858A1D800CACE60 /* BuildkiteTestCollector */; }; 3F3CA65025D3003C00642A89 /* StatsWidgetsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F3CA64F25D3003C00642A89 /* StatsWidgetsStore.swift */; }; 3F3D854B251E6418001CA4D2 /* AnnouncementsDataStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F3D854A251E6418001CA4D2 /* AnnouncementsDataStoreTests.swift */; }; - 3F3DD0AF26FCDA3100F5F121 /* PresentationButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F3DD0AE26FCDA3100F5F121 /* PresentationButton.swift */; }; - 3F3DD0B026FCDA3100F5F121 /* PresentationButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F3DD0AE26FCDA3100F5F121 /* PresentationButton.swift */; }; - 3F3DD0B226FD176800F5F121 /* PresentationCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F3DD0B126FD176800F5F121 /* PresentationCard.swift */; }; - 3F3DD0B326FD176800F5F121 /* PresentationCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F3DD0B126FD176800F5F121 /* PresentationCard.swift */; }; + 3F3DD0B226FD176800F5F121 /* SiteDomainsPresentationCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F3DD0B126FD176800F5F121 /* SiteDomainsPresentationCard.swift */; }; + 3F3DD0B326FD176800F5F121 /* SiteDomainsPresentationCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F3DD0B126FD176800F5F121 /* SiteDomainsPresentationCard.swift */; }; 3F3DD0B626FD18EB00F5F121 /* Blog+DomainsDashboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F3DD0B526FD18EB00F5F121 /* Blog+DomainsDashboardView.swift */; }; 3F3DD0B726FD18EB00F5F121 /* Blog+DomainsDashboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F3DD0B526FD18EB00F5F121 /* Blog+DomainsDashboardView.swift */; }; 3F411B6F28987E3F002513AE /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 3F411B6E28987E3F002513AE /* Lottie */; }; @@ -897,7 +953,6 @@ 3F43704428932F0100475B6E /* JetpackBrandingCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F43704328932F0100475B6E /* JetpackBrandingCoordinator.swift */; }; 3F44DD58289C379C006334CD /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 3F44DD57289C379C006334CD /* Lottie */; }; 3F46AAFE25BF5D6300CE2E98 /* Sites.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 3F46AB0225BF5D6300CE2E98 /* Sites.intentdefinition */; settings = {ATTRIBUTES = (codegen, ); }; }; - 3F46EEC728BC4935004F02B2 /* JetpackPrompt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F46EEC628BC4935004F02B2 /* JetpackPrompt.swift */; }; 3F46EECE28BC4962004F02B2 /* JetpackLandingScreenView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F46EECB28BC4962004F02B2 /* JetpackLandingScreenView.swift */; }; 3F46EED128BFF339004F02B2 /* JetpackPromptsConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F46EED028BFF339004F02B2 /* JetpackPromptsConfiguration.swift */; }; 3F4A4C212AD39CB100DE5DF8 /* TruthTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F4A4C202AD39CB100DE5DF8 /* TruthTable.swift */; }; @@ -909,6 +964,7 @@ 3F4EB39228AC561600B8DD86 /* JetpackWordPressLogoAnimation_rtl.json in Resources */ = {isa = PBXBuildFile; fileRef = 3F4EB39128AC561600B8DD86 /* JetpackWordPressLogoAnimation_rtl.json */; }; 3F50945B2454ECA000C4470B /* ReaderTabItemsStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F50945A2454ECA000C4470B /* ReaderTabItemsStoreTests.swift */; }; 3F50945F245537A700C4470B /* ReaderTabViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F50945E245537A700C4470B /* ReaderTabViewModelTests.swift */; }; + 3F56F55C2AEA2F67006BDCEA /* ReaderPostBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F56F55B2AEA2F67006BDCEA /* ReaderPostBuilder.swift */; }; 3F593FDD2A81DC6D00B29E86 /* NSError+TestInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F593FDC2A81DC6D00B29E86 /* NSError+TestInstance.swift */; }; 3F5AAC242877791900AEF5DD /* JetpackButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FFA5ED12876152E00830E28 /* JetpackButton.swift */; }; 3F5B3EAF23A851330060FF1F /* ReaderReblogPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F5B3EAE23A851330060FF1F /* ReaderReblogPresenter.swift */; }; @@ -962,6 +1018,9 @@ 3F946C592684DD8E00B946F6 /* BloggingRemindersActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F946C582684DD8E00B946F6 /* BloggingRemindersActions.swift */; }; 3F946C5A2684DD8E00B946F6 /* BloggingRemindersActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F946C582684DD8E00B946F6 /* BloggingRemindersActions.swift */; }; 3F95FF4026C4F385007731D3 /* ScreenObject in Frameworks */ = {isa = PBXBuildFile; productRef = 3FC2C34226C4E8B700C6D98F /* ScreenObject */; }; + 3F9F23252B0AE1AC00B56061 /* JetpackStatsWidgetsCore in Frameworks */ = {isa = PBXBuildFile; productRef = 3F9F23242B0AE1AC00B56061 /* JetpackStatsWidgetsCore */; }; + 3F9F232B2B0B27DD00B56061 /* JetpackStatsWidgetsCore in Frameworks */ = {isa = PBXBuildFile; productRef = 3F9F232A2B0B27DD00B56061 /* JetpackStatsWidgetsCore */; }; + 3F9F232D2B0B281400B56061 /* JetpackStatsWidgetsCore in Frameworks */ = {isa = PBXBuildFile; productRef = 3F9F232C2B0B281400B56061 /* JetpackStatsWidgetsCore */; }; 3FA53E9C256571D800F4D9A2 /* HomeWidgetCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FA53E9B256571D800F4D9A2 /* HomeWidgetCache.swift */; }; 3FA62FD326FE2E4B0020793A /* ShapeWithTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FA62FD226FE2E4B0020793A /* ShapeWithTextView.swift */; }; 3FA62FD426FE2E4B0020793A /* ShapeWithTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FA62FD226FE2E4B0020793A /* ShapeWithTextView.swift */; }; @@ -972,8 +1031,8 @@ 3FA640672670D1290064401E /* UITestsFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3FA640572670CCD40064401E /* UITestsFoundation.framework */; }; 3FAE0652287C8FC500F46508 /* JPScrollViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FAE0651287C8FC500F46508 /* JPScrollViewDelegate.swift */; }; 3FAE0653287C8FC500F46508 /* JPScrollViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FAE0651287C8FC500F46508 /* JPScrollViewDelegate.swift */; }; - 3FAF9CC226D01CFE00268EA2 /* DomainsDashboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FAF9CC126D01CFE00268EA2 /* DomainsDashboardView.swift */; }; - 3FAF9CC326D02FC500268EA2 /* DomainsDashboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FAF9CC126D01CFE00268EA2 /* DomainsDashboardView.swift */; }; + 3FAF9CC226D01CFE00268EA2 /* SiteDomainsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FAF9CC126D01CFE00268EA2 /* SiteDomainsView.swift */; }; + 3FAF9CC326D02FC500268EA2 /* SiteDomainsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FAF9CC126D01CFE00268EA2 /* SiteDomainsView.swift */; }; 3FAF9CC526D03C7400268EA2 /* DomainSuggestionViewControllerWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FAF9CC426D03C7400268EA2 /* DomainSuggestionViewControllerWrapper.swift */; }; 3FB1929026C6109F000F5AA3 /* TimeSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FB1928F26C6109F000F5AA3 /* TimeSelectionView.swift */; }; 3FB1929126C6C56E000F5AA3 /* TimeSelectionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F73388126C1CE9B0075D1DD /* TimeSelectionButton.swift */; }; @@ -1016,12 +1075,16 @@ 3FE3D1FD26A6F34900F3CD10 /* WPStyleGuide+List.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEA088042696F7AA00193358 /* WPStyleGuide+List.swift */; }; 3FE3D1FE26A6F4AC00F3CD10 /* ListTableHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEA088002696E7F600193358 /* ListTableHeaderView.swift */; }; 3FE3D1FF26A6F56700F3CD10 /* Comment+Interface.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE02F95E269DC14A00752A44 /* Comment+Interface.swift */; }; + 3FE6D31E2B0705D400D14923 /* JetpackBrandingVisibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FE6D31D2B0705D400D14923 /* JetpackBrandingVisibilityTests.swift */; }; 3FEC241525D73E8B007AFE63 /* ConfettiView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FEC241425D73E8B007AFE63 /* ConfettiView.swift */; }; 3FF15A56291B4EEA00E1B4E5 /* MigrationCenterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FF15A55291B4EEA00E1B4E5 /* MigrationCenterView.swift */; }; 3FF15A5C291ED21100E1B4E5 /* MigrationNotificationsCenterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FF15A5B291ED21100E1B4E5 /* MigrationNotificationsCenterView.swift */; }; 3FF1A853242D5FCB00373F5D /* WPTabBarController+ReaderTabNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FF1A852242D5FCB00373F5D /* WPTabBarController+ReaderTabNavigation.swift */; }; 3FF717FF291F07AB00323614 /* MigrationCenterViewConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FF717FE291F07AB00323614 /* MigrationCenterViewConfiguration.swift */; }; 3FFA5ED22876152E00830E28 /* JetpackButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FFA5ED12876152E00830E28 /* JetpackButton.swift */; }; + 3FFB3F202AFC70B400A742B0 /* JetpackStatsWidgetsCore in Frameworks */ = {isa = PBXBuildFile; productRef = 3FFB3F1F2AFC70B400A742B0 /* JetpackStatsWidgetsCore */; }; + 3FFB3F222AFC72EC00A742B0 /* DeepLinkSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FFB3F212AFC72EC00A742B0 /* DeepLinkSourceTests.swift */; }; + 3FFB3F242AFC730C00A742B0 /* JetpackStatsWidgetsCore in Frameworks */ = {isa = PBXBuildFile; productRef = 3FFB3F232AFC730C00A742B0 /* JetpackStatsWidgetsCore */; }; 3FFDEF7829177D7500B625CE /* MigrationNotificationsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FFDEF7729177D7500B625CE /* MigrationNotificationsViewModel.swift */; }; 3FFDEF7A29177D8C00B625CE /* MigrationNotificationsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FFDEF7929177D8C00B625CE /* MigrationNotificationsViewController.swift */; }; 3FFDEF7F29177FB100B625CE /* MigrationStepConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FFDEF7E29177FB100B625CE /* MigrationStepConfiguration.swift */; }; @@ -1042,7 +1105,6 @@ 40232A9E230A6A740036B0B6 /* AbstractPost+HashHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = 40232A9D230A6A740036B0B6 /* AbstractPost+HashHelpers.m */; }; 40247E022120FE3600AE1C3C /* AutomatedTransferHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40247E012120FE3600AE1C3C /* AutomatedTransferHelper.swift */; }; 402B2A7920ACD7690027C1DC /* ActivityStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402B2A7820ACD7690027C1DC /* ActivityStore.swift */; }; - 402FFB1C218C27C100FF4A0B /* RegisterDomain.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 402FFB1B218C27C100FF4A0B /* RegisterDomain.storyboard */; }; 403269922027719C00608441 /* PluginDirectoryAccessoryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 403269912027719C00608441 /* PluginDirectoryAccessoryItem.swift */; }; 4034FDEA2007C42400153B87 /* ExpandableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4034FDE92007C42400153B87 /* ExpandableCell.swift */; }; 4034FDEE2007D4F700153B87 /* ExpandableCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4034FDED2007D4F700153B87 /* ExpandableCell.xib */; }; @@ -1114,7 +1176,6 @@ 436D55DF210F866900CEAA33 /* StoryboardLoadable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 436D55DE210F866900CEAA33 /* StoryboardLoadable.swift */; }; 436D55F02115CB6800CEAA33 /* RegisterDomainDetailsSectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 436D55EF2115CB6800CEAA33 /* RegisterDomainDetailsSectionTests.swift */; }; 436D55F5211632B700CEAA33 /* RegisterDomainDetailsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 436D55F4211632B700CEAA33 /* RegisterDomainDetailsViewModelTests.swift */; }; - 436D561F2117312700CEAA33 /* RegisterDomainSuggestionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 436D560C2117312600CEAA33 /* RegisterDomainSuggestionsViewController.swift */; }; 436D56212117312700CEAA33 /* RegisterDomainDetailsViewModel+SectionDefinitions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 436D56102117312700CEAA33 /* RegisterDomainDetailsViewModel+SectionDefinitions.swift */; }; 436D56222117312700CEAA33 /* RegisterDomainDetailsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 436D56112117312700CEAA33 /* RegisterDomainDetailsViewModel.swift */; }; 436D56242117312700CEAA33 /* RegisterDomainDetailsViewModel+RowList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 436D56132117312700CEAA33 /* RegisterDomainDetailsViewModel+RowList.swift */; }; @@ -1174,8 +1235,6 @@ 465F8A0A263B692600F4C950 /* wp-block-editor-v1-settings-success-ThemeJSON.json in Resources */ = {isa = PBXBuildFile; fileRef = 465F8A09263B692600F4C950 /* wp-block-editor-v1-settings-success-ThemeJSON.json */; }; 46638DF6244904A3006E8439 /* GutenbergBlockProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46638DF5244904A3006E8439 /* GutenbergBlockProcessor.swift */; }; 4666534A2501552A00165DD4 /* LayoutPreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 466653492501552A00165DD4 /* LayoutPreviewViewController.swift */; }; - 467D3DFA25E4436000EB9CB0 /* SitePromptView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 467D3DF925E4436000EB9CB0 /* SitePromptView.swift */; }; - 467D3E0C25E4436D00EB9CB0 /* SitePromptView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 467D3E0B25E4436D00EB9CB0 /* SitePromptView.xib */; }; 4688E6CC26AB571D00A5D894 /* RequestAuthenticatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4688E6CB26AB571D00A5D894 /* RequestAuthenticatorTests.swift */; }; 469CE06D24BCED75003BDC8B /* CategorySectionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 469CE06B24BCED75003BDC8B /* CategorySectionTableViewCell.swift */; }; 469CE06E24BCED75003BDC8B /* CategorySectionTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 469CE06C24BCED75003BDC8B /* CategorySectionTableViewCell.xib */; }; @@ -1221,8 +1280,6 @@ 4A1E77CD2989F2F7006281CC /* WPAccount+DeduplicateBlogs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A1E77CB2989F2F7006281CC /* WPAccount+DeduplicateBlogs.swift */; }; 4A2172F828EAACFF0006F4F1 /* BlogQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A2172F728EAACFF0006F4F1 /* BlogQuery.swift */; }; 4A2172F928EAACFF0006F4F1 /* BlogQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A2172F728EAACFF0006F4F1 /* BlogQuery.swift */; }; - 4A2172FE28F688890006F4F1 /* Blog+Media.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A2172FD28F688890006F4F1 /* Blog+Media.swift */; }; - 4A2172FF28F688890006F4F1 /* Blog+Media.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A2172FD28F688890006F4F1 /* Blog+Media.swift */; }; 4A266B8F282B05210089CF3D /* JSONObjectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A266B8E282B05210089CF3D /* JSONObjectTests.swift */; }; 4A266B91282B13A70089CF3D /* CoreDataTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A266B90282B13A70089CF3D /* CoreDataTestCase.swift */; }; 4A2C73E12A943D9000ACE79E /* TaggedManagedObjectID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A2C73E02A943D8F00ACE79E /* TaggedManagedObjectID.swift */; }; @@ -1239,6 +1296,11 @@ 4A358DEA29B5F14C00BFCEBE /* SharingButton+Lookup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A358DE829B5F14C00BFCEBE /* SharingButton+Lookup.swift */; }; 4A526BDF296BE9A50007B5BA /* CoreDataService.m in Sources */ = {isa = PBXBuildFile; fileRef = 4A526BDD296BE9A50007B5BA /* CoreDataService.m */; }; 4A526BE0296BE9A50007B5BA /* CoreDataService.m in Sources */ = {isa = PBXBuildFile; fileRef = 4A526BDD296BE9A50007B5BA /* CoreDataService.m */; }; + 4A535E142AF3368B008B87B9 /* MenusViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A535E132AF3368B008B87B9 /* MenusViewController.swift */; }; + 4A535E152AF3368B008B87B9 /* MenusViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A535E132AF3368B008B87B9 /* MenusViewController.swift */; }; + 4A5598852B05AC180083C220 /* PagesListTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5598842B05AC180083C220 /* PagesListTests.swift */; }; + 4A5DE7382B0D511900363171 /* PageTree.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5DE7372B0D511900363171 /* PageTree.swift */; }; + 4A5DE7392B0D511900363171 /* PageTree.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5DE7372B0D511900363171 /* PageTree.swift */; }; 4A76A4BB29D4381100AABF4B /* CommentService+LikesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A76A4BA29D4381000AABF4B /* CommentService+LikesTests.swift */; }; 4A76A4BD29D43BFD00AABF4B /* CommentService+MorderationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A76A4BC29D43BFD00AABF4B /* CommentService+MorderationTests.swift */; }; 4A76A4BF29D4F0A500AABF4B /* reader-post-comments-success.json in Resources */ = {isa = PBXBuildFile; fileRef = 4A76A4BE29D4F0A500AABF4B /* reader-post-comments-success.json */; }; @@ -1264,12 +1326,14 @@ 4AA33F022999D11A005B6E23 /* ReaderSiteTopic+Lookup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA33F002999D11A005B6E23 /* ReaderSiteTopic+Lookup.swift */; }; 4AA33F04299A1F93005B6E23 /* ReaderTagTopic+Lookup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA33F03299A1F93005B6E23 /* ReaderTagTopic+Lookup.swift */; }; 4AA33F05299A1F93005B6E23 /* ReaderTagTopic+Lookup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA33F03299A1F93005B6E23 /* ReaderTagTopic+Lookup.swift */; }; + 4AA7EE0F2ADF7367007D261D /* PostRepositoryPostsListTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA7EE0E2ADF7367007D261D /* PostRepositoryPostsListTests.swift */; }; 4AAD69082A6F68A5007FE77E /* MediaRepositoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AAD69072A6F68A5007FE77E /* MediaRepositoryTests.swift */; }; 4AD5656C28E3D0670054C676 /* ReaderPost+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AD5656B28E3D0670054C676 /* ReaderPost+Helper.swift */; }; 4AD5656D28E3D0670054C676 /* ReaderPost+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AD5656B28E3D0670054C676 /* ReaderPost+Helper.swift */; }; 4AD5656F28E413160054C676 /* Blog+History.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AD5656E28E413160054C676 /* Blog+History.swift */; }; 4AD5657028E413160054C676 /* Blog+History.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AD5656E28E413160054C676 /* Blog+History.swift */; }; 4AD5657228E543A30054C676 /* BlogQueryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AD5657128E543A30054C676 /* BlogQueryTests.swift */; }; + 4AD862E52AFAEF1700A07557 /* PostsListAPIStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AD862E42AFAEF1700A07557 /* PostsListAPIStub.swift */; }; 4AEF2DD929A84B2C00345734 /* ReaderSiteServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AEF2DD829A84B2C00345734 /* ReaderSiteServiceTests.swift */; }; 4AFB1A812A9C08CE007CE165 /* StoppableProgressIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AFB1A802A9C08CE007CE165 /* StoppableProgressIndicatorView.swift */; }; 4AFB1A822A9C08CE007CE165 /* StoppableProgressIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AFB1A802A9C08CE007CE165 /* StoppableProgressIndicatorView.swift */; }; @@ -1278,13 +1342,8 @@ 4BB2296498BE66D515E3D610 /* Pods_JetpackShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 23052F0F1F9B2503E33D0A26 /* Pods_JetpackShareExtension.framework */; }; 4D520D4F22972BC9002F5924 /* acknowledgements.html in Resources */ = {isa = PBXBuildFile; fileRef = 4D520D4E22972BC9002F5924 /* acknowledgements.html */; }; 56885C912A7D15930027C78F /* HTMLEditorScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56885C902A7D15930027C78F /* HTMLEditorScreen.swift */; }; - 570265152298921800F2214C /* PostListTableViewHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 570265142298921800F2214C /* PostListTableViewHandler.swift */; }; - 570265172298960B00F2214C /* PostListTableViewHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 570265162298960B00F2214C /* PostListTableViewHandlerTests.swift */; }; 5703A4C622C003DC0028A343 /* WPStyleGuide+Posts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5703A4C522C003DC0028A343 /* WPStyleGuide+Posts.swift */; }; - 57047A4F22A961BC00B461DF /* PostSearchHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57047A4E22A961BC00B461DF /* PostSearchHeader.swift */; }; 570B037722F1FFF6009D8411 /* PostCoordinatorFailedPostsFetcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 570B037622F1FFF6009D8411 /* PostCoordinatorFailedPostsFetcherTests.swift */; }; - 570BFD8B22823D7B007859A8 /* PostActionSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 570BFD8A22823D7B007859A8 /* PostActionSheet.swift */; }; - 570BFD8D22823DE5007859A8 /* PostActionSheetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 570BFD8C22823DE5007859A8 /* PostActionSheetTests.swift */; }; 570BFD902282418A007859A8 /* PostBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 570BFD8F2282418A007859A8 /* PostBuilder.swift */; }; 57240224234E5BE200227067 /* PostServiceSelfHostedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57240223234E5BE200227067 /* PostServiceSelfHostedTests.swift */; }; 57276E71239BDFD200515BE2 /* NotificationCenter+ObserveMultiple.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57276E70239BDFD200515BE2 /* NotificationCenter+ObserveMultiple.swift */; }; @@ -1294,15 +1353,10 @@ 57569CF2230485680052EE14 /* PostAutoUploadInteractorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57569CF1230485680052EE14 /* PostAutoUploadInteractorTests.swift */; }; 575802132357C41200E4C63C /* MediaCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 575802122357C41200E4C63C /* MediaCoordinatorTests.swift */; }; 575E126322973EBB0041B3EB /* PostCompactCellGhostableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 575E126222973EBB0041B3EB /* PostCompactCellGhostableTests.swift */; }; - 575E126F229779E70041B3EB /* RestorePostTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 575E126E229779E70041B3EB /* RestorePostTableViewCell.swift */; }; - 577C2AAB22936DCB00AD1F03 /* PostCardCellGhostableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 577C2AAA22936DCB00AD1F03 /* PostCardCellGhostableTests.swift */; }; 577C2AB422943FEC00AD1F03 /* PostCompactCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 577C2AB322943FEC00AD1F03 /* PostCompactCell.swift */; }; 577C2AB62294401800AD1F03 /* PostCompactCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 577C2AB52294401800AD1F03 /* PostCompactCell.xib */; }; 57889AB823589DF100DAE56D /* PageBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57889AB723589DF100DAE56D /* PageBuilder.swift */; }; 5789E5C822D7D40800333698 /* AztecPostViewControllerAttachmentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5789E5C722D7D40800333698 /* AztecPostViewControllerAttachmentTests.swift */; }; - 57AA848F228715DA00D3C2A2 /* PostCardCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57AA848E228715DA00D3C2A2 /* PostCardCell.swift */; }; - 57AA8491228715E700D3C2A2 /* PostCardCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 57AA8490228715E700D3C2A2 /* PostCardCell.xib */; }; - 57AA8493228790AA00D3C2A2 /* PostCardCellTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57AA8492228790AA00D3C2A2 /* PostCardCellTests.swift */; }; 57B71D4E230DB5F200789A68 /* BlogBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57B71D4D230DB5F200789A68 /* BlogBuilder.swift */; }; 57BAD50C225CCE1A006139EC /* WPTabBarController+Swift.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57BAD50B225CCE1A006139EC /* WPTabBarController+Swift.swift */; }; 57C2331822FE0EC900A3863B /* PostAutoUploadInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57C2331722FE0EC900A3863B /* PostAutoUploadInteractor.swift */; }; @@ -1312,7 +1366,6 @@ 57D66B9A234BB206005A2D74 /* PostServiceRemoteFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57D66B99234BB206005A2D74 /* PostServiceRemoteFactory.swift */; }; 57D66B9D234BB78B005A2D74 /* PostServiceWPComTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57D66B9C234BB78B005A2D74 /* PostServiceWPComTests.swift */; }; 57D6C83E22945A10003DDC7E /* PostCompactCellTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57D6C83D22945A10003DDC7E /* PostCompactCellTests.swift */; }; - 57D6C840229498C5003DDC7E /* InteractivePostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57D6C83F229498C4003DDC7E /* InteractivePostView.swift */; }; 57DF04C1231489A200CC93D6 /* PostCardStatusViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57DF04C0231489A200CC93D6 /* PostCardStatusViewModelTests.swift */; }; 590E873B1CB8205700D1B734 /* PostListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 590E873A1CB8205700D1B734 /* PostListViewController.swift */; }; 591232691CCEAA5100B86207 /* AbstractPostListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 591232681CCEAA5100B86207 /* AbstractPostListViewController.swift */; }; @@ -1330,7 +1383,6 @@ 596C03601B84F24000899EEB /* ThemeBrowser.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 596C035F1B84F24000899EEB /* ThemeBrowser.storyboard */; }; 5981FE051AB8A89A0009E080 /* WPUserAgentTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 5981FE041AB8A89A0009E080 /* WPUserAgentTests.m */; }; 598DD1711B97985700146967 /* ThemeBrowserCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 598DD1701B97985700146967 /* ThemeBrowserCell.swift */; }; - 59A3CADD1CD2FF0C009BFA1B /* BasePageListCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 59A3CADC1CD2FF0C009BFA1B /* BasePageListCell.m */; }; 59A9AB351B4C33A500A433DC /* ThemeService.m in Sources */ = {isa = PBXBuildFile; fileRef = 59A9AB341B4C33A500A433DC /* ThemeService.m */; }; 59B48B621B99E132008EBB84 /* JSONObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59B48B611B99E132008EBB84 /* JSONObject.swift */; }; 59DCA5211CC68AF3000F245F /* PageListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59DCA5201CC68AF3000F245F /* PageListViewController.swift */; }; @@ -1342,12 +1394,9 @@ 5D1181E71B4D6DEB003F3084 /* WPStyleGuide+Reader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D1181E61B4D6DEB003F3084 /* WPStyleGuide+Reader.swift */; }; 5D13FA571AF99C2100F06492 /* PageListSectionHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5D13FA561AF99C2100F06492 /* PageListSectionHeaderView.xib */; }; 5D146EBB189857ED0068FDC6 /* FeaturedImageViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D146EBA189857ED0068FDC6 /* FeaturedImageViewController.m */; }; - 5D18FE9F1AFBB17400EFEED0 /* RestorePageTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D18FE9D1AFBB17400EFEED0 /* RestorePageTableViewCell.m */; }; - 5D18FEA01AFBB17400EFEED0 /* RestorePageTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5D18FE9E1AFBB17400EFEED0 /* RestorePageTableViewCell.xib */; }; 5D1D04751B7A50B100CDE646 /* Reader.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5D1D04731B7A50B100CDE646 /* Reader.storyboard */; }; 5D1D04761B7A50B100CDE646 /* ReaderStreamViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D1D04741B7A50B100CDE646 /* ReaderStreamViewController.swift */; }; 5D2B30B91B7411C700DA15F3 /* ReaderCardDiscoverAttributionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D2B30B81B7411C700DA15F3 /* ReaderCardDiscoverAttributionView.swift */; }; - 5D2FB2831AE98C4600F1D4ED /* RestorePostTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5D2FB2821AE98C4600F1D4ED /* RestorePostTableViewCell.xib */; }; 5D3D559718F88C3500782892 /* ReaderPostService.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D3D559618F88C3500782892 /* ReaderPostService.m */; }; 5D3E334E15EEBB6B005FC6F2 /* ReachabilityUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D3E334D15EEBB6B005FC6F2 /* ReachabilityUtils.m */; }; 5D42A3DF175E7452005CFF05 /* AbstractPost.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D42A3D7175E7452005CFF05 /* AbstractPost.m */; }; @@ -1376,18 +1425,14 @@ 5D97C2F315CAF8D8009B44DD /* UINavigationController+KeyboardFix.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D97C2F215CAF8D8009B44DD /* UINavigationController+KeyboardFix.m */; }; 5DA3EE161925090A00294E0B /* MediaService.m in Sources */ = {isa = PBXBuildFile; fileRef = 5DA3EE151925090A00294E0B /* MediaService.m */; }; 5DA5BF4418E32DCF005F11F9 /* Theme.m in Sources */ = {isa = PBXBuildFile; fileRef = 5DA5BF3418E32DCF005F11F9 /* Theme.m */; }; - 5DB3BA0518D0E7B600F3F3E9 /* WPPickerView.m in Sources */ = {isa = PBXBuildFile; fileRef = 5DB3BA0418D0E7B600F3F3E9 /* WPPickerView.m */; }; 5DB4683B18A2E718004A89A9 /* LocationService.m in Sources */ = {isa = PBXBuildFile; fileRef = 5DB4683A18A2E718004A89A9 /* LocationService.m */; }; 5DB767411588F64D00EBE36C /* postPreview.html in Resources */ = {isa = PBXBuildFile; fileRef = 5DB767401588F64D00EBE36C /* postPreview.html */; }; 5DBCD9D518F35D7500B32229 /* ReaderTopicService.m in Sources */ = {isa = PBXBuildFile; fileRef = 5DBCD9D418F35D7500B32229 /* ReaderTopicService.m */; }; - 5DBFC8A91A9BE07B00E00DE4 /* Posts.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5DBFC8A81A9BE07B00E00DE4 /* Posts.storyboard */; }; 5DED0E181B432E0400431FCD /* SourcePostAttribution.m in Sources */ = {isa = PBXBuildFile; fileRef = 5DED0E171B432E0400431FCD /* SourcePostAttribution.m */; }; 5DF7F7741B22337C003A05C8 /* WordPress-30-31.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = 5DF7F7731B22337C003A05C8 /* WordPress-30-31.xcmappingmodel */; }; 5DF7F7781B223916003A05C8 /* PostToPost30To31.m in Sources */ = {isa = PBXBuildFile; fileRef = 5DF7F7771B223916003A05C8 /* PostToPost30To31.m */; }; 5DF8D26119E82B1000A2CD95 /* ReaderCommentsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 5DF8D26019E82B1000A2CD95 /* ReaderCommentsViewController.m */; }; 5DFA7EC31AF7CB910072023B /* Pages.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5DFA7EC21AF7CB910072023B /* Pages.storyboard */; }; - 5DFA7EC71AF814E40072023B /* PageListTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 5DFA7EC51AF814E40072023B /* PageListTableViewCell.m */; }; - 5DFA7EC81AF814E40072023B /* PageListTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5DFA7EC61AF814E40072023B /* PageListTableViewCell.xib */; }; 6E5BA46926A59D620043A6F2 /* SupportScreenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5BA46826A59D620043A6F2 /* SupportScreenTests.swift */; }; 730354BA21C867E500CD18C2 /* SiteCreatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 730354B921C867E500CD18C2 /* SiteCreatorTests.swift */; }; 7305138321C031FC006BD0A1 /* AssembledSiteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7305138221C031FC006BD0A1 /* AssembledSiteView.swift */; }; @@ -1447,7 +1492,6 @@ 738B9A5621B85CF20005062B /* ModelSettableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 738B9A4921B85CF20005062B /* ModelSettableCell.swift */; }; 738B9A5721B85CF20005062B /* TableDataCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 738B9A4A21B85CF20005062B /* TableDataCoordinator.swift */; }; 738B9A5821B85CF20005062B /* TitleSubtitleHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 738B9A4B21B85CF20005062B /* TitleSubtitleHeader.swift */; }; - 738B9A5921B85CF20005062B /* KeyboardInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 738B9A4C21B85CF20005062B /* KeyboardInfo.swift */; }; 738B9A5A21B85CF20005062B /* SiteCreationHeaderData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 738B9A4D21B85CF20005062B /* SiteCreationHeaderData.swift */; }; 738B9A5C21B85EB00005062B /* UIView+ContentLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 738B9A5B21B85EB00005062B /* UIView+ContentLayout.swift */; }; 738B9A5E21B8632E0005062B /* UITableView+Header.swift in Sources */ = {isa = PBXBuildFile; fileRef = 738B9A5D21B8632E0005062B /* UITableView+Header.swift */; }; @@ -1466,7 +1510,6 @@ 73C8F06621BEF76B00DDDF7E /* SiteAssemblyViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73C8F06521BEF76B00DDDF7E /* SiteAssemblyViewTests.swift */; }; 73C8F06821BF1A5E00DDDF7E /* SiteAssemblyContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73C8F06721BF1A5E00DDDF7E /* SiteAssemblyContentView.swift */; }; 73CB13972289BEFB00265F49 /* Charts+LargeValueFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73CB13962289BEFB00265F49 /* Charts+LargeValueFormatter.swift */; }; - 73CE3E0E21F7F9D3007C9C85 /* TableViewOffsetCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73CE3E0D21F7F9D3007C9C85 /* TableViewOffsetCoordinator.swift */; }; 73D5AC63212622B200ADDDD2 /* NotificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73D5AC5C212622B200ADDDD2 /* NotificationViewController.swift */; }; 73D86969223AF4040064920F /* StatsChartLegendView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73D86968223AF4040064920F /* StatsChartLegendView.swift */; }; 73E40D8921238BF50012ABA6 /* Tracks.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5FA22821C99F6180016CA7C /* Tracks.swift */; }; @@ -1595,7 +1638,6 @@ 74FA4BE61FBFA0660031EAAD /* Extensions.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 74FA4BE31FBFA0660031EAAD /* Extensions.xcdatamodeld */; }; 74FA4BED1FBFA2350031EAAD /* SharedCoreDataStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 746D6B241FBF701F003C45BE /* SharedCoreDataStack.swift */; }; 7D21280D251CF0850086DD2C /* EditPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D21280C251CF0850086DD2C /* EditPageViewController.swift */; }; - 7E14635720B3BEAB00B95F41 /* WPStyleGuide+Loader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E14635620B3BEAB00B95F41 /* WPStyleGuide+Loader.swift */; }; 7E21C761202BBC8E00837CF5 /* iAd.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7E21C760202BBC8D00837CF5 /* iAd.framework */; }; 7E21C765202BBF4400837CF5 /* SearchAdsAttribution.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E21C764202BBF4400837CF5 /* SearchAdsAttribution.swift */; }; 7E3AB3DB20F52654001F33B6 /* ActivityContentStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E3AB3DA20F52654001F33B6 /* ActivityContentStyles.swift */; }; @@ -1610,7 +1652,6 @@ 7E3E7A6420E44ED60075D159 /* SubjectContentGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E3E7A6320E44ED60075D159 /* SubjectContentGroup.swift */; }; 7E3E7A6620E44F200075D159 /* HeaderContentGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E3E7A6520E44F200075D159 /* HeaderContentGroup.swift */; }; 7E3E9B702177C9DC00FD5797 /* GutenbergViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E3E9B6F2177C9DC00FD5797 /* GutenbergViewController.swift */; }; - 7E407121237163B8003627FA /* GutenbergStockPhotos.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E407120237163B8003627FA /* GutenbergStockPhotos.swift */; }; 7E40713A2372AD54003627FA /* GutenbergFilesAppMediaSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E4071392372AD54003627FA /* GutenbergFilesAppMediaSource.swift */; }; 7E40716223741376003627FA /* GutenbergNetworking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E40716123741375003627FA /* GutenbergNetworking.swift */; }; 7E4123B920F4097B00DF8486 /* FormattableContentFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E4123AC20F4097900DF8486 /* FormattableContentFactory.swift */; }; @@ -1676,7 +1717,6 @@ 7EB5824720EC41B200002702 /* NotificationContentFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EB5824620EC41B200002702 /* NotificationContentFactory.swift */; }; 7EBB4126206C388100012D98 /* StockPhotosService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EBB4125206C388100012D98 /* StockPhotosService.swift */; }; 7EC9FE0B22C627DB00C5A888 /* PostEditorAnalyticsSessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EC9FE0A22C627DB00C5A888 /* PostEditorAnalyticsSessionTests.swift */; }; - 7ECD5B8120C4D823001AEBC5 /* MediaPreviewHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ECD5B8020C4D823001AEBC5 /* MediaPreviewHelper.swift */; }; 7EDAB3F420B046FE002D1A76 /* CircularProgressView+ActivityIndicatorType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EDAB3F320B046FE002D1A76 /* CircularProgressView+ActivityIndicatorType.swift */; }; 7EF2EEA0210A67B60007A76B /* notifications-unapproved-comment.json in Resources */ = {isa = PBXBuildFile; fileRef = 7EF2EE9F210A67B60007A76B /* notifications-unapproved-comment.json */; }; 7EFF208620EAD918009C4699 /* FormattableUserContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EFF208520EAD918009C4699 /* FormattableUserContent.swift */; }; @@ -1709,6 +1749,9 @@ 8031F34A292FF46B00E8F95E /* ExtensionConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 800035C0292307E8007D2D26 /* ExtensionConfiguration.swift */; }; 8031F34B292FF46E00E8F95E /* ExtensionConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 800035C0292307E8007D2D26 /* ExtensionConfiguration.swift */; }; 8031F34C29302A2500E8F95E /* ExtensionConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 800035C229230A0B007D2D26 /* ExtensionConfiguration.swift */; }; + 80348F312AF87FEA0045CCD3 /* AllDomainsListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80348F302AF87FEA0045CCD3 /* AllDomainsListViewController.swift */; }; + 80348F332AF880820045CCD3 /* DomainPurchaseChoicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80348F322AF880820045CCD3 /* DomainPurchaseChoicesView.swift */; }; + 80348F342AF89BD00045CCD3 /* AllDomainsAddDomainCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80348F2D2AF870A70045CCD3 /* AllDomainsAddDomainCoordinator.swift */; }; 80379C6E2A5C0D8F00D924AC /* PostTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80379C6D2A5C0D8F00D924AC /* PostTests.swift */; }; 80379C6F2A5C0D8F00D924AC /* PostTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80379C6D2A5C0D8F00D924AC /* PostTests.swift */; }; 803BB9792959543D00B3F6D6 /* RootViewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 803BB9782959543D00B3F6D6 /* RootViewCoordinator.swift */; }; @@ -1929,8 +1972,6 @@ 80A2153E29C35197002FE8EB /* StaticScreensTabBarWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80A2153C29C35197002FE8EB /* StaticScreensTabBarWrapper.swift */; }; 80A2154029CA68D5002FE8EB /* RemoteFeatureFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80A2153F29CA68D5002FE8EB /* RemoteFeatureFlag.swift */; }; 80A2154129CA68D5002FE8EB /* RemoteFeatureFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80A2153F29CA68D5002FE8EB /* RemoteFeatureFlag.swift */; }; - 80A2154329D1177A002FE8EB /* RemoteConfigDebugViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80A2154229D1177A002FE8EB /* RemoteConfigDebugViewController.swift */; }; - 80A2154429D1177A002FE8EB /* RemoteConfigDebugViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80A2154229D1177A002FE8EB /* RemoteConfigDebugViewController.swift */; }; 80A2154629D15B88002FE8EB /* RemoteConfigOverrideStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80A2154529D15B88002FE8EB /* RemoteConfigOverrideStore.swift */; }; 80A2154729D15B88002FE8EB /* RemoteConfigOverrideStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80A2154529D15B88002FE8EB /* RemoteConfigOverrideStore.swift */; }; 80B016CF27FEBDC900D15566 /* DashboardCardTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80B016CE27FEBDC900D15566 /* DashboardCardTests.swift */; }; @@ -1961,6 +2002,11 @@ 80D9D04729F765C900FE3400 /* FailableDecodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80D9D04529F760C400FE3400 /* FailableDecodable.swift */; }; 80D9D04A29FC0D9000FE3400 /* NSMutableArray+NullableObjects.m in Sources */ = {isa = PBXBuildFile; fileRef = 80D9D04929FC0D9000FE3400 /* NSMutableArray+NullableObjects.m */; }; 80D9D04B29FC118900FE3400 /* NSMutableArray+NullableObjects.m in Sources */ = {isa = PBXBuildFile; fileRef = 80D9D04929FC0D9000FE3400 /* NSMutableArray+NullableObjects.m */; }; + 80DB57922AF8B59B00C728FF /* RegisterDomainCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80DB57912AF8B59B00C728FF /* RegisterDomainCoordinator.swift */; }; + 80DB57932AF8B59B00C728FF /* RegisterDomainCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80DB57912AF8B59B00C728FF /* RegisterDomainCoordinator.swift */; }; + 80DB57942AF8D04600C728FF /* DomainPurchaseChoicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80348F322AF880820045CCD3 /* DomainPurchaseChoicesView.swift */; }; + 80DB57982AF99E0900C728FF /* BlogListConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80DB57972AF99E0900C728FF /* BlogListConfiguration.swift */; }; + 80DB57992AF99E0900C728FF /* BlogListConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80DB57972AF99E0900C728FF /* BlogListConfiguration.swift */; }; 80EF671F27F135EB0063B138 /* WhatIsNewViewAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80EF671E27F135EB0063B138 /* WhatIsNewViewAppearance.swift */; }; 80EF672027F135EB0063B138 /* WhatIsNewViewAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80EF671E27F135EB0063B138 /* WhatIsNewViewAppearance.swift */; }; 80EF672227F160720063B138 /* DashboardCustomAnnouncementCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80EF672127F160720063B138 /* DashboardCustomAnnouncementCell.swift */; }; @@ -2061,6 +2107,7 @@ 8332DD2429259AE300802F7D /* DataMigrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8332DD2329259AE300802F7D /* DataMigrator.swift */; }; 8332DD2529259AE300802F7D /* DataMigrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8332DD2329259AE300802F7D /* DataMigrator.swift */; }; 8332DD2829259BEB00802F7D /* DataMigratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8332DD2729259BEB00802F7D /* DataMigratorTests.swift */; }; + 833441C82B1AA9DF00B1FD44 /* SOTWCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6AFE462B1A351F00F76520 /* SOTWCardView.swift */; }; 834A49D22A0C23A90042ED3D /* TemplatePageTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 834A49D12A0C23A90042ED3D /* TemplatePageTableViewCell.swift */; }; 834A49D32A0C23A90042ED3D /* TemplatePageTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 834A49D12A0C23A90042ED3D /* TemplatePageTableViewCell.swift */; }; 834CE7341256D0DE0046A4A3 /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 834CE7331256D0DE0046A4A3 /* CFNetwork.framework */; }; @@ -2164,8 +2211,6 @@ 8B260D7E2444FC9D0010F756 /* PostVisibilitySelectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B260D7D2444FC9D0010F756 /* PostVisibilitySelectorViewController.swift */; }; 8B2D4F5327ECE089009B085C /* dashboard-200-without-posts.json in Resources */ = {isa = PBXBuildFile; fileRef = 8B2D4F5227ECE089009B085C /* dashboard-200-without-posts.json */; }; 8B2D4F5527ECE376009B085C /* BlogDashboardPostsParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B2D4F5427ECE376009B085C /* BlogDashboardPostsParserTests.swift */; }; - 8B33BC9527A0C14C00DB5985 /* BlogDetailsViewController+QuickActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B33BC9427A0C14C00DB5985 /* BlogDetailsViewController+QuickActions.swift */; }; - 8B33BC9627A0C14C00DB5985 /* BlogDetailsViewController+QuickActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B33BC9427A0C14C00DB5985 /* BlogDetailsViewController+QuickActions.swift */; }; 8B36256625A60CCA00D7CCE3 /* BackupListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B36256525A60CCA00D7CCE3 /* BackupListViewController.swift */; }; 8B3626F925A665E500D7CCE3 /* UIApplication+mainWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B3626F825A665E500D7CCE3 /* UIApplication+mainWindow.swift */; }; 8B3DECAB2388506400A459C2 /* SentryStartupEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B3DECAA2388506400A459C2 /* SentryStartupEvent.swift */; }; @@ -2200,7 +2245,6 @@ 8B749E9025AF8D2E00023F03 /* JetpackCapabilitiesServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B749E8F25AF8D2E00023F03 /* JetpackCapabilitiesServiceTests.swift */; }; 8B74A9A8268E3C68003511CE /* RewindStatus+multiSite.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B74A9A7268E3C68003511CE /* RewindStatus+multiSite.swift */; }; 8B74A9A9268E3C68003511CE /* RewindStatus+multiSite.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B74A9A7268E3C68003511CE /* RewindStatus+multiSite.swift */; }; - 8B7623382384373E00AB3EE7 /* PageListViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B7623372384373E00AB3EE7 /* PageListViewControllerTests.swift */; }; 8B7C97E325A8BFA2004A3373 /* JetpackActivityLogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B7C97E225A8BFA2004A3373 /* JetpackActivityLogViewController.swift */; }; 8B7F25A724E6EDB4007D82CC /* TopicsCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B7F25A624E6EDB4007D82CC /* TopicsCollectionView.swift */; }; 8B7F51C924EED804008CF5B5 /* ReaderTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B7F51C824EED804008CF5B5 /* ReaderTracker.swift */; }; @@ -2215,7 +2259,6 @@ 8B92D69727CD51FA001F5371 /* DashboardGhostCardCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B92D69527CD51FA001F5371 /* DashboardGhostCardCell.swift */; }; 8B93412F257029F60097D0AC /* FilterChipButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B93412E257029F50097D0AC /* FilterChipButton.swift */; }; 8B93856E22DC08060010BF02 /* PageListSectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B93856D22DC08060010BF02 /* PageListSectionHeaderView.swift */; }; - 8B939F4323832E5D00ACCB0F /* PostListViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B939F4223832E5D00ACCB0F /* PostListViewControllerTests.swift */; }; 8BA125EB27D8F5E4008B779F /* UIView+PinSubviewPriority.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BA125EA27D8F5E4008B779F /* UIView+PinSubviewPriority.swift */; }; 8BA125EC27D8F5E4008B779F /* UIView+PinSubviewPriority.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BA125EA27D8F5E4008B779F /* UIView+PinSubviewPriority.swift */; }; 8BA77BCB2482C52A00E1EBBF /* ReaderCardDiscoverAttributionView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 8BA77BCA2482C52A00E1EBBF /* ReaderCardDiscoverAttributionView.xib */; }; @@ -2292,8 +2335,6 @@ 8F228B22E190FF92D05E53DB /* TimeZoneSearchHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F228848D5DEACE6798CE7E2 /* TimeZoneSearchHeaderView.swift */; }; 8F228F2923045666AE456D2C /* TimeZoneSelectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F2283367263B37B0681F988 /* TimeZoneSelectorViewController.swift */; }; 91138455228373EB00FB02B7 /* GutenbergVideoUploadProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91138454228373EB00FB02B7 /* GutenbergVideoUploadProcessor.swift */; }; - 912347192213484300BD9F97 /* GutenbergViewController+InformativeDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 912347182213484300BD9F97 /* GutenbergViewController+InformativeDialog.swift */; }; - 9123471B221449E200BD9F97 /* GutenbergInformativeDialogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9123471A221449E200BD9F97 /* GutenbergInformativeDialogTests.swift */; }; 912347762216E27200BD9F97 /* GutenbergViewController+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 912347752216E27200BD9F97 /* GutenbergViewController+Localization.swift */; }; 91D8364121946EFB008340B2 /* GutenbergMediaPickerHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91D8364021946EFB008340B2 /* GutenbergMediaPickerHelper.swift */; }; 91DCE84621A6A7F50062F134 /* PostEditor+MoreOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91DCE84521A6A7F50062F134 /* PostEditor+MoreOptions.swift */; }; @@ -2546,10 +2587,8 @@ 98EB126A20D2DC2500D2D5B5 /* NoResultsViewController+Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98EB126920D2DC2500D2D5B5 /* NoResultsViewController+Model.swift */; }; 98ED5963265EBD0000A0B33E /* ReaderDetailLikesListController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98ED5962265EBD0000A0B33E /* ReaderDetailLikesListController.swift */; }; 98ED5964265EBD0000A0B33E /* ReaderDetailLikesListController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98ED5962265EBD0000A0B33E /* ReaderDetailLikesListController.swift */; }; - 98F1B12A2111017A00139493 /* NoResultsStockPhotosConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98F1B1292111017900139493 /* NoResultsStockPhotosConfiguration.swift */; }; 98F537A722496CF300B334F9 /* SiteStatsTableHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98F537A622496CF300B334F9 /* SiteStatsTableHeaderView.swift */; }; 98F537A922496D0D00B334F9 /* SiteStatsTableHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 98F537A822496D0D00B334F9 /* SiteStatsTableHeaderView.xib */; }; - 98F93182239AF64800E4E96E /* ThisWeekWidgetStats.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98F93181239AF64800E4E96E /* ThisWeekWidgetStats.swift */; }; 98FCFC232231DF43006ECDD4 /* PostStatsTitleCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98FCFC212231DF43006ECDD4 /* PostStatsTitleCell.swift */; }; 98FCFC242231DF43006ECDD4 /* PostStatsTitleCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 98FCFC222231DF43006ECDD4 /* PostStatsTitleCell.xib */; }; 98FF6A3E23A30A250025FD72 /* QuickStartNavigationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98FF6A3D23A30A240025FD72 /* QuickStartNavigationSettings.swift */; }; @@ -2563,7 +2602,6 @@ 9A162F2B21C2A21A00FDC035 /* RevisionPreviewTextViewManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A162F2A21C2A21A00FDC035 /* RevisionPreviewTextViewManager.swift */; }; 9A19D441236C7C7500D393E5 /* StatsGhostTopHeaderCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 9A19D440236C7C7500D393E5 /* StatsGhostTopHeaderCell.xib */; }; 9A1A67A622B2AD4E00FF8422 /* CountriesMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A1A67A522B2AD4E00FF8422 /* CountriesMap.swift */; }; - 9A22D9C0214A6BCA00BAEAF2 /* PageListTableViewHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A22D9BF214A6BCA00BAEAF2 /* PageListTableViewHandler.swift */; }; 9A2B28E8219046ED00458F2A /* ShowRevisionsListManger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A2B28E7219046ED00458F2A /* ShowRevisionsListManger.swift */; }; 9A2B28EE2191B50500458F2A /* RevisionsTableViewFooter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A2B28ED2191B50500458F2A /* RevisionsTableViewFooter.swift */; }; 9A2B28F52192121400458F2A /* RevisionOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A2B28F42192121400458F2A /* RevisionOperation.swift */; }; @@ -2667,6 +2705,8 @@ B0B89DC12A1E882F003D5295 /* DomainResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0B89DBF2A1E882F003D5295 /* DomainResultView.swift */; }; B0CD27CF286F8858009500BF /* JetpackBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0CD27CE286F8858009500BF /* JetpackBannerView.swift */; }; B0CD27D0286F8858009500BF /* JetpackBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0CD27CE286F8858009500BF /* JetpackBannerView.swift */; }; + B0DE91B52AF9778200D51A02 /* DomainSetupNoticeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0DE91B42AF9778200D51A02 /* DomainSetupNoticeView.swift */; }; + B0DE91B62AF9778200D51A02 /* DomainSetupNoticeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0DE91B42AF9778200D51A02 /* DomainSetupNoticeView.swift */; }; B0F2EFBF259378E600C7EB6D /* SiteSuggestionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0F2EFBE259378E600C7EB6D /* SiteSuggestionService.swift */; }; B5015C581D4FDBB300C9449E /* NotificationActionsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5015C571D4FDBB300C9449E /* NotificationActionsService.swift */; }; B50248AF1C96FF6200AFBDED /* WPStyleGuide+Share.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50248AE1C96FF6200AFBDED /* WPStyleGuide+Share.swift */; }; @@ -2715,7 +2755,6 @@ B54E1DF11A0A7BAA00807537 /* ReplyTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B54E1DEE1A0A7BAA00807537 /* ReplyTextView.swift */; }; B54E1DF21A0A7BAA00807537 /* ReplyTextView.xib in Resources */ = {isa = PBXBuildFile; fileRef = B54E1DEF1A0A7BAA00807537 /* ReplyTextView.xib */; }; B54E1DF41A0A7BBF00807537 /* NotificationMediaDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = B54E1DF31A0A7BBF00807537 /* NotificationMediaDownloader.swift */; }; - B55086211CC15CCB004EADB4 /* PromptViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B55086201CC15CCB004EADB4 /* PromptViewController.swift */; }; B5552D7E1CD101A600B26DF6 /* NSExtensionContext+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5552D7D1CD101A600B26DF6 /* NSExtensionContext+Extensions.swift */; }; B5552D801CD1028C00B26DF6 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5552D7F1CD1028C00B26DF6 /* String+Extensions.swift */; }; B5552D821CD1061F00B26DF6 /* StringExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5552D811CD1061F00B26DF6 /* StringExtensionsTests.swift */; }; @@ -2880,7 +2919,6 @@ C700FAB3258020DB0090938E /* JetpackScanThreatCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = C700FAB1258020DB0090938E /* JetpackScanThreatCell.xib */; }; C7124E4E2638528F00929318 /* JetpackPrologueViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = C7124E4C2638528F00929318 /* JetpackPrologueViewController.xib */; }; C7124E4F2638528F00929318 /* JetpackPrologueViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7124E4D2638528F00929318 /* JetpackPrologueViewController.swift */; }; - C7124E922638905B00929318 /* StarFieldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7124E912638905B00929318 /* StarFieldView.swift */; }; C7192ECF25E8432D00C3020D /* ReaderTopicsCardCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = C7192ECE25E8432D00C3020D /* ReaderTopicsCardCell.xib */; }; C71AF533281064DE00F9E99E /* OnboardingQuestionsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C71AF532281064DE00F9E99E /* OnboardingQuestionsCoordinator.swift */; }; C71AF534281064DE00F9E99E /* OnboardingQuestionsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C71AF532281064DE00F9E99E /* OnboardingQuestionsCoordinator.swift */; }; @@ -2895,9 +2933,6 @@ C7234A4F2832C47D0045C63F /* QRLoginVerifyAuthorizationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7234A4C2832C47D0045C63F /* QRLoginVerifyAuthorizationViewController.swift */; }; C7234A502832C47D0045C63F /* QRLoginVerifyAuthorizationViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = C7234A4D2832C47D0045C63F /* QRLoginVerifyAuthorizationViewController.xib */; }; C7234A512832C47D0045C63F /* QRLoginVerifyAuthorizationViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = C7234A4D2832C47D0045C63F /* QRLoginVerifyAuthorizationViewController.xib */; }; - C72A4F68264088E4009CA633 /* JetpackNotFoundErrorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72A4F67264088E4009CA633 /* JetpackNotFoundErrorViewModel.swift */; }; - C72A4F7B26408943009CA633 /* JetpackNotWPErrorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72A4F7A26408943009CA633 /* JetpackNotWPErrorViewModel.swift */; }; - C72A4F8E26408C73009CA633 /* JetpackNoSitesErrorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72A4F8D26408C73009CA633 /* JetpackNoSitesErrorViewModel.swift */; }; C72A52CF2649B158009CA633 /* JetpackWindowManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72A52CE2649B157009CA633 /* JetpackWindowManager.swift */; }; C737553E27C80DD500C6E9A1 /* String+CondenseWhitespace.swift in Sources */ = {isa = PBXBuildFile; fileRef = C737553D27C80DD500C6E9A1 /* String+CondenseWhitespace.swift */; }; C737553F27C80DD500C6E9A1 /* String+CondenseWhitespace.swift in Sources */ = {isa = PBXBuildFile; fileRef = C737553D27C80DD500C6E9A1 /* String+CondenseWhitespace.swift */; }; @@ -2965,9 +3000,6 @@ C7F79369260D14C100CE547F /* AppConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA25F9FD2609AA830005E08F /* AppConfiguration.swift */; }; C7F7936A260D14C200CE547F /* AppConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA25F9FD2609AA830005E08F /* AppConfiguration.swift */; }; C7F7ABD6261CED7A00CE547F /* JetpackAuthenticationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7F7ABD5261CED7A00CE547F /* JetpackAuthenticationManager.swift */; }; - C7F7AC75261CF1F300CE547F /* JetpackLoginErrorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7F7AC73261CF1F300CE547F /* JetpackLoginErrorViewController.swift */; }; - C7F7AC76261CF1F300CE547F /* JetpackLoginErrorViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = C7F7AC74261CF1F300CE547F /* JetpackLoginErrorViewController.xib */; }; - C7F7ACBE261E4F0600CE547F /* JetpackErrorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7F7ACBD261E4F0600CE547F /* JetpackErrorViewModel.swift */; }; C7F7BDBD26262A1B00CE547F /* AppDependency.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7F7BDBC26262A1B00CE547F /* AppDependency.swift */; }; C7F7BDD026262A4C00CE547F /* AppDependency.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7F7BDCF26262A4C00CE547F /* AppDependency.swift */; }; C7F7BE0726262B9A00CE547F /* AuthenticationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7F7BE0626262B9900CE547F /* AuthenticationHandler.swift */; }; @@ -2985,29 +3017,22 @@ C81CCD7C243BF7A600A83E27 /* TenorPageable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C81CCD72243BF7A500A83E27 /* TenorPageable.swift */; }; C81CCD7D243BF7A600A83E27 /* TenorDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = C81CCD73243BF7A500A83E27 /* TenorDataSource.swift */; }; C81CCD7E243BF7A600A83E27 /* TenorMedia.swift in Sources */ = {isa = PBXBuildFile; fileRef = C81CCD74243BF7A500A83E27 /* TenorMedia.swift */; }; - C81CCD7F243BF7A600A83E27 /* TenorMediaGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C81CCD75243BF7A500A83E27 /* TenorMediaGroup.swift */; }; - C81CCD80243BF7A600A83E27 /* TenorPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = C81CCD76243BF7A600A83E27 /* TenorPicker.swift */; }; C81CCD81243BF7A600A83E27 /* TenorService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C81CCD77243BF7A600A83E27 /* TenorService.swift */; }; C81CCD82243BF7A600A83E27 /* TenorResultsPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C81CCD78243BF7A600A83E27 /* TenorResultsPage.swift */; }; C81CCD83243BF7A600A83E27 /* TenorDataLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C81CCD79243BF7A600A83E27 /* TenorDataLoader.swift */; }; - C81CCD84243BF7A600A83E27 /* NoResultsTenorConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C81CCD7A243BF7A600A83E27 /* NoResultsTenorConfiguration.swift */; }; C81CCD86243C00E000A83E27 /* TenorPageableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C81CCD85243C00E000A83E27 /* TenorPageableTests.swift */; }; - C856748F243EF177001A995E /* GutenbergTenorMediaPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = C856748E243EF177001A995E /* GutenbergTenorMediaPicker.swift */; }; + C856748F243EF177001A995E /* GutenbergExternalMeidaPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = C856748E243EF177001A995E /* GutenbergExternalMeidaPicker.swift */; }; C8567492243F3751001A995E /* tenor-search-response.json in Resources */ = {isa = PBXBuildFile; fileRef = C8567491243F3751001A995E /* tenor-search-response.json */; }; C8567494243F388F001A995E /* tenor-invalid-search-reponse.json in Resources */ = {isa = PBXBuildFile; fileRef = C8567493243F388F001A995E /* tenor-invalid-search-reponse.json */; }; C8567496243F3D37001A995E /* TenorResultsPageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8567495243F3D37001A995E /* TenorResultsPageTests.swift */; }; C8567498243F41CA001A995E /* MockTenorService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8567497243F41CA001A995E /* MockTenorService.swift */; }; C856749A243F4292001A995E /* TenorMockDataHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8567499243F4292001A995E /* TenorMockDataHelper.swift */; }; C94C0B1B25DCFA0100F2F69B /* FilterableCategoriesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C94C0B1A25DCFA0100F2F69B /* FilterableCategoriesViewController.swift */; }; - C995C22329D306E100ACEF43 /* URL+WidgetSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = C995C22129D306DD00ACEF43 /* URL+WidgetSource.swift */; }; - C995C22429D30A9900ACEF43 /* URL+WidgetSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = C995C22129D306DD00ACEF43 /* URL+WidgetSource.swift */; }; - C995C22629D30AB000ACEF43 /* WidgetUrlSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C995C22529D30AB000ACEF43 /* WidgetUrlSourceTests.swift */; }; C99B08FC26081AD600CA71EB /* TemplatePreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C99B08FB26081AD600CA71EB /* TemplatePreviewViewController.swift */; }; C9B4778729C85949008CBF49 /* LockScreenStatsWidgetEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B4778329C85949008CBF49 /* LockScreenStatsWidgetEntry.swift */; }; C9B477A929CC13CB008CBF49 /* LockScreenSiteListProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B477A729CC13C6008CBF49 /* LockScreenSiteListProvider.swift */; }; C9B477AD29CC15D9008CBF49 /* WidgetDataReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B477AB29CC15D9008CBF49 /* WidgetDataReader.swift */; }; C9B477AE29CC35A0008CBF49 /* WidgetDataReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B477AB29CC15D9008CBF49 /* WidgetDataReader.swift */; }; - C9B477B029CC35C5008CBF49 /* WidgetDataReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B477AF29CC35C5008CBF49 /* WidgetDataReaderTests.swift */; }; C9B477B229CC4949008CBF49 /* HomeWidgetDataFileReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B477B129CC4949008CBF49 /* HomeWidgetDataFileReader.swift */; }; C9B477B429CC4949008CBF49 /* HomeWidgetDataFileReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B477B129CC4949008CBF49 /* HomeWidgetDataFileReader.swift */; }; C9B477B729CD2EF7008CBF49 /* LockScreenUnconfiguredView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B477B529CD2EF7008CBF49 /* LockScreenUnconfiguredView.swift */; }; @@ -3044,9 +3069,6 @@ D0E2AA7C4D4CB1679173958E /* Pods_WordPressShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 213A62FF811EBDB969FA7669 /* Pods_WordPressShareExtension.framework */; }; D8071631203DA23700B32FD9 /* Accessible.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8071630203DA23700B32FD9 /* Accessible.swift */; }; D809E686203F0215001AA0DE /* OldReaderPostCardCellTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D809E685203F0215001AA0DE /* OldReaderPostCardCellTests.swift */; }; - D80BC79C207464D200614A59 /* MediaLibraryMediaPickingCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D80BC79B207464D200614A59 /* MediaLibraryMediaPickingCoordinator.swift */; }; - D80BC79E20746B4100614A59 /* MediaPickingContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D80BC79D20746B4100614A59 /* MediaPickingContext.swift */; }; - D80BC7A22074739400614A59 /* MediaLibraryStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D80BC7A12074739300614A59 /* MediaLibraryStrings.swift */; }; D81322B32050F9110067714D /* NotificationName+Names.swift in Sources */ = {isa = PBXBuildFile; fileRef = D81322B22050F9110067714D /* NotificationName+Names.swift */; }; D813D67F21AA8BBF0055CCA1 /* ShadowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D813D67E21AA8BBF0055CCA1 /* ShadowView.swift */; }; D8160442209C1B0F00ABAFFA /* ReaderSaveForLaterAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8160441209C1B0F00ABAFFA /* ReaderSaveForLaterAction.swift */; }; @@ -3085,7 +3107,6 @@ D821C819210037F8002ED995 /* activity-log-activity-content.json in Resources */ = {isa = PBXBuildFile; fileRef = D821C818210037F8002ED995 /* activity-log-activity-content.json */; }; D821C81B21003AE9002ED995 /* FormattableContentGroupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D821C81A21003AE9002ED995 /* FormattableContentGroupTests.swift */; }; D82253DC2199411F0014D0E2 /* SiteAddressService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D82253DB2199411F0014D0E2 /* SiteAddressService.swift */; }; - D82253DF2199418B0014D0E2 /* WebAddressWizardContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D82253DD2199418B0014D0E2 /* WebAddressWizardContent.swift */; }; D82253E5219956540014D0E2 /* AddressTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D82253E3219956540014D0E2 /* AddressTableViewCell.swift */; }; D8225409219AB2030014D0E2 /* SiteInformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8225407219AB0520014D0E2 /* SiteInformation.swift */; }; D826D67F211D21C700A5D8FE /* NullMockUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = D826D67E211D21C700A5D8FE /* NullMockUserDefaults.swift */; }; @@ -3134,14 +3155,11 @@ D88106FA20C0CFEE001D2F00 /* ReaderSaveForLaterRemovedPosts.swift in Sources */ = {isa = PBXBuildFile; fileRef = D88106F920C0CFEE001D2F00 /* ReaderSaveForLaterRemovedPosts.swift */; }; D88A6492208D7A0A008AE9BC /* MockStockPhotosService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D88A6491208D7A0A008AE9BC /* MockStockPhotosService.swift */; }; D88A6494208D7AD0008AE9BC /* DefaultStockPhotosService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D88A6493208D7AD0008AE9BC /* DefaultStockPhotosService.swift */; }; - D88A6496208D7B0B008AE9BC /* NullStockPhotosService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D88A6495208D7B0B008AE9BC /* NullStockPhotosService.swift */; }; - D88A649C208D7D81008AE9BC /* StockPhotosDataSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D88A649B208D7D81008AE9BC /* StockPhotosDataSourceTests.swift */; }; D88A649E208D82D2008AE9BC /* XCTestCase+Wait.swift in Sources */ = {isa = PBXBuildFile; fileRef = D88A649D208D82D2008AE9BC /* XCTestCase+Wait.swift */; }; - D88A64A0208D8B7D008AE9BC /* StockPhotosMediaGroupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D88A649F208D8B7D008AE9BC /* StockPhotosMediaGroupTests.swift */; }; D88A64A2208D8F05008AE9BC /* StockPhotosMediaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D88A64A1208D8F05008AE9BC /* StockPhotosMediaTests.swift */; }; D88A64A4208D8FB6008AE9BC /* stock-photos-search-response.json in Resources */ = {isa = PBXBuildFile; fileRef = D88A64A3208D8FB6008AE9BC /* stock-photos-search-response.json */; }; D88A64A6208D92B1008AE9BC /* stock-photos-media.json in Resources */ = {isa = PBXBuildFile; fileRef = D88A64A5208D92B1008AE9BC /* stock-photos-media.json */; }; - D88A64A8208D9733008AE9BC /* ThumbnailCollectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D88A64A7208D9733008AE9BC /* ThumbnailCollectionTests.swift */; }; + D88A64A8208D9733008AE9BC /* StockPhotosThumbnailCollectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D88A64A7208D9733008AE9BC /* StockPhotosThumbnailCollectionTests.swift */; }; D88A64AA208D974D008AE9BC /* thumbnail-collection.json in Resources */ = {isa = PBXBuildFile; fileRef = D88A64A9208D974D008AE9BC /* thumbnail-collection.json */; }; D88A64AC208D9B09008AE9BC /* StockPhotosPageableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D88A64AB208D9B09008AE9BC /* StockPhotosPageableTests.swift */; }; D88A64AE208D9CF5008AE9BC /* stock-photos-pageable.json in Resources */ = {isa = PBXBuildFile; fileRef = D88A64AD208D9CF5008AE9BC /* stock-photos-pageable.json */; }; @@ -3149,11 +3167,8 @@ D8A3A5AA2069E53900992576 /* AztecMediaPickingCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A3A5A92069E53900992576 /* AztecMediaPickingCoordinator.swift */; }; D8A3A5AC2069FE5B00992576 /* StockPhotosStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A3A5AB2069FE5B00992576 /* StockPhotosStrings.swift */; }; D8A3A5AF206A442800992576 /* StockPhotosDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A3A5AE206A442800992576 /* StockPhotosDataSource.swift */; }; - D8A3A5B1206A49A100992576 /* StockPhotosMediaGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A3A5B0206A49A100992576 /* StockPhotosMediaGroup.swift */; }; D8A3A5B3206A49BF00992576 /* StockPhotosMedia.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A3A5B2206A49BF00992576 /* StockPhotosMedia.swift */; }; - D8A3A5B5206A4C7800992576 /* StockPhotosPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A3A5B4206A4C7800992576 /* StockPhotosPicker.swift */; }; D8A468E02181C6450094B82F /* site-segment.json in Resources */ = {isa = PBXBuildFile; fileRef = D8A468DF2181C6450094B82F /* site-segment.json */; }; - D8A468E521828D940094B82F /* SiteVerticalsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A468E421828D940094B82F /* SiteVerticalsService.swift */; }; D8B6BEB7203E11F2007C8A19 /* Bundle+LoadFromNib.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B6BEB6203E11F2007C8A19 /* Bundle+LoadFromNib.swift */; }; D8B9B58F204F4EA1003C6042 /* NetworkAware.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B9B58E204F4EA1003C6042 /* NetworkAware.swift */; }; D8BA274D20FDEA2E007A5C77 /* NotificationTextContentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8BA274C20FDEA2E007A5C77 /* NotificationTextContentTests.swift */; }; @@ -3404,15 +3419,12 @@ E6805D301DCD399600168E4F /* WPRichTextEmbed.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6805D2D1DCD399600168E4F /* WPRichTextEmbed.swift */; }; E6805D311DCD399600168E4F /* WPRichTextImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6805D2E1DCD399600168E4F /* WPRichTextImage.swift */; }; E6805D321DCD399600168E4F /* WPRichTextMediaAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6805D2F1DCD399600168E4F /* WPRichTextMediaAttachment.swift */; }; - E684383E221F535900752258 /* LoadMoreCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E684383D221F535900752258 /* LoadMoreCounter.swift */; }; - E6843840221F5A2200752258 /* PostListExcessiveLoadMoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E684383F221F5A2200752258 /* PostListExcessiveLoadMoreTests.swift */; }; E68580F61E0D91470090EE63 /* WPHorizontalRuleAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = E68580F51E0D91470090EE63 /* WPHorizontalRuleAttachment.swift */; }; E690F6EF25E05D180015A777 /* InviteLinks+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = E690F6ED25E05D170015A777 /* InviteLinks+CoreDataClass.swift */; }; E690F6F025E05D180015A777 /* InviteLinks+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = E690F6EE25E05D180015A777 /* InviteLinks+CoreDataProperties.swift */; }; E69551F61B8B6AE200CB8E4F /* ReaderStreamViewController+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = E69551F51B8B6AE200CB8E4F /* ReaderStreamViewController+Helper.swift */; }; E696541F25A8ED7C000E2A52 /* UIApplication+mainWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B3626F825A665E500D7CCE3 /* UIApplication+mainWindow.swift */; }; E696542025A8ED7C000E2A52 /* UIApplication+mainWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B3626F825A665E500D7CCE3 /* UIApplication+mainWindow.swift */; }; - E69BA1981BB5D7D300078740 /* WPStyleGuide+ReadableMargins.m in Sources */ = {isa = PBXBuildFile; fileRef = E69BA1971BB5D7D300078740 /* WPStyleGuide+ReadableMargins.m */; }; E6A215901D1065F200DE5270 /* AbstractPostTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6A2158F1D1065F200DE5270 /* AbstractPostTest.swift */; }; E6A3384C1BB08E3F00371587 /* ReaderGapMarker.m in Sources */ = {isa = PBXBuildFile; fileRef = E6A3384B1BB08E3F00371587 /* ReaderGapMarker.m */; }; E6A3384E1BB0A50900371587 /* ReaderGapMarkerCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = E6A3384D1BB0A50900371587 /* ReaderGapMarkerCell.xib */; }; @@ -3547,9 +3559,24 @@ F1F083F6241FFE930056D3B1 /* AtomicAuthenticationServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1F083F5241FFE930056D3B1 /* AtomicAuthenticationServiceTests.swift */; }; F4026B1D2A1BC88A00CC7781 /* DashboardDomainRegistrationCardCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4026B1C2A1BC88A00CC7781 /* DashboardDomainRegistrationCardCell.swift */; }; F4026B1E2A1BC88A00CC7781 /* DashboardDomainRegistrationCardCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4026B1C2A1BC88A00CC7781 /* DashboardDomainRegistrationCardCell.swift */; }; + F413F77A2B2A183E00A64A94 /* BlogDashboardDynamicCardCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F413F7792B2A183E00A64A94 /* BlogDashboardDynamicCardCell.swift */; }; + F413F77B2B2A183E00A64A94 /* BlogDashboardDynamicCardCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F413F7792B2A183E00A64A94 /* BlogDashboardDynamicCardCell.swift */; }; + F413F7882B2B253A00A64A94 /* DashboardCard+Personalization.swift in Sources */ = {isa = PBXBuildFile; fileRef = F413F7872B2B253A00A64A94 /* DashboardCard+Personalization.swift */; }; + F413F7892B2B253A00A64A94 /* DashboardCard+Personalization.swift in Sources */ = {isa = PBXBuildFile; fileRef = F413F7872B2B253A00A64A94 /* DashboardCard+Personalization.swift */; }; + F4141EE42AE7152F000D2AAE /* AllDomainsListViewController+Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4141EE22AE7152F000D2AAE /* AllDomainsListViewController+Strings.swift */; }; + F4141EE62AE71AF0000D2AAE /* AllDomainsListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4141EE52AE71AF0000D2AAE /* AllDomainsListViewModel.swift */; }; + F4141EE82AE72DC4000D2AAE /* AllDomainsListTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4141EE72AE72DC4000D2AAE /* AllDomainsListTableViewCell.swift */; }; + F4141EEA2AE74ADA000D2AAE /* AllDomainsListActivityIndicatorTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4141EE92AE74ADA000D2AAE /* AllDomainsListActivityIndicatorTableViewCell.swift */; }; + F4141EEC2AE945C7000D2AAE /* AllDomainsListItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4141EEB2AE945C7000D2AAE /* AllDomainsListItemViewModel.swift */; }; F41BDD73290BBDCA00B7F2B0 /* MigrationActionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F41BDD72290BBDCA00B7F2B0 /* MigrationActionsView.swift */; }; F41BDD792910AFCA00B7F2B0 /* MigrationFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F41BDD782910AFCA00B7F2B0 /* MigrationFlowCoordinator.swift */; }; F41BDD7B29114E2400B7F2B0 /* MigrationStep.swift in Sources */ = {isa = PBXBuildFile; fileRef = F41BDD7A29114E2400B7F2B0 /* MigrationStep.swift */; }; + F41D98D72B389735004EC050 /* DashboardDynamicCardAnalyticsEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = F41D98D62B389735004EC050 /* DashboardDynamicCardAnalyticsEvent.swift */; }; + F41D98D92B3901F5004EC050 /* DashboardDynamicCardAnalyticsEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = F41D98D62B389735004EC050 /* DashboardDynamicCardAnalyticsEvent.swift */; }; + F41D98E12B39C5CE004EC050 /* BlogDashboardDynamicCardCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F41D98E02B39C5CE004EC050 /* BlogDashboardDynamicCardCoordinatorTests.swift */; }; + F41D98E42B39CAA5004EC050 /* BlogDashboardDynamicCardCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F41D98E22B39C9E7004EC050 /* BlogDashboardDynamicCardCoordinator.swift */; }; + F41D98E52B39CAAA004EC050 /* BlogDashboardDynamicCardCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F41D98E22B39C9E7004EC050 /* BlogDashboardDynamicCardCoordinator.swift */; }; + F41D98E82B39E14F004EC050 /* DashboardDynamicCardAnalyticsEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F41D98E72B39E14F004EC050 /* DashboardDynamicCardAnalyticsEventTests.swift */; }; F41E32FE287B47A500F89082 /* SuggestionsListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F41E32FD287B47A500F89082 /* SuggestionsListViewModel.swift */; }; F41E32FF287B47A500F89082 /* SuggestionsListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F41E32FD287B47A500F89082 /* SuggestionsListViewModel.swift */; }; F41E3301287B5FE500F89082 /* SuggestionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F41E3300287B5FE500F89082 /* SuggestionViewModel.swift */; }; @@ -3613,6 +3640,11 @@ F44FB6CB287895AF0001E3CE /* SuggestionsListViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F44FB6CA287895AF0001E3CE /* SuggestionsListViewModelTests.swift */; }; F44FB6D12878A1020001E3CE /* user-suggestions.json in Resources */ = {isa = PBXBuildFile; fileRef = F44FB6D02878A1020001E3CE /* user-suggestions.json */; }; F4552086299D147B00D9F6A8 /* BlockedSite.swift in Sources */ = {isa = PBXBuildFile; fileRef = F48D44B5298992C30051EAA6 /* BlockedSite.swift */; }; + F46546292AED89790017E3D1 /* AllDomainsListEmptyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F46546282AED89790017E3D1 /* AllDomainsListEmptyView.swift */; }; + F465462D2AEF22070017E3D1 /* AllDomainsListViewModel+Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = F465462C2AEF22070017E3D1 /* AllDomainsListViewModel+Strings.swift */; }; + F46546312AF2F8D30017E3D1 /* DomainsStateViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F46546302AF2F8D20017E3D1 /* DomainsStateViewModel.swift */; }; + F46546332AF54DCD0017E3D1 /* AllDomainsListItemViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F46546322AF54DCD0017E3D1 /* AllDomainsListItemViewModelTests.swift */; }; + F46546352AF550A20017E3D1 /* AllDomainsListItem+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = F46546342AF550A20017E3D1 /* AllDomainsListItem+Helpers.swift */; }; F465976E28E4669200D5F49A /* cool-green-icon-app-76@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = F465976928E4669200D5F49A /* cool-green-icon-app-76@2x.png */; }; F465976F28E4669200D5F49A /* cool-green-icon-app-76.png in Resources */ = {isa = PBXBuildFile; fileRef = F465976A28E4669200D5F49A /* cool-green-icon-app-76.png */; }; F465977028E4669200D5F49A /* cool-green-icon-app-60@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = F465976B28E4669200D5F49A /* cool-green-icon-app-60@3x.png */; }; @@ -3684,6 +3716,7 @@ F465980B28E66A5B00D5F49A /* white-on-blue-icon-app-60@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = F465980628E66A5A00D5F49A /* white-on-blue-icon-app-60@2x.png */; }; F465980C28E66A5B00D5F49A /* white-on-blue-icon-app-83.5@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = F465980728E66A5B00D5F49A /* white-on-blue-icon-app-83.5@2x.png */; }; F478B152292FC1BC00AA8645 /* MigrationAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = F478B151292FC1BC00AA8645 /* MigrationAppearance.swift */; }; + F479995F2AFD241E0023F4FB /* RegisterDomainTransferFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F479995D2AFD241E0023F4FB /* RegisterDomainTransferFooterView.swift */; }; F47E154A29E84A9300B6E426 /* SiteCreationPurchasingWebFlowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F47E154929E84A9300B6E426 /* SiteCreationPurchasingWebFlowController.swift */; }; F47E154B29E84A9300B6E426 /* SiteCreationPurchasingWebFlowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F47E154929E84A9300B6E426 /* SiteCreationPurchasingWebFlowController.swift */; }; F484D4EA2A32B51C0050BE15 /* RootViewPresenter+AppSettingsNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F484D4E92A32B51C0050BE15 /* RootViewPresenter+AppSettingsNavigation.swift */; }; @@ -3695,6 +3728,10 @@ F48D44BB2989A9070051EAA6 /* ReaderSiteService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F48D44B92989A58C0051EAA6 /* ReaderSiteService.swift */; }; F48D44BC2989AA8A0051EAA6 /* ReaderSiteService.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D44EB371986D8BA008B7175 /* ReaderSiteService.m */; }; F48D44BD2989AA8C0051EAA6 /* ReaderSiteService.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D44EB371986D8BA008B7175 /* ReaderSiteService.m */; }; + F48EBF8A2B2F94DD004CD561 /* BlogDashboardAnalyticPropertiesProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = F48EBF892B2F94DD004CD561 /* BlogDashboardAnalyticPropertiesProviding.swift */; }; + F48EBF8B2B2F94DD004CD561 /* BlogDashboardAnalyticPropertiesProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = F48EBF892B2F94DD004CD561 /* BlogDashboardAnalyticPropertiesProviding.swift */; }; + F48EBF942B333550004CD561 /* dashboard-200-with-only-one-dynamic-card.json in Resources */ = {isa = PBXBuildFile; fileRef = F48EBF912B333111004CD561 /* dashboard-200-with-only-one-dynamic-card.json */; }; + F48EBF952B333B31004CD561 /* dashboard-200-with-multiple-dynamic-cards.json in Resources */ = {isa = PBXBuildFile; fileRef = F48EBF8C2B3262D5004CD561 /* dashboard-200-with-multiple-dynamic-cards.json */; }; F49B99FF2937C9B4000CEFCE /* MigrationEmailService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F49B99FE2937C9B4000CEFCE /* MigrationEmailService.swift */; }; F49B9A0029393049000CEFCE /* MigrationAppDetection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33A5ADB2935848F00961E3A /* MigrationAppDetection.swift */; }; F49B9A06293A21BF000CEFCE /* MigrationAnalyticsTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = F49B9A05293A21BF000CEFCE /* MigrationAnalyticsTracker.swift */; }; @@ -3703,6 +3740,9 @@ F49B9A0A293A3249000CEFCE /* MigrationAnalyticsTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = F49B9A05293A21BF000CEFCE /* MigrationAnalyticsTracker.swift */; }; F49D7BEB29DF329500CB93A5 /* UIPopoverPresentationController+PopoverAnchor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F49D7BEA29DF329500CB93A5 /* UIPopoverPresentationController+PopoverAnchor.swift */; }; F49D7BEC29DF329500CB93A5 /* UIPopoverPresentationController+PopoverAnchor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F49D7BEA29DF329500CB93A5 /* UIPopoverPresentationController+PopoverAnchor.swift */; }; + F4AA1E5E2AF66D3300EBA201 /* AllDomainsListItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4141EEB2AE945C7000D2AAE /* AllDomainsListItemViewModel.swift */; }; + F4B0F4832ADED9B5003ABC61 /* DomainsService+AllDomains.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4B0F4822ADED9B5003ABC61 /* DomainsService+AllDomains.swift */; }; + F4B0F4842ADED9B5003ABC61 /* DomainsService+AllDomains.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4B0F4822ADED9B5003ABC61 /* DomainsService+AllDomains.swift */; }; F4BECD1B288EE5220078391A /* SuggestionsViewModelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4BECD1A288EE5220078391A /* SuggestionsViewModelType.swift */; }; F4BECD1C288EE5220078391A /* SuggestionsViewModelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4BECD1A288EE5220078391A /* SuggestionsViewModelType.swift */; }; F4C1FC632A44831300AD7CB0 /* PrivacySettingsAnalyticsTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C1FC622A44831300AD7CB0 /* PrivacySettingsAnalyticsTracker.swift */; }; @@ -3714,6 +3754,9 @@ F4CBE3D7292597E3004FFBB6 /* SupportTableViewControllerConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4CBE3D5292597E3004FFBB6 /* SupportTableViewControllerConfiguration.swift */; }; F4CBE3D929265BC8004FFBB6 /* LogOutActionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4CBE3D829265BC8004FFBB6 /* LogOutActionHandler.swift */; }; F4CBE3DA29265BC8004FFBB6 /* LogOutActionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4CBE3D829265BC8004FFBB6 /* LogOutActionHandler.swift */; }; + F4D140202AFD9B9700961797 /* TransferDomainsWebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4D1401F2AFD9B9700961797 /* TransferDomainsWebViewController.swift */; }; + F4D140212AFD9D5300961797 /* TransferDomainsWebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4D1401F2AFD9B9700961797 /* TransferDomainsWebViewController.swift */; }; + F4D140222AFE794300961797 /* RegisterDomainTransferFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F479995D2AFD241E0023F4FB /* RegisterDomainTransferFooterView.swift */; }; F4D36AD5298498E600E6B84C /* ReaderPostBlockingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4D36AD4298498E600E6B84C /* ReaderPostBlockingController.swift */; }; F4D36AD6298498E600E6B84C /* ReaderPostBlockingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4D36AD4298498E600E6B84C /* ReaderPostBlockingController.swift */; }; F4D7FD6C2A57030E00642E06 /* CompliancePopoverViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4D7FD6B2A57030E00642E06 /* CompliancePopoverViewControllerTests.swift */; }; @@ -3740,6 +3783,10 @@ F4EDAA4D29A516EA00622D3D /* ReaderPostService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4EDAA4B29A516E900622D3D /* ReaderPostService.swift */; }; F4EDAA5129A795C600622D3D /* BlockedAuthor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F42A1D9629928B360059CC70 /* BlockedAuthor.swift */; }; F4EF4BAB291D3D4700147B61 /* SiteIconTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4EF4BAA291D3D4700147B61 /* SiteIconTests.swift */; }; + F4F7B2512AF8EF2C00207282 /* DomainDetailsWebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4F7B2502AF8EBDB00207282 /* DomainDetailsWebViewController.swift */; }; + F4F7B2532AFA585700207282 /* DomainDetailsWebViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4F7B2522AFA585700207282 /* DomainDetailsWebViewControllerTests.swift */; }; + F4F7B2542AFA5D8600207282 /* AllDomainsListCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08240C2D2AB8A2DD00E7AEA8 /* AllDomainsListCardView.swift */; }; + F4F7B2552AFA60DA00207282 /* DomainDetailsWebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4F7B2502AF8EBDB00207282 /* DomainDetailsWebViewController.swift */; }; F4F9D5EA2909622E00502576 /* MigrationWelcomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4F9D5E92909622E00502576 /* MigrationWelcomeViewController.swift */; }; F4F9D5EC29096CF500502576 /* MigrationHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4F9D5EB29096CF500502576 /* MigrationHeaderView.swift */; }; F4F9D5F2290993D400502576 /* MigrationWelcomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4F9D5F1290993D400502576 /* MigrationWelcomeViewModel.swift */; }; @@ -3758,7 +3805,6 @@ F52CACCC24512EA700661380 /* EmptyActionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F52CACCB24512EA700661380 /* EmptyActionView.swift */; }; F532AD61253B81320013B42E /* StoriesIntroDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = F532AD60253B81320013B42E /* StoriesIntroDataSource.swift */; }; F532AE1C253E55D40013B42E /* CreateButtonActionSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = F532AE1B253E55D40013B42E /* CreateButtonActionSheet.swift */; }; - F53FF3A823EA723D001AD596 /* ActionRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = F53FF3A723EA723D001AD596 /* ActionRow.swift */; }; F53FF3AA23EA725C001AD596 /* SiteIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F53FF3A923EA725C001AD596 /* SiteIconView.swift */; }; F543AF5723A84E4D0022F595 /* PublishSettingsControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F543AF5623A84E4D0022F595 /* PublishSettingsControllerTests.swift */; }; F551E7F523F6EA3100751212 /* FloatingActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = F551E7F423F6EA3100751212 /* FloatingActionButton.swift */; }; @@ -3772,7 +3818,6 @@ F580C3C123D22E2D0038E243 /* PreviewDeviceLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F580C3C023D22E2D0038E243 /* PreviewDeviceLabel.swift */; }; F580C3CB23D8F9B40038E243 /* AbstractPost+Dates.swift in Sources */ = {isa = PBXBuildFile; fileRef = F580C3CA23D8F9B40038E243 /* AbstractPost+Dates.swift */; }; F582060223A85495005159A9 /* SiteDateFormatters.swift in Sources */ = {isa = PBXBuildFile; fileRef = F582060123A85495005159A9 /* SiteDateFormatters.swift */; }; - F5844B6B235EAF3D007C6557 /* PartScreenPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5844B6A235EAF3D007C6557 /* PartScreenPresentationController.swift */; }; F59AAC10235E430F00385EE6 /* ChosenValueRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = F59AAC0F235E430E00385EE6 /* ChosenValueRow.swift */; }; F59AAC16235EA46D00385EE6 /* LightNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F59AAC15235EA46D00385EE6 /* LightNavigationController.swift */; }; F5A34A9925DEF47D00C9654B /* WPMediaPicker+MediaPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5A34A9825DEF47D00C9654B /* WPMediaPicker+MediaPicker.swift */; }; @@ -3831,6 +3876,11 @@ FA00863D24EB68B100C863F2 /* FollowCommentsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA00863C24EB68B100C863F2 /* FollowCommentsService.swift */; }; FA111E382A2F38FC00896FCE /* BlazeCampaignsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA111E372A2F38FC00896FCE /* BlazeCampaignsViewController.swift */; }; FA111E392A2F474600896FCE /* BlazeCampaignsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA111E372A2F38FC00896FCE /* BlazeCampaignsViewController.swift */; }; + FA141F272AEC1D9E00C9A653 /* PageMenuViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA141F262AEC1D9E00C9A653 /* PageMenuViewModel.swift */; }; + FA141F282AEC1D9E00C9A653 /* PageMenuViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA141F262AEC1D9E00C9A653 /* PageMenuViewModel.swift */; }; + FA141F2A2AEC23E300C9A653 /* PageListViewController+Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA141F292AEC23E300C9A653 /* PageListViewController+Menu.swift */; }; + FA141F2B2AEC23E300C9A653 /* PageListViewController+Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA141F292AEC23E300C9A653 /* PageListViewController+Menu.swift */; }; + FA141F322AF139A200C9A653 /* PageMenuViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA141F312AF139A200C9A653 /* PageMenuViewModelTests.swift */; }; FA1A543E25A6E2F60033967D /* RestoreWarningView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA1A543D25A6E2F60033967D /* RestoreWarningView.swift */; }; FA1A544025A6E3080033967D /* RestoreWarningView.xib in Resources */ = {isa = PBXBuildFile; fileRef = FA1A543F25A6E3080033967D /* RestoreWarningView.xib */; }; FA1A55EF25A6F0740033967D /* RestoreStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA1A55EE25A6F0740033967D /* RestoreStatusView.swift */; }; @@ -3929,6 +3979,8 @@ FAA9084D27BD60710093FFA8 /* MySiteViewController+QuickStart.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAA9084B27BD60710093FFA8 /* MySiteViewController+QuickStart.swift */; }; FAADE42626159AFE00BF29FE /* AppConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAADE3F02615996E00BF29FE /* AppConstants.swift */; }; FAADE43A26159B2800BF29FE /* AppConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAADE42726159B1300BF29FE /* AppConstants.swift */; }; + FAAEFAE02B1E29F0004AE802 /* SitePickerViewController+SiteActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAAEFADF2B1E29F0004AE802 /* SitePickerViewController+SiteActions.swift */; }; + FAAEFAE12B1E29F0004AE802 /* SitePickerViewController+SiteActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAAEFADF2B1E29F0004AE802 /* SitePickerViewController+SiteActions.swift */; }; FAB37D4627ED84BC00CA993C /* DashboardStatsNudgeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAB37D4527ED84BC00CA993C /* DashboardStatsNudgeView.swift */; }; FAB37D4727ED84BC00CA993C /* DashboardStatsNudgeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAB37D4527ED84BC00CA993C /* DashboardStatsNudgeView.swift */; }; FAB4F32724EDE12A00F259BA /* FollowCommentsServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAB4F32624EDE12A00F259BA /* FollowCommentsServiceTests.swift */; }; @@ -3967,7 +4019,6 @@ FABB1FC82602FC2C00C8785C /* EpilogueSectionHeaderFooter.xib in Resources */ = {isa = PBXBuildFile; fileRef = 98579BC6203DD86E004086E4 /* EpilogueSectionHeaderFooter.xib */; }; FABB1FC92602FC2C00C8785C /* reader.css in Resources */ = {isa = PBXBuildFile; fileRef = 8B64B4B1247EC3A2009A1229 /* reader.css */; }; FABB1FCC2602FC2C00C8785C /* SiteStatsTableHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 98F537A822496D0D00B334F9 /* SiteStatsTableHeaderView.xib */; }; - FABB1FCD2602FC2C00C8785C /* Posts.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5DBFC8A81A9BE07B00E00DE4 /* Posts.storyboard */; }; FABB1FCE2602FC2C00C8785C /* StatsChildRowsView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 98B11B8A2216536C00B7F2D7 /* StatsChildRowsView.xib */; }; FABB1FD02602FC2C00C8785C /* xhtml1-transitional.dtd in Resources */ = {isa = PBXBuildFile; fileRef = 2FAE97070E33B21600CA8540 /* xhtml1-transitional.dtd */; }; FABB1FD22602FC2C00C8785C /* RestoreStatusView.xib in Resources */ = {isa = PBXBuildFile; fileRef = FA1A55FE25A6F07F0033967D /* RestoreStatusView.xib */; }; @@ -4001,7 +4052,6 @@ FABB1FFE2602FC2C00C8785C /* Notifications.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B558541019631A1000FAF6C3 /* Notifications.storyboard */; }; FABB20022602FC2C00C8785C /* StatsTableFooter.xib in Resources */ = {isa = PBXBuildFile; fileRef = 983DBBA822125DD300753988 /* StatsTableFooter.xib */; }; FABB20052602FC2C00C8785C /* ThemeBrowserSectionHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 820ADD6F1F3A1F88002D7F93 /* ThemeBrowserSectionHeaderView.xib */; }; - FABB20072602FC2C00C8785C /* SitePromptView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 467D3E0B25E4436D00EB9CB0 /* SitePromptView.xib */; }; FABB20082602FC2C00C8785C /* DeleteSite.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 746A6F561E71C691003B67E3 /* DeleteSite.storyboard */; }; FABB200A2602FC2C00C8785C /* Noticons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = F5A34D0C25DF2F7700C9654B /* Noticons.ttf */; }; FABB200B2602FC2C00C8785C /* LoginEpilogue.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B51AD77A2056C31100A6C545 /* LoginEpilogue.storyboard */; }; @@ -4010,7 +4060,6 @@ FABB20112602FC2C00C8785C /* SpaceMono-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = F5A34D0825DF2F7700C9654B /* SpaceMono-Bold.ttf */; }; FABB20142602FC2C00C8785C /* CommentsList.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9835F16D25E492EE002EFF23 /* CommentsList.storyboard */; }; FABB20162602FC2C00C8785C /* TabbedTotalsCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 98563DDC21BF30C40006F5E9 /* TabbedTotalsCell.xib */; }; - FABB20172602FC2C00C8785C /* PageListTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5DFA7EC61AF814E40072023B /* PageListTableViewCell.xib */; }; FABB20182602FC2C00C8785C /* NoteBlockImageTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = B5C66B771ACF073900F68370 /* NoteBlockImageTableViewCell.xib */; }; FABB201B2602FC2C00C8785C /* SignupEpilogue.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B59F34A0207678480069992D /* SignupEpilogue.storyboard */; }; FABB201D2602FC2C00C8785C /* PostStatsTitleCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 98FCFC222231DF43006ECDD4 /* PostStatsTitleCell.xib */; }; @@ -4048,7 +4097,6 @@ FABB204C2602FC2C00C8785C /* StatsGhostTitleCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 9AB36B83236B25D900FAD72A /* StatsGhostTitleCell.xib */; }; FABB204D2602FC2C00C8785C /* NoteBlockButtonTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 176CF3AB25E0079600E1E598 /* NoteBlockButtonTableViewCell.xib */; }; FABB204F2602FC2C00C8785C /* NoteBlockCommentTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = B5C66B751ACF072C00F68370 /* NoteBlockCommentTableViewCell.xib */; }; - FABB20502602FC2C00C8785C /* PostCardCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 57AA8490228715E700D3C2A2 /* PostCardCell.xib */; }; FABB20552602FC2C00C8785C /* ReaderDetailFeaturedImageView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3223393D24FEC2A700BDD4BF /* ReaderDetailFeaturedImageView.xib */; }; FABB20562602FC2C00C8785C /* loader.html in Resources */ = {isa = PBXBuildFile; fileRef = E18165FC14E4428B006CE885 /* loader.html */; }; FABB20582602FC2C00C8785C /* JetpackRestoreHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = FA4F661325946B8500EAA9F5 /* JetpackRestoreHeaderView.xib */; }; @@ -4066,7 +4114,6 @@ FABB206D2602FC2C00C8785C /* NoteBlockUserTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = B5C66B791ACF074600F68370 /* NoteBlockUserTableViewCell.xib */; }; FABB20702602FC2C00C8785C /* ReaderListStreamHeader.xib in Resources */ = {isa = PBXBuildFile; fileRef = E6D2E1621B8AAA340000ED14 /* ReaderListStreamHeader.xib */; }; FABB20722602FC2C00C8785C /* MyProfileHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE1CCB2E2050502B000EE3AC /* MyProfileHeaderView.xib */; }; - FABB20742602FC2C00C8785C /* RestorePageTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5D18FE9E1AFBB17400EFEED0 /* RestorePageTableViewCell.xib */; }; FABB20762602FC2C00C8785C /* WordPressShare.js in Resources */ = {isa = PBXBuildFile; fileRef = E1AFA8C21E8E34230004A323 /* WordPressShare.js */; }; FABB20772602FC2C00C8785C /* world-map.svg in Resources */ = {isa = PBXBuildFile; fileRef = 9A76C32E22AFDA2100F5D819 /* world-map.svg */; }; FABB20782602FC2C00C8785C /* NoteBlockTextTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = B5C66B711ACF071000F68370 /* NoteBlockTextTableViewCell.xib */; }; @@ -4077,14 +4124,12 @@ FABB20862602FC2C00C8785C /* n.caf in Resources */ = {isa = PBXBuildFile; fileRef = 5D69DBC3165428CA00A2D1F7 /* n.caf */; }; FABB20882602FC2C00C8785C /* People.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E1B912801BB00EFD003C25B9 /* People.storyboard */; }; FABB208B2602FC2C00C8785C /* LatestPostSummaryCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 9826AE8121B5C6A700C851FA /* LatestPostSummaryCell.xib */; }; - FABB208D2602FC2C00C8785C /* RestorePostTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5D2FB2821AE98C4600F1D4ED /* RestorePostTableViewCell.xib */; }; FABB208E2602FC2C00C8785C /* Plans.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1724DDCB1C6121D00099D273 /* Plans.storyboard */; }; FABB208F2602FC2C00C8785C /* PluginDirectoryCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 40A2778020191B5E00D078D5 /* PluginDirectoryCollectionViewCell.xib */; }; FABB20902602FC2C00C8785C /* ReaderWelcomeBanner.xib in Resources */ = {isa = PBXBuildFile; fileRef = 8BD8201824BCCE8600FF25FD /* ReaderWelcomeBanner.xib */; }; FABB20952602FC2C00C8785C /* StatsGhostPostingActivityCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 9A9E3FB3230EC4F700909BC4 /* StatsGhostPostingActivityCell.xib */; }; FABB20962602FC2C00C8785C /* ColorPalette.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 435B76292297484200511813 /* ColorPalette.xcassets */; }; FABB20972602FC2C00C8785C /* RestoreCompleteView.xib in Resources */ = {isa = PBXBuildFile; fileRef = FAB800C125AEE3D200D5D54A /* RestoreCompleteView.xib */; }; - FABB20992602FC2C00C8785C /* RegisterDomain.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 402FFB1B218C27C100FF4A0B /* RegisterDomain.storyboard */; }; FABB209A2602FC2C00C8785C /* PostListFooterView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5D732F981AE84E5400CD89E7 /* PostListFooterView.xib */; }; FABB209B2602FC2C00C8785C /* acknowledgements.html in Resources */ = {isa = PBXBuildFile; fileRef = 4D520D4E22972BC9002F5924 /* acknowledgements.html */; }; FABB209C2602FC2C00C8785C /* ReaderDetailViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8BDA5A6E247C308300AB124C /* ReaderDetailViewController.storyboard */; }; @@ -4124,7 +4169,6 @@ FABB20C82602FC2C00C8785C /* ActivityPluginRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E4A773320F800ED001C706D /* ActivityPluginRange.swift */; }; FABB20C92602FC2C00C8785C /* ShareNoticeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7462BFCF2028C49800B552D8 /* ShareNoticeViewModel.swift */; }; FABB20CA2602FC2C00C8785C /* Routes+Reader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17A4A36820EE51870071C2CA /* Routes+Reader.swift */; }; - FABB20CB2602FC2C00C8785C /* WPContentSearchHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0807CB711CE670A800CDBDAC /* WPContentSearchHelper.swift */; }; FABB20CC2602FC2C00C8785C /* SiteStatsViewModel+AsyncBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A73B7142362FBAE004624A8 /* SiteStatsViewModel+AsyncBlock.swift */; }; FABB20CD2602FC2C00C8785C /* PostUploadOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74AF4D711FE417D200E3EBFE /* PostUploadOperation.swift */; }; FABB20CE2602FC2C00C8785C /* VerticallyStackedButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 37022D901981BF9200F322B7 /* VerticallyStackedButton.m */; }; @@ -4304,7 +4348,7 @@ FABB21902602FC2C00C8785C /* JetpackRestoreStatusFailedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAD95D2625B91BCF00F011B5 /* JetpackRestoreStatusFailedViewController.swift */; }; FABB21912602FC2C00C8785C /* ReaderTopicToReaderListTopic37to38.swift in Sources */ = {isa = PBXBuildFile; fileRef = E66969DB1B9E55C300EC9C00 /* ReaderTopicToReaderListTopic37to38.swift */; }; FABB21922602FC2C00C8785C /* BlogService+JetpackConvenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A2D0B22225CB92B009E585F /* BlogService+JetpackConvenience.swift */; }; - FABB21932602FC2C00C8785C /* GutenbergTenorMediaPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = C856748E243EF177001A995E /* GutenbergTenorMediaPicker.swift */; }; + FABB21932602FC2C00C8785C /* GutenbergExternalMeidaPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = C856748E243EF177001A995E /* GutenbergExternalMeidaPicker.swift */; }; FABB21942602FC2C00C8785C /* AztecPostViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50C0C5D1EF42A4A00372C65 /* AztecPostViewController.swift */; }; FABB21972602FC2C00C8785C /* MenusViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 086E1FDF1BBB35D2002D86CA /* MenusViewController.m */; }; FABB21982602FC2C00C8785C /* UIViewController+Notice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F3EFCA2208E308900268758 /* UIViewController+Notice.swift */; }; @@ -4340,7 +4384,7 @@ FABB21B72602FC2C00C8785C /* TopicsCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B7F25A624E6EDB4007D82CC /* TopicsCollectionView.swift */; }; FABB21B82602FC2C00C8785C /* SiteSegmentsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8CB561F2181A8CE00554EAE /* SiteSegmentsService.swift */; }; FABB21B92602FC2C00C8785C /* BlogListDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11E77591E72932F0072AD40 /* BlogListDataSource.swift */; }; - FABB21BA2602FC2C00C8785C /* MediaItemTableViewCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1730D4A21E97E3E400326B7C /* MediaItemTableViewCells.swift */; }; + FABB21BA2602FC2C00C8785C /* MediaItemHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1730D4A21E97E3E400326B7C /* MediaItemHeaderView.swift */; }; FABB21BB2602FC2C00C8785C /* AbstractPost+fixLocalMediaURLs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BFE36FC230F16580061EBA8 /* AbstractPost+fixLocalMediaURLs.swift */; }; FABB21BC2602FC2C00C8785C /* MediaNoticeNavigationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1750BD6C201144DB0050F13A /* MediaNoticeNavigationCoordinator.swift */; }; FABB21BD2602FC2C00C8785C /* ReaderPostToReaderPost37to38.swift in Sources */ = {isa = PBXBuildFile; fileRef = E66969E31B9E68B200EC9C00 /* ReaderPostToReaderPost37to38.swift */; }; @@ -4363,7 +4407,6 @@ FABB21D02602FC2C00C8785C /* SiteStatsPeriodTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 984B138D21F65F860004B6A2 /* SiteStatsPeriodTableViewController.swift */; }; FABB21D12602FC2C00C8785C /* TextBundleWrapper.m in Sources */ = {isa = PBXBuildFile; fileRef = 43EE90EE223B1028006A33E9 /* TextBundleWrapper.m */; }; FABB21D22602FC2C00C8785C /* WPCategoryTree.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F605FA925145F7200F99544 /* WPCategoryTree.swift */; }; - FABB21D32602FC2C00C8785C /* PageListTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 5DFA7EC51AF814E40072023B /* PageListTableViewCell.m */; }; FABB21D42602FC2C00C8785C /* PageSettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D62BAD618AA88210044E5F7 /* PageSettingsViewController.m */; }; FABB21D52602FC2C00C8785C /* SiteDesignContentCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46241C3A2540D483002B8A12 /* SiteDesignContentCollectionViewController.swift */; }; FABB21D62602FC2C00C8785C /* PlanComparisonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17D2FDC11C6A468A00944265 /* PlanComparisonViewController.swift */; }; @@ -4380,7 +4423,6 @@ FABB21E32602FC2C00C8785C /* NSCalendar+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E167F319C08D18009535AA /* NSCalendar+Helpers.swift */; }; FABB21E42602FC2C00C8785C /* GutenbergCoverUploadProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4629E4202440C5B20002E15C /* GutenbergCoverUploadProcessor.swift */; }; FABB21E52602FC2C00C8785C /* MediaQuotaCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF00889E204E01AE007CCE66 /* MediaQuotaCell.swift */; }; - FABB21E62602FC2C00C8785C /* PartScreenPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5844B6A235EAF3D007C6557 /* PartScreenPresentationController.swift */; }; FABB21E72602FC2C00C8785C /* WPRichTextImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6805D2E1DCD399600168E4F /* WPRichTextImage.swift */; }; FABB21E82602FC2C00C8785C /* FormattableContentRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E4123B420F4097A00DF8486 /* FormattableContentRange.swift */; }; FABB21E92602FC2C00C8785C /* MenuItemSourceHeaderView.m in Sources */ = {isa = PBXBuildFile; fileRef = 08D978521CD2AF7D0054F19A /* MenuItemSourceHeaderView.m */; }; @@ -4391,7 +4433,6 @@ FABB21EF2602FC2C00C8785C /* KeyboardDismissHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56695AF1D411EEB007E342F /* KeyboardDismissHelper.swift */; }; FABB21F02602FC2C00C8785C /* FancyAlertViewController+CreateButtonAnnouncement.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5B9D7EF245BA938002BB2C7 /* FancyAlertViewController+CreateButtonAnnouncement.swift */; }; FABB21F12602FC2C00C8785C /* GutenbergSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF54D4631D6F3FA900A0DC4D /* GutenbergSettings.swift */; }; - FABB21F22602FC2C00C8785C /* MediaPickingContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D80BC79D20746B4100614A59 /* MediaPickingContext.swift */; }; FABB21F32602FC2C00C8785C /* ReaderStreamHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6D2E16B1B8B423B0000ED14 /* ReaderStreamHeader.swift */; }; FABB21F42602FC2C00C8785C /* Progress+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD12D5D1FE1998D00F20A00 /* Progress+Helpers.swift */; }; FABB21F52602FC2C00C8785C /* PostNoticeNavigationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 170CE73F2064478600A48191 /* PostNoticeNavigationCoordinator.swift */; }; @@ -4405,13 +4446,11 @@ FABB21FE2602FC2C00C8785C /* WPLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = 59DD94331AC479ED0032DD6B /* WPLogger.m */; }; FABB21FF2602FC2C00C8785C /* JetpackBackupStatusCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAB8FD4F25AEB0F500D5D54A /* JetpackBackupStatusCoordinator.swift */; }; FABB22012602FC2C00C8785C /* RevisionDiff.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A38DC67218899FB006A409B /* RevisionDiff.swift */; }; - FABB22022602FC2C00C8785C /* MediaThumbnailCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4C069E206560E500E0B2BC /* MediaThumbnailCoordinator.swift */; }; FABB22032602FC2C00C8785C /* RevisionDiffsPageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A4697B121B002AD00468B64 /* RevisionDiffsPageManager.swift */; }; FABB22042602FC2C00C8785C /* CalendarCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5660D06235D114500020B1E /* CalendarCollectionView.swift */; }; FABB22052602FC2C00C8785C /* SubjectContentGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E3E7A6320E44ED60075D159 /* SubjectContentGroup.swift */; }; FABB22062602FC2C00C8785C /* PluginListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E151C0C51F3889DF00710A83 /* PluginListRow.swift */; }; FABB22072602FC2C00C8785C /* BlogSettings+DateAndTimeFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8217380A1FE05EE600BEC94C /* BlogSettings+DateAndTimeFormat.swift */; }; - FABB22082602FC2C00C8785C /* PostSearchHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57047A4E22A961BC00B461DF /* PostSearchHeader.swift */; }; FABB22092602FC2C00C8785C /* ReaderPostCardContentLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 088B89881DA6F93B000E8DEF /* ReaderPostCardContentLabel.swift */; }; FABB220A2602FC2C00C8785C /* FilterChipButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B93412E257029F50097D0AC /* FilterChipButton.swift */; }; FABB220C2602FC2C00C8785C /* Blog.m in Sources */ = {isa = PBXBuildFile; fileRef = CEBD3EAA0FF1BA3B00C1396E /* Blog.m */; }; @@ -4422,12 +4461,9 @@ FABB22112602FC2C00C8785C /* MyProfileHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1CCB2C204DDD18000EE3AC /* MyProfileHeaderView.swift */; }; FABB22142602FC2C00C8785C /* TitleBadgeDisclosureCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E66E2A671FE432B900788F22 /* TitleBadgeDisclosureCell.swift */; }; FABB22152602FC2C00C8785C /* FormattableContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E4123B220F4097A00DF8486 /* FormattableContent.swift */; }; - FABB22162602FC2C00C8785C /* WebAddressWizardContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D82253DD2199418B0014D0E2 /* WebAddressWizardContent.swift */; }; FABB22182602FC2C00C8785C /* SiteSegmentsStep.swift in Sources */ = {isa = PBXBuildFile; fileRef = D853723B21952DC90076F461 /* SiteSegmentsStep.swift */; }; FABB22192602FC2C00C8785C /* ReaderInterestsCollectionViewFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32E1BFD924A66F2A007A08F0 /* ReaderInterestsCollectionViewFlowLayout.swift */; }; - FABB221A2602FC2C00C8785C /* PHAsset+Metadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF70A3201FD5840500BC270D /* PHAsset+Metadata.swift */; }; FABB221B2602FC2C00C8785C /* Theme.m in Sources */ = {isa = PBXBuildFile; fileRef = 5DA5BF3418E32DCF005F11F9 /* Theme.m */; }; - FABB221C2602FC2C00C8785C /* StockPhotosMediaGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A3A5B0206A49A100992576 /* StockPhotosMediaGroup.swift */; }; FABB221D2602FC2C00C8785C /* DefaultFormattableContentAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E4123AF20F4097A00DF8486 /* DefaultFormattableContentAction.swift */; }; FABB221E2602FC2C00C8785C /* PostEditorNavigationBarManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E504D4921A5B8D400E341A8 /* PostEditorNavigationBarManager.swift */; }; FABB221F2602FC2C00C8785C /* Charts+LargeValueFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73CB13962289BEFB00265F49 /* Charts+LargeValueFormatter.swift */; }; @@ -4457,7 +4493,6 @@ FABB22382602FC2C00C8785C /* EventLoggingDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F913BB0D24B3C58B00C19032 /* EventLoggingDelegate.swift */; }; FABB223B2602FC2C00C8785C /* AbstractPost+Searchable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74729CAD205722E300D1394D /* AbstractPost+Searchable.swift */; }; FABB223C2602FC2C00C8785C /* EditCommentViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2906F810110CDA8900169D56 /* EditCommentViewController.m */; }; - FABB223D2602FC2C00C8785C /* ThisWeekWidgetStats.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98F93181239AF64800E4E96E /* ThisWeekWidgetStats.swift */; }; FABB223E2602FC2C00C8785C /* PostAutoUploadMessageProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F16C35D923F3F76C00C81331 /* PostAutoUploadMessageProvider.swift */; }; FABB223F2602FC2C00C8785C /* GutenbergMediaPickerHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91D8364021946EFB008340B2 /* GutenbergMediaPickerHelper.swift */; }; FABB22402602FC2C00C8785C /* FeatureFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D690151F828FF000200E30 /* FeatureFlag.swift */; }; @@ -4522,7 +4557,6 @@ FABB22822602FC2C00C8785C /* WPTableViewActivityCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 8370D10911FA499A009D650F /* WPTableViewActivityCell.m */; }; FABB22842602FC2C00C8785C /* PeopleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1B912881BB01288003C25B9 /* PeopleViewController.swift */; }; FABB22852602FC2C00C8785C /* ConfettiView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FEC241425D73E8B007AFE63 /* ConfettiView.swift */; }; - FABB22862602FC2C00C8785C /* RegisterDomainSuggestionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 436D560C2117312600CEAA33 /* RegisterDomainSuggestionsViewController.swift */; }; FABB22872602FC2C00C8785C /* AbstractPost+MarkAsFailedAndDraftIfNeeded.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BC12F732320181E004DDA72 /* AbstractPost+MarkAsFailedAndDraftIfNeeded.swift */; }; FABB22882602FC2C00C8785C /* UserSuggestion+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0B68A9B252FA91E0001B28C /* UserSuggestion+CoreDataProperties.swift */; }; FABB228A2602FC2C00C8785C /* ReaderHeaderAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8212CC620AA85C1008E8AE8 /* ReaderHeaderAction.swift */; }; @@ -4530,7 +4564,6 @@ FABB228C2602FC2C00C8785C /* ReaderSiteStreamHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6D2E1641B8AAD7E0000ED14 /* ReaderSiteStreamHeader.swift */; }; FABB228D2602FC2C00C8785C /* VideoUploadProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F126FDFD20A33BDB0010EB6E /* VideoUploadProcessor.swift */; }; FABB228E2602FC2C00C8785C /* PeopleCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E166FA1A1BB0656B00374B5B /* PeopleCellViewModel.swift */; }; - FABB228F2602FC2C00C8785C /* TableViewOffsetCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73CE3E0D21F7F9D3007C9C85 /* TableViewOffsetCoordinator.swift */; }; FABB22902602FC2C00C8785C /* RegisterDomainDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 436D56192117312700CEAA33 /* RegisterDomainDetailsViewController.swift */; }; FABB22912602FC2C00C8785C /* FilterProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5E29035243E4F5F00C19CA5 /* FilterProvider.swift */; }; FABB22922602FC2C00C8785C /* DomainCreditEligibilityChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027AC51C227896540033E56E /* DomainCreditEligibilityChecker.swift */; }; @@ -4547,12 +4580,10 @@ FABB229D2602FC2C00C8785C /* ImgUploadProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F126FDFE20A33BDB0010EB6E /* ImgUploadProcessor.swift */; }; FABB229E2602FC2C00C8785C /* RegisterDomainDetailsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 436D56112117312700CEAA33 /* RegisterDomainDetailsViewModel.swift */; }; FABB229F2602FC2C00C8785C /* JetpackScanHistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76F48DB25BA202600BFEC87 /* JetpackScanHistoryViewController.swift */; }; - FABB22A02602FC2C00C8785C /* GutenbergStockPhotos.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E407120237163B8003627FA /* GutenbergStockPhotos.swift */; }; FABB22A12602FC2C00C8785C /* MediaHost+Blog.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11C9F73243B3C3E00921DDC /* MediaHost+Blog.swift */; }; FABB22A22602FC2C00C8785C /* PlanListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E15644EC1CE0E4FE00D96E64 /* PlanListRow.swift */; }; FABB22A32602FC2C00C8785C /* SiteCreationWizard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 738B9A4021B85CF20005062B /* SiteCreationWizard.swift */; }; FABB22A52602FC2C00C8785C /* SignupEpilogueViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98656BD72037A1770079DE67 /* SignupEpilogueViewController.swift */; }; - FABB22A62602FC2C00C8785C /* TenorPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = C81CCD76243BF7A600A83E27 /* TenorPicker.swift */; }; FABB22A72602FC2C00C8785C /* BlogListViewController+Activity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74EFB5C7208674250070BD4E /* BlogListViewController+Activity.swift */; }; FABB22A82602FC2C00C8785C /* TableDataCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 738B9A4A21B85CF20005062B /* TableDataCoordinator.swift */; }; FABB22A92602FC2C00C8785C /* SettingsMultiTextViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = B541276A1C0F7D610015CA80 /* SettingsMultiTextViewController.m */; }; @@ -4565,11 +4596,9 @@ FABB22B12602FC2C00C8785C /* ReaderDetailFeaturedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3223393B24FEC18000BDD4BF /* ReaderDetailFeaturedImageView.swift */; }; FABB22B22602FC2C00C8785C /* StatsDataHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98B52AE021F7AF4A006FF6B4 /* StatsDataHelper.swift */; }; FABB22B32602FC2C00C8785C /* PrepublishingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BAD272B241FEF3300E9D105 /* PrepublishingViewController.swift */; }; - FABB22B42602FC2C00C8785C /* PageListTableViewHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A22D9BF214A6BCA00BAEAF2 /* PageListTableViewHandler.swift */; }; FABB22B52602FC2C00C8785C /* SearchableItemConvertable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74729CA52056FE6000D1394D /* SearchableItemConvertable.swift */; }; FABB22B72602FC2C00C8785C /* SettingsListEditorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B59D994E1C0790CC0003D795 /* SettingsListEditorViewController.swift */; }; FABB22B82602FC2C00C8785C /* QuickStartChecklistManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A4E215B21F75BBE00EFF212 /* QuickStartChecklistManager.swift */; }; - FABB22B92602FC2C00C8785C /* NoResultsTenorConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C81CCD7A243BF7A600A83E27 /* NoResultsTenorConfiguration.swift */; }; FABB22BA2602FC2C00C8785C /* JetpackRemoteInstallViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A8ECE062254A3260043C8DA /* JetpackRemoteInstallViewController.swift */; }; FABB22BB2602FC2C00C8785C /* PlanDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1724DDC71C60F1200099D273 /* PlanDetailViewController.swift */; }; FABB22BC2602FC2C00C8785C /* Array+Page.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A2CD5362146B8C700AE5055 /* Array+Page.swift */; }; @@ -4603,7 +4632,6 @@ FABB22DA2602FC2C00C8785C /* RoundedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E15644E81CE0E47C00D96E64 /* RoundedButton.swift */; }; FABB22DC2602FC2C00C8785C /* MenuLocation.m in Sources */ = {isa = PBXBuildFile; fileRef = 08CC677D1C49B65A00153AD7 /* MenuLocation.m */; }; FABB22DD2602FC2C00C8785C /* RevisionsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4349B0AD218A477F0034118A /* RevisionsTableViewCell.swift */; }; - FABB22DE2602FC2C00C8785C /* KeyboardInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 738B9A4C21B85CF20005062B /* KeyboardInfo.swift */; }; FABB22DF2602FC2C00C8785C /* PlanDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E15644F21CE0E5A500D96E64 /* PlanDetailViewModel.swift */; }; FABB22E02602FC2C00C8785C /* DebugMenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17E4CD0B238C33F300C56916 /* DebugMenuViewController.swift */; }; FABB22E12602FC2C00C8785C /* MenuItemCheckButtonView.m in Sources */ = {isa = PBXBuildFile; fileRef = 08D978501CD2AF7D0054F19A /* MenuItemCheckButtonView.m */; }; @@ -4616,7 +4644,6 @@ FABB22E82602FC2C00C8785C /* WPStyleGuide+Posts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5703A4C522C003DC0028A343 /* WPStyleGuide+Posts.swift */; }; FABB22E92602FC2C00C8785C /* GutenbergVideoUploadProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91138454228373EB00FB02B7 /* GutenbergVideoUploadProcessor.swift */; }; FABB22EA2602FC2C00C8785C /* PostCategory.m in Sources */ = {isa = PBXBuildFile; fileRef = E125445512BF5B3900D87A0A /* PostCategory.m */; }; - FABB22EB2602FC2C00C8785C /* MediaAssetExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08C388651ED7705E0057BE49 /* MediaAssetExporter.swift */; }; FABB22EC2602FC2C00C8785C /* SharingButtonsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6C892D51C601D55007AD612 /* SharingButtonsViewController.swift */; }; FABB22ED2602FC2C00C8785C /* EditorFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = F115308021B17E65002F1D65 /* EditorFactory.swift */; }; FABB22EE2602FC2C00C8785C /* NotificationSiteSubscriptionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F8E38BD209C6DE200454E3C /* NotificationSiteSubscriptionViewController.swift */; }; @@ -4658,7 +4685,7 @@ FABB23162602FC2C00C8785C /* WPWebViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = E1B62A7A13AA61A100A6FCA4 /* WPWebViewController.m */; }; FABB23172602FC2C00C8785C /* ShadowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D813D67E21AA8BBF0055CCA1 /* ShadowView.swift */; }; FABB23182602FC2C00C8785C /* Wizard.swift in Sources */ = {isa = PBXBuildFile; fileRef = D865721121869C590023A99C /* Wizard.swift */; }; - FABB23192602FC2C00C8785C /* Media+WPMediaAsset.m in Sources */ = {isa = PBXBuildFile; fileRef = 08C388691ED78EE70057BE49 /* Media+WPMediaAsset.m */; }; + FABB23192602FC2C00C8785C /* Media+Extensions.m in Sources */ = {isa = PBXBuildFile; fileRef = 08C388691ED78EE70057BE49 /* Media+Extensions.m */; }; FABB231A2602FC2C00C8785C /* GutenbergImgUploadProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF2EC3BF2209A144006176E1 /* GutenbergImgUploadProcessor.swift */; }; FABB231B2602FC2C00C8785C /* PluginListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E151C0C71F388A2000710A83 /* PluginListViewModel.swift */; }; FABB231C2602FC2C00C8785C /* WPStyleGuide+People.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1B9128A1BB0129C003C25B9 /* WPStyleGuide+People.swift */; }; @@ -4676,7 +4703,6 @@ FABB23282602FC2C00C8785C /* LoggingURLRedactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F93735F022D534FE00A3C312 /* LoggingURLRedactor.swift */; }; FABB23292602FC2C00C8785C /* SiteStatsTableViewCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9874766E219630240080967F /* SiteStatsTableViewCells.swift */; }; FABB232A2602FC2C00C8785C /* SharingButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6E27D611C6144DB0063F821 /* SharingButton.swift */; }; - FABB232B2602FC2C00C8785C /* PostActionSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 570BFD8A22823D7B007859A8 /* PostActionSheet.swift */; }; FABB232C2602FC2C00C8785C /* PublicizeConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6374DBD1C444D8B00F24720 /* PublicizeConnection.swift */; }; FABB232D2602FC2C00C8785C /* TenorPageable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C81CCD72243BF7A500A83E27 /* TenorPageable.swift */; }; FABB232E2602FC2C00C8785C /* AccountService+MergeDuplicates.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6C0ED3A231DA23400A08B57 /* AccountService+MergeDuplicates.swift */; }; @@ -4697,7 +4723,6 @@ FABB23402602FC2C00C8785C /* ReaderStreamViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D1D04741B7A50B100CDE646 /* ReaderStreamViewController.swift */; }; FABB23412602FC2C00C8785C /* SeparatorsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57B92BC1B73B08100DFF00B /* SeparatorsView.swift */; }; FABB23422602FC2C00C8785C /* JetpackScanCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C789952425816F96001B7B43 /* JetpackScanCoordinator.swift */; }; - FABB23432602FC2C00C8785C /* WPStyleGuide+ReadableMargins.m in Sources */ = {isa = PBXBuildFile; fileRef = E69BA1971BB5D7D300078740 /* WPStyleGuide+ReadableMargins.m */; }; FABB23442602FC2C00C8785C /* FormattableContentStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E4123B620F4097B00DF8486 /* FormattableContentStyles.swift */; }; FABB23452602FC2C00C8785C /* StatsBarChartConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 735A9680228E421F00461135 /* StatsBarChartConfiguration.swift */; }; FABB23462602FC2C00C8785C /* LoadingStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F161B0522CC2DC70066A5C5 /* LoadingStatusView.swift */; }; @@ -4711,7 +4736,6 @@ FABB234E2602FC2C00C8785C /* ReaderMenuAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8212CC820AA87E5008E8AE8 /* ReaderMenuAction.swift */; }; FABB234F2602FC2C00C8785C /* PostSharingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 593F26601CAB00CA00F14073 /* PostSharingController.swift */; }; FABB23502602FC2C00C8785C /* LongPressGestureLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA37B19215724E900C80377 /* LongPressGestureLabel.swift */; }; - FABB23512602FC2C00C8785C /* NoResultsStockPhotosConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98F1B1292111017900139493 /* NoResultsStockPhotosConfiguration.swift */; }; FABB23532602FC2C00C8785C /* UIImage+Export.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF70A3211FD5840500BC270D /* UIImage+Export.swift */; }; FABB23542602FC2C00C8785C /* NotificationName+Names.swift in Sources */ = {isa = PBXBuildFile; fileRef = D81322B22050F9110067714D /* NotificationName+Names.swift */; }; FABB23552602FC2C00C8785C /* NSFetchedResultsController+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5DD04731CD3DAB00003DF89 /* NSFetchedResultsController+Helpers.swift */; }; @@ -4727,7 +4751,6 @@ FABB23602602FC2C00C8785C /* PostServiceRemoteFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57D66B99234BB206005A2D74 /* PostServiceRemoteFactory.swift */; }; FABB23612602FC2C00C8785C /* ReaderTabItemsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FC8D19A244F43B500495820 /* ReaderTabItemsStore.swift */; }; FABB23622602FC2C00C8785C /* NoResultsViewController+Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98EB126920D2DC2500D2D5B5 /* NoResultsViewController+Model.swift */; }; - FABB23632602FC2C00C8785C /* PostListTableViewHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 570265142298921800F2214C /* PostListTableViewHandler.swift */; }; FABB23642602FC2C00C8785C /* UIAlertController+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5969E2120A49E86005E9DF1 /* UIAlertController+Helpers.swift */; }; FABB23652602FC2C00C8785C /* RevisionDiff+CoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A4E61F721A2C3BC0017A925 /* RevisionDiff+CoreData.swift */; }; FABB23662602FC2C00C8785C /* NotificationCommentRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E7947AA210BAC5E005BB851 /* NotificationCommentRange.swift */; }; @@ -4739,7 +4762,6 @@ FABB236C2602FC2C00C8785C /* StatSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98CAD295221B4ED1003E8F45 /* StatSection.swift */; }; FABB236D2602FC2C00C8785C /* ReaderDetailCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BDA5A71247C5E5800AB124C /* ReaderDetailCoordinator.swift */; }; FABB236E2602FC2C00C8785C /* AccountService.m in Sources */ = {isa = PBXBuildFile; fileRef = 93C1147E18EC5DD500DAC95C /* AccountService.m */; }; - FABB236F2602FC2C00C8785C /* MediaLibraryPickerDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = FF945F6F1B28242300FB8AC4 /* MediaLibraryPickerDataSource.m */; }; FABB23702602FC2C00C8785C /* PostStatsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 986C90872231AD6200FC31E1 /* PostStatsViewModel.swift */; }; FABB23712602FC2C00C8785C /* StatsStore+Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A4A8F4A235758EF00088CE4 /* StatsStore+Cache.swift */; }; FABB23722602FC2C00C8785C /* WP3DTouchShortcutHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE87E1A11BD405790075D45B /* WP3DTouchShortcutHandler.swift */; }; @@ -4748,7 +4770,6 @@ FABB23752602FC2C00C8785C /* ReaderInterestsDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3236F77124ABB6C90088E8F3 /* ReaderInterestsDataSource.swift */; }; FABB23762602FC2C00C8785C /* ActivityContentGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E4123C920F4184200DF8486 /* ActivityContentGroup.swift */; }; FABB23772602FC2C00C8785C /* MediaImageExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08F8CD2E1EBD29440049D0C0 /* MediaImageExporter.swift */; }; - FABB23782602FC2C00C8785C /* GutenbergViewController+InformativeDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 912347182213484300BD9F97 /* GutenbergViewController+InformativeDialog.swift */; }; FABB23792602FC2C00C8785C /* ChangePasswordViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA6511621F26A24009AA935 /* ChangePasswordViewController.swift */; }; FABB237A2602FC2C00C8785C /* LikeComment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D816C1EF20E0893A00C4D82F /* LikeComment.swift */; }; FABB237B2602FC2C00C8785C /* Page.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59E1D46C1CEF77B500126697 /* Page.swift */; }; @@ -4759,7 +4780,6 @@ FABB23802602FC2C00C8785C /* ReaderCommentsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 5DF8D26019E82B1000A2CD95 /* ReaderCommentsViewController.m */; }; FABB23812602FC2C00C8785C /* MySiteViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1716AEFB25F2927600CF49EC /* MySiteViewController.swift */; }; FABB23822602FC2C00C8785C /* FooterContentGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E3E7A5F20E44E490075D159 /* FooterContentGroup.swift */; }; - FABB23832602FC2C00C8785C /* BasePageListCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 59A3CADC1CD2FF0C009BFA1B /* BasePageListCell.m */; }; FABB23852602FC2C00C8785C /* WPError.m in Sources */ = {isa = PBXBuildFile; fileRef = E114D799153D85A800984182 /* WPError.m */; }; FABB23862602FC2C00C8785C /* ContentRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E53AB0120FE5EAE005796FE /* ContentRouter.swift */; }; FABB23872602FC2C00C8785C /* BlogToBlog32to33.swift in Sources */ = {isa = PBXBuildFile; fileRef = E16273E01B2ACEB600088AF7 /* BlogToBlog32to33.swift */; }; @@ -4770,7 +4790,6 @@ FABB238D2602FC2C00C8785C /* WPStyleGuide+SiteCreation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B560914B208A671E00399AE4 /* WPStyleGuide+SiteCreation.swift */; }; FABB238E2602FC2C00C8785C /* NotificationSettingsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5EFB1C11B31B98E007608A3 /* NotificationSettingsService.swift */; }; FABB23922602FC2C00C8785C /* UIImage+Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = B58C4EC9207C5E1900E32E4D /* UIImage+Assets.swift */; }; - FABB23932602FC2C00C8785C /* LoadMoreCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E684383D221F535900752258 /* LoadMoreCounter.swift */; }; FABB23942602FC2C00C8785C /* LoginEpilogueUserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6158AC91ECDF518005FA441 /* LoginEpilogueUserInfo.swift */; }; FABB23962602FC2C00C8785C /* StatsTableFooter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 983DBBA922125DD300753988 /* StatsTableFooter.swift */; }; FABB23972602FC2C00C8785C /* BlogSyncFacade.m in Sources */ = {isa = PBXBuildFile; fileRef = 85D239A21AE5A5FC0074768D /* BlogSyncFacade.m */; }; @@ -4842,7 +4861,6 @@ FABB23DE2602FC2C00C8785C /* SharingConnectionsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = E6431DE01C4E892900FD8D90 /* SharingConnectionsViewController.m */; }; FABB23DF2602FC2C00C8785C /* BlogSelectorViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D8D53EE19250412003C8859 /* BlogSelectorViewController.m */; }; FABB23E02602FC2C00C8785C /* HomepageSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17523380246C4F9200870B4A /* HomepageSettingsViewController.swift */; }; - FABB23E12602FC2C00C8785C /* TenorMediaGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C81CCD75243BF7A500A83E27 /* TenorMediaGroup.swift */; }; FABB23E22602FC2C00C8785C /* SiteSuggestionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0F2EFBE259378E600C7EB6D /* SiteSuggestionService.swift */; }; FABB23E32602FC2C00C8785C /* NotificationsViewController+PushPrimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4388FEFF20A4E19C00783948 /* NotificationsViewController+PushPrimer.swift */; }; FABB23E42602FC2C00C8785C /* ReaderPostService.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D3D559618F88C3500782892 /* ReaderPostService.m */; }; @@ -4865,9 +4883,7 @@ FABB23F62602FC2C00C8785C /* ActivityLogDetailViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 93069F581762410B000C966D /* ActivityLogDetailViewController.m */; }; FABB23F72602FC2C00C8785C /* NSObject+Helpers.m in Sources */ = {isa = PBXBuildFile; fileRef = B57B99DD19A2DBF200506504 /* NSObject+Helpers.m */; }; FABB23F82602FC2C00C8785C /* ImmuTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E49CE31C4902EE002393A4 /* ImmuTableViewController.swift */; }; - FABB23F92602FC2C00C8785C /* NullStockPhotosService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D88A6495208D7B0B008AE9BC /* NullStockPhotosService.swift */; }; FABB23FA2602FC2C00C8785C /* RevisionPreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A162F2221C26D7500FDC035 /* RevisionPreviewViewController.swift */; }; - FABB23FC2602FC2C00C8785C /* SitePromptView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 467D3DF925E4436000EB9CB0 /* SitePromptView.swift */; }; FABB23FD2602FC2C00C8785C /* BaseRestoreCompleteViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAB8AA2125AF031200F9F8A0 /* BaseRestoreCompleteViewController.swift */; }; FABB23FE2602FC2C00C8785C /* SharingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E616E4B21C480896002C024E /* SharingService.swift */; }; FABB24002602FC2C00C8785C /* BottomSheetPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5E032E52408D537003AF350 /* BottomSheetPresentationController.swift */; }; @@ -4912,7 +4928,6 @@ FABB242A2602FC2C00C8785C /* Pattern.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CFC1561E0AC8FF001DF9E9 /* Pattern.swift */; }; FABB242B2602FC2C00C8785C /* ShareExtensionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 930F09161C7D110E00995926 /* ShareExtensionService.swift */; }; FABB242C2602FC2C00C8785C /* KanvasCameraCustomUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5A34BCA25DF244F00C9654B /* KanvasCameraCustomUI.swift */; }; - FABB242D2602FC2C00C8785C /* MediaPreviewHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ECD5B8020C4D823001AEBC5 /* MediaPreviewHelper.swift */; }; FABB242E2602FC2C00C8785C /* OffsetTableViewHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5A738BC244DF75400EDE065 /* OffsetTableViewHandler.swift */; }; FABB242F2602FC2C00C8785C /* UINavigationController+SplitViewFullscreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 177E7DAC1DD0D1E600890467 /* UINavigationController+SplitViewFullscreen.swift */; }; FABB24302602FC2C00C8785C /* ReaderReblogPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F5B3EAE23A851330060FF1F /* ReaderReblogPresenter.swift */; }; @@ -4961,7 +4976,6 @@ FABB245D2602FC2C00C8785C /* ResultsPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D83CA3A620842CD90060E310 /* ResultsPage.swift */; }; FABB245E2602FC2C00C8785C /* FormattableRangesFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E929CD02110D4F200BCAD88 /* FormattableRangesFactory.swift */; }; FABB245F2602FC2C00C8785C /* Menu+ViewDesign.m in Sources */ = {isa = PBXBuildFile; fileRef = 08D9784C1CD2AF7D0054F19A /* Menu+ViewDesign.m */; }; - FABB24602602FC2C00C8785C /* BlogDetailsViewController+DomainCredit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02AC3091226FFFAA0018D23B /* BlogDetailsViewController+DomainCredit.swift */; }; FABB24612602FC2C00C8785C /* KeyringAccountHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = E60BD230230A3DD400727E82 /* KeyringAccountHelper.swift */; }; FABB24622602FC2C00C8785C /* ManagedAccountSettings+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = E14200791C117A3B00B3B115 /* ManagedAccountSettings+CoreDataProperties.swift */; }; FABB24632602FC2C00C8785C /* PushAuthenticationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B535209C1AF7EB9F00B33BA8 /* PushAuthenticationService.swift */; }; @@ -5028,7 +5042,6 @@ FABB24A52602FC2C00C8785C /* SiteIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F53FF3A923EA725C001AD596 /* SiteIconView.swift */; }; FABB24A62602FC2C00C8785C /* JetpackRestoreCompleteViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA3536F425B01A2C0005A3A0 /* JetpackRestoreCompleteViewController.swift */; }; FABB24A72602FC2C00C8785C /* FormattableNoticonRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E7947AC210BAC7B005BB851 /* FormattableNoticonRange.swift */; }; - FABB24A82602FC2C00C8785C /* PromptViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B55086201CC15CCB004EADB4 /* PromptViewController.swift */; }; FABB24AA2602FC2C00C8785C /* ActivityListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82FC612B1FA8B7FC00A1757E /* ActivityListRow.swift */; }; FABB24AB2602FC2C00C8785C /* UIColor+MurielColorsObjC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 436110DF22C4241A000773AD /* UIColor+MurielColorsObjC.swift */; }; FABB24AC2602FC2C00C8785C /* InteractiveNotificationsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5B68BD31C19AAED00EB59E0 /* InteractiveNotificationsManager.swift */; }; @@ -5057,7 +5070,6 @@ FABB24C52602FC2C00C8785C /* SiteSuggestion+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = B06378BF253F639D00FD45D2 /* SiteSuggestion+CoreDataProperties.swift */; }; FABB24C62602FC2C00C8785C /* UntouchableWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4353BFB121A376BF0009CED3 /* UntouchableWindow.swift */; }; FABB24C72602FC2C00C8785C /* WPProgressTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = FF0AAE091A150A560089841D /* WPProgressTableViewCell.m */; }; - FABB24C82602FC2C00C8785C /* StockPhotosPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A3A5B4206A4C7800992576 /* StockPhotosPicker.swift */; }; FABB24CA2602FC2C00C8785C /* UnifiedPrologueViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 176E194625C465F70058F1C5 /* UnifiedPrologueViewController.swift */; }; FABB24CC2602FC2C00C8785C /* CollapsableHeaderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4625B555253789C000C04AAD /* CollapsableHeaderViewController.swift */; }; FABB24CD2602FC2C00C8785C /* Blog+Lookup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2420BEF025D8DAB300966129 /* Blog+Lookup.swift */; }; @@ -5084,7 +5096,6 @@ FABB24E52602FC2C00C8785C /* CoreDataIterativeMigrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FD83CBE246C751800381999 /* CoreDataIterativeMigrator.swift */; }; FABB24E62602FC2C00C8785C /* ReaderSiteSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17CE77EE20C6CDAA001DEA5A /* ReaderSiteSearchViewController.swift */; }; FABB24E82602FC2C00C8785C /* ReaderPost.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D42A3DD175E7452005CFF05 /* ReaderPost.m */; }; - FABB24E92602FC2C00C8785C /* MediaLibraryMediaPickingCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D80BC79B207464D200614A59 /* MediaLibraryMediaPickingCoordinator.swift */; }; FABB24EA2602FC2C00C8785C /* PageTemplateLayout+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46183CF2251BD658004F9AFD /* PageTemplateLayout+CoreDataClass.swift */; }; FABB24EB2602FC2C00C8785C /* NoResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 982A4C3420227D6700B5518E /* NoResultsViewController.swift */; }; FABB24EC2602FC2C00C8785C /* ActionDispatcherFacade.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57C3D392235DFD8E00FE9CE6 /* ActionDispatcherFacade.swift */; }; @@ -5133,24 +5144,20 @@ FABB251A2602FC2C00C8785C /* HeaderContentStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E3E7A5620E44D130075D159 /* HeaderContentStyles.swift */; }; FABB251B2602FC2C00C8785C /* Revision.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A38DC64218899FA006A409B /* Revision.swift */; }; FABB251C2602FC2C00C8785C /* PluginDirectoryAccessoryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 403269912027719C00608441 /* PluginDirectoryAccessoryItem.swift */; }; - FABB251D2602FC2C00C8785C /* PHAsset+Exporters.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB1FA9F1BF0EC4E0090C761 /* PHAsset+Exporters.swift */; }; FABB251E2602FC2C00C8785C /* GutenbergLightNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 465B097924C877E500336B6C /* GutenbergLightNavigationController.swift */; }; FABB251F2602FC2C00C8785C /* AppFeedbackPromptView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8298F38E1EEF2B15008EB7F0 /* AppFeedbackPromptView.swift */; }; FABB25202602FC2C00C8785C /* UIView+ExistingConstraints.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17A2A1D23BFBD72001E96AC /* UIView+ExistingConstraints.swift */; }; FABB25222602FC2C00C8785C /* MediaRequestAuthenticator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1450CF22437DA3E00A28BFE /* MediaRequestAuthenticator.swift */; }; FABB25232602FC2C00C8785C /* StatsCellHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9881296C219CF1300075FF33 /* StatsCellHeader.swift */; }; FABB25242602FC2C00C8785C /* UIEdgeInsets.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1823E6B1E42231C00C19F53 /* UIEdgeInsets.swift */; }; - FABB25252602FC2C00C8785C /* RestorePageTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D18FE9D1AFBB17400EFEED0 /* RestorePageTableViewCell.m */; }; FABB25262602FC2C00C8785C /* String+RegEx.swift in Sources */ = {isa = PBXBuildFile; fileRef = B54C02231F38F50100574572 /* String+RegEx.swift */; }; FABB25272602FC2C00C8785C /* UIImage+Exporters.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB1FA9D1BF0EB840090C761 /* UIImage+Exporters.swift */; }; FABB25282602FC2C00C8785C /* PostStatsTitleCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98FCFC212231DF43006ECDD4 /* PostStatsTitleCell.swift */; }; FABB25292602FC2C00C8785C /* CommentService.m in Sources */ = {isa = PBXBuildFile; fileRef = E1556CF1193F6FE900FC52EA /* CommentService.m */; }; - FABB252A2602FC2C00C8785C /* MediaLibraryStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D80BC7A12074739300614A59 /* MediaLibraryStrings.swift */; }; FABB252B2602FC2C00C8785C /* StoreFetchingStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A09F914230C3E9700F42AB7 /* StoreFetchingStatus.swift */; }; FABB252C2602FC2C00C8785C /* SiteDateFormatters.swift in Sources */ = {isa = PBXBuildFile; fileRef = F582060123A85495005159A9 /* SiteDateFormatters.swift */; }; FABB252D2602FC2C00C8785C /* PostEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13ACCD31EE5672100CCE985 /* PostEditor.swift */; }; FABB252F2602FC2C00C8785C /* JetpackRestoreWarningCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAD9458D25B5678700F011B5 /* JetpackRestoreWarningCoordinator.swift */; }; - FABB25302602FC2C00C8785C /* WPPickerView.m in Sources */ = {isa = PBXBuildFile; fileRef = 5DB3BA0418D0E7B600F3F3E9 /* WPPickerView.m */; }; FABB25312602FC2C00C8785C /* UIImageView+SiteIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E94D141FE04815000E7C20 /* UIImageView+SiteIcon.swift */; }; FABB25322602FC2C00C8785C /* ReaderSiteSearchService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17CE77EC20C6C2F3001DEA5A /* ReaderSiteSearchService.swift */; }; FABB25332602FC2C00C8785C /* QuickStartNavigationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98FF6A3D23A30A240025FD72 /* QuickStartNavigationSettings.swift */; }; @@ -5163,7 +5170,6 @@ FABB253A2602FC2C00C8785C /* WordPressAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1749965E2271BF08007021BD /* WordPressAppDelegate.swift */; }; FABB253B2602FC2C00C8785C /* MediaService.m in Sources */ = {isa = PBXBuildFile; fileRef = 5DA3EE151925090A00294E0B /* MediaService.m */; }; FABB253C2602FC2C00C8785C /* FormattableContentAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E4123B820F4097B00DF8486 /* FormattableContentAction.swift */; }; - FABB253D2602FC2C00C8785C /* SiteVerticalsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A468E421828D940094B82F /* SiteVerticalsService.swift */; }; FABB253E2602FC2C00C8785C /* ReaderBlockedSiteCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65219FA1B8D10DA000B1217 /* ReaderBlockedSiteCell.swift */; }; FABB253F2602FC2C00C8785C /* JetpackRestoreWarningViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAF13C5225A57ABD003EE470 /* JetpackRestoreWarningViewController.swift */; }; FABB25402602FC2C00C8785C /* SiteAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73178C3021BEE45300E37C9A /* SiteAssembly.swift */; }; @@ -5182,7 +5188,6 @@ FABB254D2602FC2C00C8785C /* WPStyleGuide+Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5B56D3119AFB68800B4E29B /* WPStyleGuide+Notifications.swift */; }; FABB254E2602FC2C00C8785C /* SettingTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = FF8DDCDE1B5DB1C10098826F /* SettingTableViewCell.m */; }; FABB254F2602FC2C00C8785C /* CountriesMapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A3BDA0D22944F3500FBF510 /* CountriesMapView.swift */; }; - FABB25502602FC2C00C8785C /* MediaLibraryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17BB26AD1E6D8321008CD031 /* MediaLibraryViewController.swift */; }; FABB25512602FC2C00C8785C /* TodayWidgetStats.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E58A2E2360D23400E5534B /* TodayWidgetStats.swift */; }; FABB25522602FC2C00C8785C /* GutenbergMediaFilesUploadProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5E1577E25DE04E200EEEDFB /* GutenbergMediaFilesUploadProcessor.swift */; }; FABB25532602FC2C00C8785C /* WordPress-30-31.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = 5DF7F7731B22337C003A05C8 /* WordPress-30-31.xcmappingmodel */; }; @@ -5206,7 +5211,6 @@ FABB25672602FC2C00C8785C /* PushNotificationsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5416CF41C171D7100006DD8 /* PushNotificationsManager.swift */; }; FABB25682602FC2C00C8785C /* StatsInsightsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98AE3DF4219A1788003C0E24 /* StatsInsightsStore.swift */; }; FABB25692602FC2C00C8785C /* MenuItemPagesViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 08216FB71CDBF96000304BA7 /* MenuItemPagesViewController.m */; }; - FABB256B2602FC2C00C8785C /* InteractivePostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57D6C83F229498C4003DDC7E /* InteractivePostView.swift */; }; FABB256C2602FC2C00C8785C /* LinearGradientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F2565F25012D3F006B8BC4 /* LinearGradientView.swift */; }; FABB256D2602FC2C00C8785C /* WizardStep.swift in Sources */ = {isa = PBXBuildFile; fileRef = D865721021869C590023A99C /* WizardStep.swift */; }; FABB256E2602FC2C00C8785C /* PostPostViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43D54D121DCAA070007F575F /* PostPostViewController.swift */; }; @@ -5226,8 +5230,6 @@ FABB257D2602FC2C00C8785C /* SiteStatsDetailTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98467A3E221CD48500DF51AE /* SiteStatsDetailTableViewController.swift */; }; FABB257E2602FC2C00C8785C /* MessageAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11450DE1C4E47E600A6BD0F /* MessageAnimator.swift */; }; FABB257F2602FC2C00C8785C /* JetpackRestoreOptionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA4F65A62594337300EAA9F5 /* JetpackRestoreOptionsViewController.swift */; }; - FABB25802602FC2C00C8785C /* WPStyleGuide+Loader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E14635620B3BEAB00B95F41 /* WPStyleGuide+Loader.swift */; }; - FABB25812602FC2C00C8785C /* MediaThumbnailService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 087EBFA71F02313E001F7ACE /* MediaThumbnailService.swift */; }; FABB25822602FC2C00C8785C /* MediaExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08F8CD291EBD22EF0049D0C0 /* MediaExporter.swift */; }; FABB25832602FC2C00C8785C /* ReaderRelatedPostsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC086D525EDFB1E00B94F2A /* ReaderRelatedPostsCell.swift */; }; FABB25842602FC2C00C8785C /* NoResultsViewController+FollowedSites.swift in Sources */ = {isa = PBXBuildFile; fileRef = 324780E0247F2E2A00987525 /* NoResultsViewController+FollowedSites.swift */; }; @@ -5253,9 +5255,7 @@ FABB259A2602FC2C00C8785C /* ReaderBlockSiteAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8212CC220AA7F57008E8AE8 /* ReaderBlockSiteAction.swift */; }; FABB259C2602FC2C00C8785C /* ReaderPostService+RelatedPosts.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA8E1F7625EEFA7300063673 /* ReaderPostService+RelatedPosts.swift */; }; FABB259D2602FC2C00C8785C /* Blog+Capabilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B518E1641CCAA19200ADFE75 /* Blog+Capabilities.swift */; }; - FABB259E2602FC2C00C8785C /* RestorePostTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 575E126E229779E70041B3EB /* RestorePostTableViewCell.swift */; }; FABB259F2602FC2C00C8785C /* MenuItemCategoriesViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 08216FAF1CDBF96000304BA7 /* MenuItemCategoriesViewController.m */; }; - FABB25A02602FC2C00C8785C /* UINavigationBar+Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 171CC15724FCEBF7008B7180 /* UINavigationBar+Appearance.swift */; }; FABB25A12602FC2C00C8785C /* QuickStartChecklistViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43C9908D21067E22009EFFEB /* QuickStartChecklistViewController.swift */; }; FABB25A22602FC2C00C8785C /* MenuItemsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 082635BA1CEA69280088030C /* MenuItemsViewController.m */; }; FABB25A32602FC2C00C8785C /* ReachabilityUtils+OnlineActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 822876F01E929CFD00696BF7 /* ReachabilityUtils+OnlineActions.swift */; }; @@ -5364,10 +5364,8 @@ FABB26162602FC2C00C8785C /* WPUploadStatusButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 740BD8341A0D4C3600F04D18 /* WPUploadStatusButton.m */; }; FABB26172602FC2C00C8785C /* MediaExternalExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EAD7CCF206D761200BEDCFD /* MediaExternalExporter.swift */; }; FABB26182602FC2C00C8785C /* RegisterDomainDetailsViewModel+SectionDefinitions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 436D56102117312700CEAA33 /* RegisterDomainDetailsViewModel+SectionDefinitions.swift */; }; - FABB26192602FC2C00C8785C /* ActionRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = F53FF3A723EA723D001AD596 /* ActionRow.swift */; }; FABB261A2602FC2C00C8785C /* AppSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA162301CB7031A00E2E110 /* AppSettingsViewController.swift */; }; FABB261B2602FC2C00C8785C /* ReaderPostStreamService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B16CE9925251C89007BE5A9 /* ReaderPostStreamService.swift */; }; - FABB261E2602FC2C00C8785C /* PostCardCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57AA848E228715DA00D3C2A2 /* PostCardCell.swift */; }; FABB26202602FC2C00C8785C /* iAd.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7E21C760202BBC8D00837CF5 /* iAd.framework */; }; FABB26212602FC2C00C8785C /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8298F3911EEF3BA7008EB7F0 /* StoreKit.framework */; }; FABB26222602FC2C00C8785C /* CoreSpotlight.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 93F2E5431E9E5A570050D489 /* CoreSpotlight.framework */; }; @@ -5408,6 +5406,12 @@ FAC1B82729B1F1EE00E0C542 /* BlazePostPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC1B82629B1F1EE00E0C542 /* BlazePostPreviewView.swift */; }; FAC1B82829B1F1EE00E0C542 /* BlazePostPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC1B82629B1F1EE00E0C542 /* BlazePostPreviewView.swift */; }; FACB36F11C5C2BF800C6DF4E /* ThemeWebNavigationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FACB36F01C5C2BF800C6DF4E /* ThemeWebNavigationDelegate.swift */; }; + FACF66CA2ADD4703008C3E13 /* PostListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FACF66C92ADD4703008C3E13 /* PostListCell.swift */; }; + FACF66CB2ADD4703008C3E13 /* PostListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FACF66C92ADD4703008C3E13 /* PostListCell.swift */; }; + FACF66CD2ADD645C008C3E13 /* PostListHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FACF66CC2ADD645C008C3E13 /* PostListHeaderView.swift */; }; + FACF66CE2ADD645C008C3E13 /* PostListHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FACF66CC2ADD645C008C3E13 /* PostListHeaderView.swift */; }; + FACF66D02ADD6CD8008C3E13 /* PostListItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FACF66CF2ADD6CD8008C3E13 /* PostListItemViewModel.swift */; }; + FACF66D12ADD6CD8008C3E13 /* PostListItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FACF66CF2ADD6CD8008C3E13 /* PostListItemViewModel.swift */; }; FAD1263C2A0CF2F50004E24C /* String+NonbreakingSpace.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAD1263B2A0CF2F50004E24C /* String+NonbreakingSpace.swift */; }; FAD1263D2A0CF2F50004E24C /* String+NonbreakingSpace.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAD1263B2A0CF2F50004E24C /* String+NonbreakingSpace.swift */; }; FAD2538F26116A1600EDAF88 /* AppStyleGuide.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAD2538E26116A1600EDAF88 /* AppStyleGuide.swift */; }; @@ -5420,6 +5424,8 @@ FAD256B82611B01B00EDAF88 /* UIColor+WordPressColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAD256922611B01700EDAF88 /* UIColor+WordPressColors.swift */; }; FAD257132611B04D00EDAF88 /* UIColor+JetpackColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAD257112611B04D00EDAF88 /* UIColor+JetpackColors.swift */; }; FAD257F52611B54200EDAF88 /* UIColor+WordPressColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAD256922611B01700EDAF88 /* UIColor+WordPressColors.swift */; }; + FAD3DE812AE2965A00A3B031 /* AbstractPostMenuHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAD3DE802AE2965A00A3B031 /* AbstractPostMenuHelper.swift */; }; + FAD3DE822AE2965A00A3B031 /* AbstractPostMenuHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAD3DE802AE2965A00A3B031 /* AbstractPostMenuHelper.swift */; }; FAD7625B29ED780B00C09583 /* JSONDecoderExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAD7625A29ED780B00C09583 /* JSONDecoderExtension.swift */; }; FAD7625C29ED780B00C09583 /* JSONDecoderExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAD7625A29ED780B00C09583 /* JSONDecoderExtension.swift */; }; FAD7626429F1480E00C09583 /* DashboardActivityLogCardCell+ActivityPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAD7626329F1480E00C09583 /* DashboardActivityLogCardCell+ActivityPresenter.swift */; }; @@ -5450,6 +5456,8 @@ FAE8EE99273AC06F00A65307 /* QuickStartSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE8EE98273AC06F00A65307 /* QuickStartSettings.swift */; }; FAE8EE9A273AC06F00A65307 /* QuickStartSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE8EE98273AC06F00A65307 /* QuickStartSettings.swift */; }; FAE8EE9C273AD0A800A65307 /* QuickStartSettingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE8EE9B273AD0A800A65307 /* QuickStartSettingsTests.swift */; }; + FAEC116E2AEBEEA600F9DA54 /* AbstractPostMenuViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAEC116D2AEBEEA600F9DA54 /* AbstractPostMenuViewModel.swift */; }; + FAEC116F2AEBEEA600F9DA54 /* AbstractPostMenuViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAEC116D2AEBEEA600F9DA54 /* AbstractPostMenuViewModel.swift */; }; FAF0FAAC2AA094C0004C3228 /* NoSiteViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAF0FAAB2AA094C0004C3228 /* NoSiteViewModelTests.swift */; }; FAF13C5325A57ABD003EE470 /* JetpackRestoreWarningViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAF13C5225A57ABD003EE470 /* JetpackRestoreWarningViewController.swift */; }; FAF13E3025A59240003EE470 /* JetpackRestoreStatusViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAF13E2F25A59240003EE470 /* JetpackRestoreStatusViewController.swift */; }; @@ -5504,6 +5512,11 @@ FE32F007275F62620040BE67 /* WebCommentContentRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE32F005275F62620040BE67 /* WebCommentContentRenderer.swift */; }; FE341705275FA157005D5CA7 /* RichCommentContentRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE341704275FA157005D5CA7 /* RichCommentContentRenderer.swift */; }; FE341706275FA157005D5CA7 /* RichCommentContentRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE341704275FA157005D5CA7 /* RichCommentContentRenderer.swift */; }; + FE34ACCF2B1661EB00108B3C /* DashboardBloganuaryCardCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE34ACCE2B1661EB00108B3C /* DashboardBloganuaryCardCell.swift */; }; + FE34ACD02B1661EB00108B3C /* DashboardBloganuaryCardCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE34ACCE2B1661EB00108B3C /* DashboardBloganuaryCardCell.swift */; }; + FE34ACD22B174AE700108B3C /* DashboardBloganuaryCardCellTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE34ACD12B174AE700108B3C /* DashboardBloganuaryCardCellTests.swift */; }; + FE34ACDB2B17AA9300108B3C /* BloganuaryOverlayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE34ACD92B17AA6C00108B3C /* BloganuaryOverlayViewController.swift */; }; + FE34ACDC2B17AA9400108B3C /* BloganuaryOverlayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE34ACD92B17AA6C00108B3C /* BloganuaryOverlayViewController.swift */; }; FE39C135269C37C900EFB827 /* ListTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = FE39C133269C37C900EFB827 /* ListTableViewCell.xib */; }; FE39C136269C37C900EFB827 /* ListTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE39C134269C37C900EFB827 /* ListTableViewCell.swift */; }; FE3D057E26C3D5C1002A51B0 /* ShareAppContentPresenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3D057D26C3D5C1002A51B0 /* ShareAppContentPresenterTests.swift */; }; @@ -5529,6 +5542,9 @@ FE50965A2A17A69F00DDD071 /* TwitterDeprecationTableFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE5096582A17A69F00DDD071 /* TwitterDeprecationTableFooterView.swift */; }; FE50965C2A20D0F300DDD071 /* CommentTableHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE50965B2A20D0F300DDD071 /* CommentTableHeaderView.swift */; }; FE50965D2A20D0F300DDD071 /* CommentTableHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE50965B2A20D0F300DDD071 /* CommentTableHeaderView.swift */; }; + FE6AFE432B18EDF200F76520 /* BloganuaryTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6AFE422B18EDF200F76520 /* BloganuaryTracker.swift */; }; + FE6AFE442B18EDF200F76520 /* BloganuaryTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6AFE422B18EDF200F76520 /* BloganuaryTracker.swift */; }; + FE6AFE472B1A351F00F76520 /* SOTWCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6AFE462B1A351F00F76520 /* SOTWCardView.swift */; }; FE6BB143293227AC001E5F7A /* ContentMigrationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6BB142293227AC001E5F7A /* ContentMigrationCoordinator.swift */; }; FE6BB144293227AC001E5F7A /* ContentMigrationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6BB142293227AC001E5F7A /* ContentMigrationCoordinator.swift */; }; FE6BB1462932289B001E5F7A /* ContentMigrationCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6BB1452932289B001E5F7A /* ContentMigrationCoordinatorTests.swift */; }; @@ -5581,10 +5597,13 @@ FEE48EFC2A4C8312008A48E0 /* Blog+JetpackSocial.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEE48EFB2A4C8312008A48E0 /* Blog+JetpackSocial.swift */; }; FEE48EFD2A4C8312008A48E0 /* Blog+JetpackSocial.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEE48EFB2A4C8312008A48E0 /* Blog+JetpackSocial.swift */; }; FEE48EFF2A4C9855008A48E0 /* Blog+PublicizeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEE48EFE2A4C9855008A48E0 /* Blog+PublicizeTests.swift */; }; + FEF207F42AF2903E0025CB2C /* BloggingPromptRemoteObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEF207F22AF2882A0025CB2C /* BloggingPromptRemoteObject.swift */; }; + FEF207F52AF2904D0025CB2C /* BloggingPromptRemoteObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEF207F22AF2882A0025CB2C /* BloggingPromptRemoteObject.swift */; }; FEF28E822ACB3DCE006C6579 /* ReaderDetailNewHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEF28E812ACB3DCE006C6579 /* ReaderDetailNewHeaderView.swift */; }; FEF28E832ACB3DCE006C6579 /* ReaderDetailNewHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEF28E812ACB3DCE006C6579 /* ReaderDetailNewHeaderView.swift */; }; FEF4DC5528439357003806BE /* ReminderScheduleCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEF4DC5428439357003806BE /* ReminderScheduleCoordinator.swift */; }; FEF4DC5628439357003806BE /* ReminderScheduleCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEF4DC5428439357003806BE /* ReminderScheduleCoordinator.swift */; }; + FEF7F3402AFEA0C200F793FC /* blogging-prompts-bloganuary.json in Resources */ = {isa = PBXBuildFile; fileRef = FEF7F33F2AFEA0C200F793FC /* blogging-prompts-bloganuary.json */; }; FEFA263E26C58427009CCB7E /* ShareAppTextActivityItemSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEFA263D26C58427009CCB7E /* ShareAppTextActivityItemSourceTests.swift */; }; FEFA263F26C5AE9A009CCB7E /* ShareAppContentPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE06AC8226C3BD0900B69DE4 /* ShareAppContentPresenter.swift */; }; FEFA264026C5AE9E009CCB7E /* ShareAppTextActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE06AC8426C3C2F800B69DE4 /* ShareAppTextActivityItemSource.swift */; }; @@ -5616,29 +5635,23 @@ FF2EC3C22209AC19006176E1 /* GutenbergImgUploadProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF2EC3C12209AC19006176E1 /* GutenbergImgUploadProcessorTests.swift */; }; FF355D981FB492DD00244E6D /* ExportableAsset.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF355D971FB492DD00244E6D /* ExportableAsset.swift */; }; FF37F90922385CA000AFA3DB /* RELEASE-NOTES.txt in Resources */ = {isa = PBXBuildFile; fileRef = FF37F90822385C9F00AFA3DB /* RELEASE-NOTES.txt */; }; - FF4C069F206560E500E0B2BC /* MediaThumbnailCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4C069E206560E500E0B2BC /* MediaThumbnailCoordinator.swift */; }; FF4DEAD8244B56E300ACA032 /* CoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FF4DEAD7244B56E200ACA032 /* CoreServices.framework */; }; FF5371631FDFF64F00619A3F /* MediaService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5371621FDFF64F00619A3F /* MediaService.swift */; }; FF54D4641D6F3FA900A0DC4D /* GutenbergSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF54D4631D6F3FA900A0DC4D /* GutenbergSettings.swift */; }; FF619DD51C75246900903B65 /* CLPlacemark+Formatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF619DD41C75246900903B65 /* CLPlacemark+Formatting.swift */; }; - FF70A3221FD5840500BC270D /* PHAsset+Metadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF70A3201FD5840500BC270D /* PHAsset+Metadata.swift */; }; FF70A3231FD5840500BC270D /* UIImage+Export.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF70A3211FD5840500BC270D /* UIImage+Export.swift */; }; FF75933B1BE2423800814D3B /* Photos.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FF75933A1BE2423800814D3B /* Photos.framework */; }; - FF7C89A31E3A1029000472A8 /* MediaLibraryPickerDataSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF7C89A21E3A1029000472A8 /* MediaLibraryPickerDataSourceTests.swift */; }; FF8032661EE9E22200861F28 /* MediaProgressCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8032651EE9E22200861F28 /* MediaProgressCoordinatorTests.swift */; }; FF8791BB1FBAF4B500AD86E6 /* MediaHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8791BA1FBAF4B400AD86E6 /* MediaHelper.swift */; }; FF8A04E01D9BFE7400523BC4 /* CachedAnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8A04DF1D9BFE7400523BC4 /* CachedAnimatedImageView.swift */; }; FF8C54AD21F677260003ABCF /* GutenbergMediaInserterHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8C54AC21F677260003ABCF /* GutenbergMediaInserterHelper.swift */; }; - FF8CD625214184EE00A33A8D /* MediaAssetExporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8CD624214184EE00A33A8D /* MediaAssetExporterTests.swift */; }; FF8DDCDF1B5DB1C10098826F /* SettingTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = FF8DDCDE1B5DB1C10098826F /* SettingTableViewCell.m */; }; - FF945F701B28242300FB8AC4 /* MediaLibraryPickerDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = FF945F6F1B28242300FB8AC4 /* MediaLibraryPickerDataSource.m */; }; FF9A6E7121F9361700D36D14 /* MediaUploadHashTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF9A6E7021F9361700D36D14 /* MediaUploadHashTests.swift */; }; FFA0B7D71CAC1F9F00533B9D /* MainNavigationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA0B7D61CAC1F9F00533B9D /* MainNavigationTests.swift */; }; FFA162311CB7031A00E2E110 /* AppSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA162301CB7031A00E2E110 /* AppSettingsViewController.swift */; }; FFABD800213423F1003C65B6 /* LinkSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFABD7FF213423F1003C65B6 /* LinkSettingsViewController.swift */; }; FFABD80821370496003C65B6 /* SelectPostViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFABD80721370496003C65B6 /* SelectPostViewController.swift */; }; FFB1FA9E1BF0EB840090C761 /* UIImage+Exporters.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB1FA9D1BF0EB840090C761 /* UIImage+Exporters.swift */; }; - FFB1FAA01BF0EC4E0090C761 /* PHAsset+Exporters.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB1FA9F1BF0EC4E0090C761 /* PHAsset+Exporters.swift */; }; FFC02B83222687BF00E64FDE /* GutenbergImageLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC02B82222687BF00E64FDE /* GutenbergImageLoader.swift */; }; FFC6ADDA1B56F366002F3C84 /* LocalCoreDataService.m in Sources */ = {isa = PBXBuildFile; fileRef = FFC6ADD91B56F366002F3C84 /* LocalCoreDataService.m */; }; FFCB9F4B22A125BD0080A45F /* WPException.m in Sources */ = {isa = PBXBuildFile; fileRef = FFCB9F4A22A125BD0080A45F /* WPException.m */; }; @@ -5829,7 +5842,7 @@ 011F52D72A1BECA200B04114 /* PlanSelectionScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlanSelectionScreen.swift; sourceTree = ""; }; 011F52D92A1CA53300B04114 /* CheckoutViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckoutViewController.swift; sourceTree = ""; }; 012041022AAAFE3900E7C707 /* WidgetCenter+JetpackWidgets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "WidgetCenter+JetpackWidgets.swift"; sourceTree = ""; }; - 01281E992A0456CB00464F8F /* DomainsSuggestionsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainsSuggestionsScreen.swift; sourceTree = ""; }; + 01281E992A0456CB00464F8F /* DomainsSelectionScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainsSelectionScreen.swift; sourceTree = ""; }; 01281E9B2A051EEA00464F8F /* MenuNavigationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuNavigationTests.swift; sourceTree = ""; }; 0133A7BD2A8CEADD00B36E58 /* SupportCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportCoordinator.swift; sourceTree = ""; }; 0133A7C02A8E4F6100B36E58 /* support_chat_widget_page.css */ = {isa = PBXFileReference; lastKnownFileType = text.css; path = support_chat_widget_page.css; sourceTree = ""; }; @@ -5842,6 +5855,7 @@ 0148CC2A2859C87000CF5D96 /* BlogServiceMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogServiceMock.swift; sourceTree = ""; }; 014ACD132A1E5033008A706C /* WebKitViewController+SandboxStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WebKitViewController+SandboxStore.swift"; sourceTree = ""; }; 015BA4EA29A788A300920F4B /* StatsTotalInsightsCellTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsTotalInsightsCellTests.swift; sourceTree = ""; }; + 0162314F2B3B3CAD0010E377 /* PrimaryDomainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryDomainView.swift; sourceTree = ""; }; 0167F4AD2AAA0250005B9E42 /* JetpackIntents.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = JetpackIntents.entitlements; sourceTree = ""; }; 0167F4AE2AAA0250005B9E42 /* JetpackIntentsRelease-Alpha.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = "JetpackIntentsRelease-Alpha.entitlements"; sourceTree = ""; }; 0167F4AF2AAA0250005B9E42 /* JetpackIntentsRelease-Internal.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = "JetpackIntentsRelease-Internal.entitlements"; sourceTree = ""; }; @@ -5850,6 +5864,8 @@ 0167F4B32AAA02BD005B9E42 /* JetpackStatsWidgetsRelease-Internal.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = "JetpackStatsWidgetsRelease-Internal.entitlements"; sourceTree = ""; }; 0167F4B42AAA02BD005B9E42 /* JetpackStatsWidgetsRelease-Alpha.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = "JetpackStatsWidgetsRelease-Alpha.entitlements"; sourceTree = ""; }; 0167F4B52AAA02BD005B9E42 /* JetpackStatsWidgets-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "JetpackStatsWidgets-Bridging-Header.h"; sourceTree = ""; }; + 017008442B35C25C00C80490 /* SiteDomainsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteDomainsViewModel.swift; sourceTree = ""; }; + 017C57BA2B2B5555001E7687 /* DomainSelectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainSelectionViewController.swift; sourceTree = ""; }; 018635832A8109DE00915532 /* SupportChatBotViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportChatBotViewController.swift; sourceTree = ""; }; 018635862A8109F900915532 /* SupportChatBotViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportChatBotViewModel.swift; sourceTree = ""; }; 018635892A810A9600915532 /* support_chat_widget_page.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = support_chat_widget_page.html; sourceTree = ""; }; @@ -5861,9 +5877,16 @@ 0188FE472AA62D080093EDA5 /* LockScreenMultiStatWidgetViewProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockScreenMultiStatWidgetViewProvider.swift; sourceTree = ""; }; 0188FE4A2AA62F800093EDA5 /* LockScreenTodayLikesCommentsStatWidgetConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockScreenTodayLikesCommentsStatWidgetConfig.swift; sourceTree = ""; }; 0189AF042ACAD89700F63393 /* ShoppingCartService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShoppingCartService.swift; sourceTree = ""; }; + 018FF1342AE6771A00F301C3 /* LockScreenVerticalCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockScreenVerticalCard.swift; sourceTree = ""; }; + 018FF1362AE67C2600F301C3 /* LockScreenFlexibleCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockScreenFlexibleCard.swift; sourceTree = ""; }; 019D699D2A5EA963003B676D /* RootViewCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootViewCoordinatorTests.swift; sourceTree = ""; }; 019D699F2A5EBF47003B676D /* WordPressAuthenticatorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WordPressAuthenticatorProtocol.swift; sourceTree = ""; }; 01A8508A2A8A126400BD8A97 /* support_chat_widget.css */ = {isa = PBXFileReference; lastKnownFileType = text.css; path = support_chat_widget.css; sourceTree = ""; }; + 01ABF16F2AD578B3004331BD /* WidgetAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetAnalytics.swift; sourceTree = ""; }; + 01B5C3C62AE7FC61007055BB /* UITestConfigurator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestConfigurator.swift; sourceTree = ""; }; + 01B759072B3ECAF300179AE6 /* DomainsStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainsStateView.swift; sourceTree = ""; }; + 01B7590A2B3ED63B00179AE6 /* DomainDetailsWebViewControllerWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainDetailsWebViewControllerWrapper.swift; sourceTree = ""; }; + 01B7590D2B3EEEA400179AE6 /* SiteDomainsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteDomainsViewModelTests.swift; sourceTree = ""; }; 01CE5006290A889F00A9C2E0 /* TracksConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracksConfiguration.swift; sourceTree = ""; }; 01CE5010290A890300A9C2E0 /* TracksConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracksConfiguration.swift; sourceTree = ""; }; 01D2FF5D2AA733690038E040 /* LockScreenFieldView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockScreenFieldView.swift; sourceTree = ""; }; @@ -5874,6 +5897,7 @@ 01DBFD8629BDCBF200F3720F /* JetpackNativeConnectionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackNativeConnectionService.swift; sourceTree = ""; }; 01E258012ACC36FA00F09666 /* PlanStep.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlanStep.swift; sourceTree = ""; }; 01E258042ACC373800F09666 /* PlanWizardContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlanWizardContent.swift; sourceTree = ""; }; + 01E258082ACC3AA000F09666 /* iOS17WidgetAPIs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOS17WidgetAPIs.swift; sourceTree = ""; }; 01E2580A2ACDC72C00F09666 /* PlanWizardContentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlanWizardContentViewModel.swift; sourceTree = ""; }; 01E2580D2ACDC88100F09666 /* PlanWizardContentViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlanWizardContentViewModelTests.swift; sourceTree = ""; }; 01E78D1C296EA54F00FB6863 /* StatsPeriodHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsPeriodHelperTests.swift; sourceTree = ""; }; @@ -5882,7 +5906,6 @@ 02761EC3227010BC009BAF0F /* BlogDetailsSectionIndexTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogDetailsSectionIndexTests.swift; sourceTree = ""; }; 027AC51C227896540033E56E /* DomainCreditEligibilityChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainCreditEligibilityChecker.swift; sourceTree = ""; }; 027AC5202278983F0033E56E /* DomainCreditEligibilityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainCreditEligibilityTests.swift; sourceTree = ""; }; - 02AC3091226FFFAA0018D23B /* BlogDetailsViewController+DomainCredit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BlogDetailsViewController+DomainCredit.swift"; sourceTree = ""; }; 02BE5CBF2281B53F00E351BA /* RegisterDomainDetailsViewModelLoadingStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisterDomainDetailsViewModelLoadingStateTests.swift; sourceTree = ""; }; 02BF30522271D7F000616558 /* DomainCreditRedemptionSuccessViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainCreditRedemptionSuccessViewController.swift; sourceTree = ""; }; 02BF978AFC1EFE50CFD558C2 /* Pods-JetpackStatsWidgets.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JetpackStatsWidgets.release.xcconfig"; path = "../Pods/Target Support Files/Pods-JetpackStatsWidgets/Pods-JetpackStatsWidgets.release.xcconfig"; sourceTree = ""; }; @@ -5890,7 +5913,6 @@ 03216EC5279946CA00D444CA /* SchedulingDatePickerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SchedulingDatePickerViewController.swift; sourceTree = ""; }; 03216ECB27995F3500D444CA /* SchedulingViewControllerPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SchedulingViewControllerPresenter.swift; sourceTree = ""; }; 069A4AA52664448F00413FA9 /* GutenbergFeaturedImageHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GutenbergFeaturedImageHelper.swift; sourceTree = ""; }; - 0807CB711CE670A800CDBDAC /* WPContentSearchHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WPContentSearchHelper.swift; sourceTree = ""; }; 080C449D1CE14A9F00B3A02F /* MenuDetailsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MenuDetailsViewController.h; sourceTree = ""; }; 080C449E1CE14A9F00B3A02F /* MenuDetailsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MenuDetailsViewController.m; sourceTree = ""; }; 0815CF451E96F22600069916 /* MediaImportService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaImportService.swift; sourceTree = ""; }; @@ -5926,7 +5948,7 @@ 08216FC51CDBF96000304BA7 /* MenuItemTypeSelectionView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MenuItemTypeSelectionView.m; sourceTree = ""; }; 08216FC61CDBF96000304BA7 /* MenuItemTypeViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MenuItemTypeViewController.h; sourceTree = ""; }; 08216FC71CDBF96000304BA7 /* MenuItemTypeViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MenuItemTypeViewController.m; sourceTree = ""; }; - 08240C2D2AB8A2DD00E7AEA8 /* DomainListCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainListCard.swift; sourceTree = ""; }; + 08240C2D2AB8A2DD00E7AEA8 /* AllDomainsListCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDomainsListCardView.swift; sourceTree = ""; }; 082635B91CEA69280088030C /* MenuItemsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MenuItemsViewController.h; sourceTree = ""; }; 082635BA1CEA69280088030C /* MenuItemsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MenuItemsViewController.m; sourceTree = ""; }; 0828D7F91E6E09AE00C7C7D4 /* WPAppAnalytics+Media.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "WPAppAnalytics+Media.swift"; sourceTree = ""; }; @@ -5936,6 +5958,7 @@ 082AB9DB1C4F035E000CA523 /* PostTag.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PostTag.h; sourceTree = ""; }; 082AB9DC1C4F035E000CA523 /* PostTag.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PostTag.m; sourceTree = ""; }; 082D50AE1EF46DB300788719 /* WordPress 61.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 61.xcdatamodel"; sourceTree = ""; }; + 0830538B2B2732E400B889FE /* DynamicDashboardCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicDashboardCard.swift; sourceTree = ""; }; 083ED8CB2A4322CB007F89B3 /* ComplianceLocationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComplianceLocationService.swift; sourceTree = ""; }; 0840513D2A4DDE3400A596E6 /* CompliancePopoverCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompliancePopoverCoordinator.swift; sourceTree = ""; }; 0845B8C51E833C56001BA771 /* URL+Helpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URL+Helpers.swift"; sourceTree = ""; }; @@ -5963,22 +5986,16 @@ 086E1FDE1BBB35D2002D86CA /* MenusViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MenusViewController.h; sourceTree = ""; }; 086E1FDF1BBB35D2002D86CA /* MenusViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MenusViewController.m; sourceTree = ""; }; 0878580228B4CF950069F96C /* UserPersistentRepositoryUtility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPersistentRepositoryUtility.swift; sourceTree = ""; }; - 08799C242A334645005317F7 /* Spacing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Spacing.swift; sourceTree = ""; }; 0879FC151E9301DD00E1EFC8 /* MediaTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaTests.swift; sourceTree = ""; }; - 087EBFA71F02313E001F7ACE /* MediaThumbnailService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaThumbnailService.swift; sourceTree = ""; }; - 0880BADB29ED6FF3002D3AB0 /* UIColor+DesignSystem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+DesignSystem.swift"; sourceTree = ""; }; 088134FE2A56C5240027C086 /* CompliancePopoverViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompliancePopoverViewModelTests.swift; sourceTree = ""; }; 0885A3661E837AFE00619B4D /* URLIncrementalFilenameTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLIncrementalFilenameTests.swift; sourceTree = ""; }; 088B89881DA6F93B000E8DEF /* ReaderPostCardContentLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderPostCardContentLabel.swift; sourceTree = ""; }; 088CC593282BEC41007B9421 /* TooltipPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TooltipPresenter.swift; sourceTree = ""; }; - 088D58A429E724F300E6C0F4 /* ColorGallery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorGallery.swift; sourceTree = ""; }; 08A250F728D9E87600F50420 /* CommentDetailInfoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentDetailInfoViewController.swift; sourceTree = ""; }; 08A250FB28D9F0E200F50420 /* CommentDetailInfoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentDetailInfoViewModel.swift; sourceTree = ""; }; 08A2AD781CCED2A800E84454 /* PostTagServiceTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PostTagServiceTests.m; sourceTree = ""; }; 08A2AD7A1CCED8E500E84454 /* PostCategoryServiceTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PostCategoryServiceTests.m; sourceTree = ""; }; - 08A4E128289D202F001D9EC7 /* UserPersistentStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPersistentStore.swift; sourceTree = ""; }; 08A4E12B289D2337001D9EC7 /* UserPersistentRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPersistentRepository.swift; sourceTree = ""; }; - 08A4E12E289D2795001D9EC7 /* UserPersistentStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPersistentStoreTests.swift; sourceTree = ""; }; 08A7343E298AB68000F925C7 /* JetpackPluginOverlayViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackPluginOverlayViewModel.swift; sourceTree = ""; }; 08AA64042A84FFF40076E38D /* DashboardGoogleDomainsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardGoogleDomainsViewModel.swift; sourceTree = ""; }; 08AA640B2A8511FB0076E38D /* DashboardGoogleDomainsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardGoogleDomainsViewModelTests.swift; sourceTree = ""; }; @@ -5995,9 +6012,8 @@ 08BA4BC5298A9AD400015BD2 /* JetpackInstallPluginLogoAnimation_rtl.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = JetpackInstallPluginLogoAnimation_rtl.json; sourceTree = ""; }; 08BA4BC6298A9AD400015BD2 /* JetpackInstallPluginLogoAnimation_ltr.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = JetpackInstallPluginLogoAnimation_ltr.json; sourceTree = ""; }; 08BBA34F2A792B4B00BDCF32 /* DashboardGoogleDomainsCardCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardGoogleDomainsCardCell.swift; sourceTree = ""; }; - 08C388651ED7705E0057BE49 /* MediaAssetExporter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaAssetExporter.swift; sourceTree = ""; }; - 08C388681ED78EE70057BE49 /* Media+WPMediaAsset.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "Media+WPMediaAsset.h"; sourceTree = ""; }; - 08C388691ED78EE70057BE49 /* Media+WPMediaAsset.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "Media+WPMediaAsset.m"; sourceTree = ""; }; + 08C388681ED78EE70057BE49 /* Media+Extensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "Media+Extensions.h"; sourceTree = ""; }; + 08C388691ED78EE70057BE49 /* Media+Extensions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "Media+Extensions.m"; sourceTree = ""; }; 08C42C30281807880034720B /* ReaderSubscribeCommentsActionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderSubscribeCommentsActionTests.swift; sourceTree = ""; }; 08CBC77829AE6CC4000026E7 /* SiteCreationEmptySiteTemplate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteCreationEmptySiteTemplate.swift; sourceTree = ""; }; 08CC67771C49B52E00153AD7 /* WordPress 45.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 45.xcdatamodel"; sourceTree = ""; }; @@ -6037,8 +6053,6 @@ 08E6E07D2A4C405500B807B0 /* CompliancePopoverViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompliancePopoverViewModel.swift; sourceTree = ""; }; 08E77F441EE87FCF006F9515 /* MediaThumbnailExporter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaThumbnailExporter.swift; sourceTree = ""; }; 08E77F461EE9D72F006F9515 /* MediaThumbnailExporterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaThumbnailExporterTests.swift; sourceTree = ""; }; - 08EA036629C9B51200B72A87 /* Color+DesignSystem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+DesignSystem.swift"; sourceTree = ""; }; - 08EA036829C9B53000B72A87 /* Colors.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Colors.xcassets; sourceTree = ""; }; 08F8CD291EBD22EF0049D0C0 /* MediaExporter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaExporter.swift; sourceTree = ""; }; 08F8CD2C1EBD245F0049D0C0 /* MediaExporterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaExporterTests.swift; sourceTree = ""; }; 08F8CD2E1EBD29440049D0C0 /* MediaImageExporter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaImageExporter.swift; sourceTree = ""; }; @@ -6093,30 +6107,43 @@ 0A69300A28B5AA5E00E98DE1 /* FullScreenCommentReplyViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenCommentReplyViewModelTests.swift; sourceTree = ""; }; 0A9610F828B2E56300076EBA /* UserSuggestion+Comparable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserSuggestion+Comparable.swift"; sourceTree = ""; }; 0A9687BB28B40771009DCD2F /* FullScreenCommentReplyViewModelMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenCommentReplyViewModelMock.swift; sourceTree = ""; }; - 0C01A6E92AB37F0F009F7145 /* SiteMediaCollectionCellBadgeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteMediaCollectionCellBadgeView.swift; sourceTree = ""; }; + 0C01A6E92AB37F0F009F7145 /* SiteMediaCollectionCellSelectionOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteMediaCollectionCellSelectionOverlayView.swift; sourceTree = ""; }; 0C0453272AC73343003079C8 /* SiteMediaVideoDurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteMediaVideoDurationView.swift; sourceTree = ""; }; 0C04532A2AC77245003079C8 /* SiteMediaDocumentInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteMediaDocumentInfoView.swift; sourceTree = ""; }; + 0C0AD1052B0C483F00EC06E6 /* ExternalMediaSelectionTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalMediaSelectionTitleView.swift; sourceTree = ""; }; + 0C0AD1092B0CCFA400EC06E6 /* MediaPreviewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreviewController.swift; sourceTree = ""; }; 0C0AE7582A8FAD6A007D9D6C /* MediaPickerMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPickerMenu.swift; sourceTree = ""; }; 0C0D3B0C2A4C79DE0050A00D /* BlazeCampaignsStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeCampaignsStream.swift; sourceTree = ""; }; + 0C1531FD2AE17140003CDE13 /* PostSearchViewModel+Highlighter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PostSearchViewModel+Highlighter.swift"; sourceTree = ""; }; + 0C1DB5FE2B095DA50028F200 /* ImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageView.swift; sourceTree = ""; }; + 0C1DB6072B0A419B0028F200 /* ImageDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDecoder.swift; sourceTree = ""; }; + 0C1DB60A2B0A9A570028F200 /* ImageDownloaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDownloaderTests.swift; sourceTree = ""; }; + 0C1DB60C2B0BDA740028F200 /* TenorWelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TenorWelcomeView.swift; sourceTree = ""; }; 0C23F3352AC4AD3400EE6117 /* SiteMediaSelectionTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteMediaSelectionTitleView.swift; sourceTree = ""; }; 0C23F33D2AC4AEF600EE6117 /* SiteMediaPickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteMediaPickerViewController.swift; sourceTree = ""; }; 0C2518AD2ABE1EA000381D31 /* iphone-photo.heic */ = {isa = PBXFileReference; lastKnownFileType = file; path = "iphone-photo.heic"; sourceTree = ""; }; 0C2C83F92A6EABF300A3ACD9 /* StatsPeriodCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsPeriodCache.swift; sourceTree = ""; }; 0C2C83FC2A6EBD3F00A3ACD9 /* StatsInsightsCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsInsightsCache.swift; sourceTree = ""; }; + 0C308FFD2B1234E70071C551 /* SiteMediaFilterButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteMediaFilterButtonView.swift; sourceTree = ""; }; + 0C3090212B12A5C90071C551 /* UIButton+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIButton+Extensions.swift"; sourceTree = ""; }; 0C35FFF029CB81F700D224EB /* BlogDashboardHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogDashboardHelpers.swift; sourceTree = ""; }; 0C35FFF329CBA6DA00D224EB /* BlogDashboardPersonalizationViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogDashboardPersonalizationViewModelTests.swift; sourceTree = ""; }; 0C35FFF529CBB5DE00D224EB /* BlogDashboardEmptyStateCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogDashboardEmptyStateCell.swift; sourceTree = ""; }; 0C391E5D2A2FE5350040EA91 /* DashboardBlazeCampaignView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardBlazeCampaignView.swift; sourceTree = ""; }; 0C391E602A3002950040EA91 /* BlazeCampaignStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeCampaignStatusView.swift; sourceTree = ""; }; 0C391E632A312DB20040EA91 /* BlazeCampaignViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeCampaignViewModelTests.swift; sourceTree = ""; }; + 0C57510F2B011468001074E5 /* RemoteConfigDebugView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteConfigDebugView.swift; sourceTree = ""; }; 0C63266E2A3D1305000B8C57 /* GutenbergFilesAppMediaSourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GutenbergFilesAppMediaSourceTests.swift; sourceTree = ""; }; 0C6C4CCF2A4F0A000049E762 /* BlazeCampaignsStreamTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeCampaignsStreamTests.swift; sourceTree = ""; }; 0C6C4CD32A4F0AD80049E762 /* blaze-search-page-1.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "blaze-search-page-1.json"; sourceTree = ""; }; 0C6C4CD52A4F0AEE0049E762 /* blaze-search-page-2.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "blaze-search-page-2.json"; sourceTree = ""; }; 0C6C4CD72A4F0F2C0049E762 /* Bundle+TestExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+TestExtensions.swift"; sourceTree = ""; }; + 0C700B852AE1E1300085C2EE /* PageListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageListCell.swift; sourceTree = ""; }; + 0C700B882AE1E1940085C2EE /* PageListItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageListItemViewModel.swift; sourceTree = ""; }; 0C7073942A65CB2E00F325CE /* MemoryCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryCache.swift; sourceTree = ""; }; 0C71959A2A3CA582002EA18C /* SiteSettingsRelatedPostsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteSettingsRelatedPostsView.swift; sourceTree = ""; }; 0C748B4A2A9D71A000809E1A /* SiteMediaCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteMediaCollectionViewController.swift; sourceTree = ""; }; + 0C749D792B0543D0004CB468 /* WPImageViewController+Swift.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WPImageViewController+Swift.swift"; sourceTree = ""; }; 0C75E26D2A9F63CB00B784E5 /* MediaImageService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaImageService.swift; sourceTree = ""; }; 0C7762222AAFD39700E07A88 /* SiteMediaAddMediaMenuController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteMediaAddMediaMenuController.swift; sourceTree = ""; }; 0C7D48192A4DB9300023CF84 /* blaze-search-response.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "blaze-search-response.json"; sourceTree = ""; }; @@ -6128,13 +6155,16 @@ 0C896DDF2A3A763400D7D4E7 /* SettingsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsCell.swift; sourceTree = ""; }; 0C896DE12A3A767200D7D4E7 /* SiteVisibility+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SiteVisibility+Extensions.swift"; sourceTree = ""; }; 0C896DE62A3A832B00D7D4E7 /* SiteVisibilityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteVisibilityTests.swift; sourceTree = ""; }; - 0C8B8C0E2ACDBE1900CCE50F /* DisabledVideoOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisabledVideoOverlay.swift; sourceTree = ""; }; 0C8E2F2C2AC4722F0023F9D6 /* SiteMediaViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteMediaViewController.swift; sourceTree = ""; }; 0C8FC9A02A8BC8630059DCE4 /* PHPickerController+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PHPickerController+Extensions.swift"; sourceTree = ""; }; 0C8FC9A32A8BD39A0059DCE4 /* ItemProviderMediaExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemProviderMediaExporter.swift; sourceTree = ""; }; 0C8FC9A62A8BFAAD0059DCE4 /* NSItemProvider+Exportable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSItemProvider+Exportable.swift"; sourceTree = ""; }; 0C8FC9A92A8C57000059DCE4 /* ItemProviderMediaExporterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemProviderMediaExporterTests.swift; sourceTree = ""; }; 0C8FC9AB2A8C57930059DCE4 /* test-webp.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = "test-webp.webp"; sourceTree = ""; }; + 0CA10F6C2ADAE86D00CE75AC /* PostSearchSuggestionsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostSearchSuggestionsService.swift; sourceTree = ""; }; + 0CA10F722ADB014C00CE75AC /* StringRankedSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringRankedSearch.swift; sourceTree = ""; }; + 0CA10FA42ADB286300CE75AC /* StringRankedSearchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringRankedSearchTests.swift; sourceTree = ""; }; + 0CA10FA62ADB76ED00CE75AC /* PostSearchService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostSearchService.swift; sourceTree = ""; }; 0CA1C8C02A940EE300F691EE /* AvatarMenuController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarMenuController.swift; sourceTree = ""; }; 0CAE8EF12A9E9E8D0073EEB9 /* SiteMediaCollectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteMediaCollectionCell.swift; sourceTree = ""; }; 0CAE8EF52A9E9EE30073EEB9 /* SiteMediaCollectionCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteMediaCollectionCellViewModel.swift; sourceTree = ""; }; @@ -6143,13 +6173,29 @@ 0CB4057029C8DCF4008EED0A /* BlogDashboardPersonalizationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogDashboardPersonalizationViewModel.swift; sourceTree = ""; }; 0CB4057229C8DD01008EED0A /* BlogDashboardPersonalizationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogDashboardPersonalizationView.swift; sourceTree = ""; }; 0CB4057B29C8DEE1008EED0A /* BlogDashboardPersonalizeCardCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogDashboardPersonalizeCardCell.swift; sourceTree = ""; }; + 0CB424ED2ADEE3CD0080B807 /* PostSearchTokenTableCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostSearchTokenTableCell.swift; sourceTree = ""; }; + 0CB424F02ADEE52A0080B807 /* PostSearchToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostSearchToken.swift; sourceTree = ""; }; + 0CB424F32ADF3CBE0080B807 /* PostSearchViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostSearchViewModelTests.swift; sourceTree = ""; }; + 0CB424F52AE0416D0080B807 /* SolidColorActivityIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SolidColorActivityIndicator.swift; sourceTree = ""; }; + 0CB54F562AEC320700582080 /* WordPressAppDelegate+PostCoordinatorDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WordPressAppDelegate+PostCoordinatorDelegate.swift"; sourceTree = ""; }; 0CD223DE2AA8ADFD002BD761 /* DashboardQuickActionsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardQuickActionsViewModel.swift; sourceTree = ""; }; 0CD382822A4B699E00612173 /* DashboardBlazeCardCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardBlazeCardCellViewModel.swift; sourceTree = ""; }; 0CD382852A4B6FCE00612173 /* DashboardBlazeCardCellViewModelTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardBlazeCardCellViewModelTest.swift; sourceTree = ""; }; + 0CD9CC9E2AD73A560044A33C /* PostSearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostSearchViewController.swift; sourceTree = ""; }; + 0CD9CCA22AD831590044A33C /* PostSearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostSearchViewModel.swift; sourceTree = ""; }; + 0CD9FB7D2AF9C4DB009D9C7A /* UIBarButtonItem+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIBarButtonItem+Extensions.swift"; sourceTree = ""; }; + 0CD9FB8A2AFADAFE009D9C7A /* SiteMediaPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteMediaPageViewController.swift; sourceTree = ""; }; 0CDEC40B2A2FAF0500BB3A91 /* DashboardBlazeCampaignsCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardBlazeCampaignsCardView.swift; sourceTree = ""; }; + 0CE538C92B0D6E0000834BA2 /* ExternalMediaDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalMediaDataSource.swift; sourceTree = ""; }; + 0CE538CF2B0E317000834BA2 /* StockPhotosWelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StockPhotosWelcomeView.swift; sourceTree = ""; }; + 0CE7833C2B08F3C300B114EB /* ExternalMediaPickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalMediaPickerViewController.swift; sourceTree = ""; }; + 0CE783402B08FB2E00B114EB /* ExternalMediaPickerCollectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalMediaPickerCollectionCell.swift; sourceTree = ""; }; 0CED955F2A460F4B0020F420 /* DebugFeatureFlagsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugFeatureFlagsView.swift; sourceTree = ""; }; + 0CF0C4222AE98C13006FFAB4 /* AbstractPostHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbstractPostHelper.swift; sourceTree = ""; }; 0CF7D6C22ABB753A006D1E89 /* MediaImageServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaImageServiceTests.swift; sourceTree = ""; }; 0CFD6C792A73E703003DD0A0 /* WordPress 152.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 152.xcdatamodel"; sourceTree = ""; }; + 0CFE9AC52AF44A9F00B8F659 /* AbstractPostHelper+Actions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AbstractPostHelper+Actions.swift"; sourceTree = ""; }; + 0CFE9AC82AF52D3B00B8F659 /* PostSettingsViewController+Swift.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PostSettingsViewController+Swift.swift"; sourceTree = ""; }; 131D0EE49695795ECEDAA446 /* Pods-WordPressTest.release-alpha.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WordPressTest.release-alpha.xcconfig"; path = "../Pods/Target Support Files/Pods-WordPressTest/Pods-WordPressTest.release-alpha.xcconfig"; sourceTree = ""; }; 150B6590614A28DF9AD25491 /* Pods-Apps-Jetpack.release-alpha.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Apps-Jetpack.release-alpha.xcconfig"; path = "../Pods/Target Support Files/Pods-Apps-Jetpack/Pods-Apps-Jetpack.release-alpha.xcconfig"; sourceTree = ""; }; 152F25D5C232985E30F56CAC /* Pods-Apps-Jetpack.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Apps-Jetpack.debug.xcconfig"; path = "../Pods/Target Support Files/Pods-Apps-Jetpack/Pods-Apps-Jetpack.debug.xcconfig"; sourceTree = ""; }; @@ -6158,7 +6204,6 @@ 1703D04B20ECD93800D292E9 /* Routes+Post.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Routes+Post.swift"; sourceTree = ""; }; 1707CE411F3121750020B7FE /* UICollectionViewCell+Tint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UICollectionViewCell+Tint.swift"; sourceTree = ""; }; 170CE73F2064478600A48191 /* PostNoticeNavigationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostNoticeNavigationCoordinator.swift; sourceTree = ""; }; - 171096CA270F01EA001BCDD6 /* DomainSuggestionsTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DomainSuggestionsTableViewController.swift; sourceTree = ""; }; 1714F8CF20E6DA8900226DCB /* RouteMatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteMatcher.swift; sourceTree = ""; }; 1715179120F4B2EB002C4A38 /* Routes+Stats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Routes+Stats.swift"; sourceTree = ""; }; 1715179320F4B5CD002C4A38 /* MySitesCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MySitesCoordinator.swift; sourceTree = ""; }; @@ -6166,7 +6211,6 @@ 17171373265FAA8A00F3A022 /* BloggingRemindersNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BloggingRemindersNavigationController.swift; sourceTree = ""; }; 1717139E265FE59700F3A022 /* ButtonStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonStyles.swift; sourceTree = ""; }; 1719633F1D378D5100898E8B /* SearchWrapperView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchWrapperView.swift; sourceTree = ""; }; - 171CC15724FCEBF7008B7180 /* UINavigationBar+Appearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationBar+Appearance.swift"; sourceTree = ""; }; 17222D45261DDDF10047B163 /* celadon-classic-icon-app-76x76.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "celadon-classic-icon-app-76x76.png"; sourceTree = ""; }; 17222D46261DDDF10047B163 /* celadon-classic-icon-app-76x76@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "celadon-classic-icon-app-76x76@2x.png"; sourceTree = ""; }; 17222D47261DDDF10047B163 /* celadon-classic-icon-app-83.5x83.5@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "celadon-classic-icon-app-83.5x83.5@2x.png"; sourceTree = ""; }; @@ -6223,8 +6267,7 @@ 172F06B62865C04E00C78FD4 /* spectrum-'22-icon-app-76x76@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "spectrum-'22-icon-app-76x76@2x.png"; sourceTree = ""; }; 172F06B72865C04F00C78FD4 /* spectrum-'22-icon-app-83.5x83.5@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "spectrum-'22-icon-app-83.5x83.5@2x.png"; sourceTree = ""; }; 172F06B82865C04F00C78FD4 /* spectrum-'22-icon-app-60x60@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "spectrum-'22-icon-app-60x60@2x.png"; sourceTree = ""; }; - 1730D4A21E97E3E400326B7C /* MediaItemTableViewCells.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaItemTableViewCells.swift; sourceTree = ""; }; - 173B215427875E0600D4DD6B /* DeviceMediaPermissionsHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceMediaPermissionsHeader.swift; sourceTree = ""; }; + 1730D4A21E97E3E400326B7C /* MediaItemHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaItemHeaderView.swift; sourceTree = ""; }; 173BCE781CEB780800AE8817 /* Domain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Domain.swift; sourceTree = ""; }; 173D82E6238EE2A7008432DA /* FeatureFlagTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureFlagTests.swift; sourceTree = ""; }; 173DF290274522A1007C64B5 /* AppAboutScreenConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppAboutScreenConfiguration.swift; sourceTree = ""; }; @@ -6245,8 +6288,6 @@ 1759F1711FE017F20003EC81 /* QueueTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueTests.swift; sourceTree = ""; }; 1759F17F1FE1460C0003EC81 /* NoticeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeView.swift; sourceTree = ""; }; 175A650B20B6F7280023E71B /* ReaderSaveForLater+Analytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReaderSaveForLater+Analytics.swift"; sourceTree = ""; }; - 175CC16F2720548700622FB4 /* DomainExpiryDateFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainExpiryDateFormatter.swift; sourceTree = ""; }; - 175CC17427205BFB00622FB4 /* DomainExpiryDateFormatterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainExpiryDateFormatterTests.swift; sourceTree = ""; }; 175CC1762721814B00622FB4 /* domain-service-updated-domains.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "domain-service-updated-domains.json"; sourceTree = ""; }; 175CC17827230DC900622FB4 /* Bool+StringRepresentation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bool+StringRepresentation.swift"; sourceTree = ""; }; 175CC17B2723103000622FB4 /* WPAnalytics+Domains.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WPAnalytics+Domains.swift"; sourceTree = ""; }; @@ -6321,7 +6362,6 @@ 17B7C89F20EC1D6A0042E260 /* Route.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Route.swift; sourceTree = ""; }; 17B7C8C020EE2A870042E260 /* Routes+Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Routes+Notifications.swift"; sourceTree = ""; }; 17BABCA52124487600B86ADF /* WordPress 79.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 79.xcdatamodel"; sourceTree = ""; }; - 17BB26AD1E6D8321008CD031 /* MediaLibraryViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaLibraryViewController.swift; sourceTree = ""; }; 17BD4A0720F76A4700975AC3 /* Routes+Banners.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Routes+Banners.swift"; sourceTree = ""; }; 17BD4A182101D31B00975AC3 /* NavigationActionHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationActionHelpers.swift; sourceTree = ""; }; 17C1D67B2670E3DC006C8970 /* SiteIconPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteIconPickerView.swift; sourceTree = ""; }; @@ -6359,6 +6399,8 @@ 1A433B1C2254CBEE00AE7910 /* WordPressComRestApi+Defaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WordPressComRestApi+Defaults.swift"; sourceTree = ""; }; 1ABA150722AE5F870039311A /* WordPressUIBundleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WordPressUIBundleTests.swift; sourceTree = ""; }; 1BC96E982E9B1A6DD86AF491 /* Pods-WordPressShareExtension.release-alpha.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WordPressShareExtension.release-alpha.xcconfig"; path = "../Pods/Target Support Files/Pods-WordPressShareExtension/Pods-WordPressShareExtension.release-alpha.xcconfig"; sourceTree = ""; }; + 1D0402722B10FA9100888C30 /* AppSettingsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettingsTests.swift; sourceTree = ""; }; + 1D0402752B10FB9E00888C30 /* AppSettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettingsScreen.swift; sourceTree = ""; }; 1D19C56229C9D9A700FB0087 /* GutenbergVideoPressUploadProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GutenbergVideoPressUploadProcessor.swift; sourceTree = ""; }; 1D19C56529C9DB0A00FB0087 /* GutenbergVideoPressUploadProcessorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GutenbergVideoPressUploadProcessorTests.swift; sourceTree = ""; }; 1D30AB110D05D00D00671497 /* Foundation.framework */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; @@ -6525,6 +6567,7 @@ 37EAAF4C1A11799A006D6306 /* CircularImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircularImageView.swift; sourceTree = ""; }; 3AB6A3B516053EA8D0BC3B17 /* Pods-JetpackStatsWidgets.release-alpha.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JetpackStatsWidgets.release-alpha.xcconfig"; path = "../Pods/Target Support Files/Pods-JetpackStatsWidgets/Pods-JetpackStatsWidgets.release-alpha.xcconfig"; sourceTree = ""; }; 3C8DE270EF0498A2129349B0 /* Pods-JetpackNotificationServiceExtension.release-alpha.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JetpackNotificationServiceExtension.release-alpha.xcconfig"; path = "../Pods/Target Support Files/Pods-JetpackNotificationServiceExtension/Pods-JetpackNotificationServiceExtension.release-alpha.xcconfig"; sourceTree = ""; }; + 3F03F2BC2B45041E00A9CE99 /* XCUIElement+TapUntil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCUIElement+TapUntil.swift"; sourceTree = ""; }; 3F09CCA72428FF3300D00A8C /* ReaderTabViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderTabViewController.swift; sourceTree = ""; }; 3F09CCA92428FF8300D00A8C /* ReaderTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderTabView.swift; sourceTree = ""; }; 3F09CCAD24292EFD00D00A8C /* ReaderTabItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderTabItem.swift; sourceTree = ""; }; @@ -6546,8 +6589,7 @@ 3F39C93427A09927001EC300 /* WordPressLibraryLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WordPressLibraryLogger.swift; sourceTree = ""; }; 3F3CA64F25D3003C00642A89 /* StatsWidgetsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsWidgetsStore.swift; sourceTree = ""; }; 3F3D854A251E6418001CA4D2 /* AnnouncementsDataStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnnouncementsDataStoreTests.swift; sourceTree = ""; }; - 3F3DD0AE26FCDA3100F5F121 /* PresentationButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresentationButton.swift; sourceTree = ""; }; - 3F3DD0B126FD176800F5F121 /* PresentationCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresentationCard.swift; sourceTree = ""; }; + 3F3DD0B126FD176800F5F121 /* SiteDomainsPresentationCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteDomainsPresentationCard.swift; sourceTree = ""; }; 3F3DD0B526FD18EB00F5F121 /* Blog+DomainsDashboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Blog+DomainsDashboardView.swift"; sourceTree = ""; }; 3F421DF424A3EC2B00CA9B9E /* Spotlightable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Spotlightable.swift; sourceTree = ""; }; 3F43602E23F31D48001DEE70 /* ScenePresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScenePresenter.swift; sourceTree = ""; }; @@ -6556,7 +6598,6 @@ 3F4370402893207C00475B6E /* JetpackOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackOverlayView.swift; sourceTree = ""; }; 3F43704328932F0100475B6E /* JetpackBrandingCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackBrandingCoordinator.swift; sourceTree = ""; }; 3F46AB0125BF5D6300CE2E98 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; name = Base; path = Base.lproj/Sites.intentdefinition; sourceTree = ""; }; - 3F46EEC628BC4935004F02B2 /* JetpackPrompt.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JetpackPrompt.swift; sourceTree = ""; }; 3F46EECB28BC4962004F02B2 /* JetpackLandingScreenView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JetpackLandingScreenView.swift; sourceTree = ""; }; 3F46EED028BFF339004F02B2 /* JetpackPromptsConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackPromptsConfiguration.swift; sourceTree = ""; }; 3F4A4C202AD39CB100DE5DF8 /* TruthTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TruthTable.swift; sourceTree = ""; }; @@ -6575,6 +6616,7 @@ 3F5689FF25420DE80048A9E4 /* MultiStatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiStatsView.swift; sourceTree = ""; }; 3F568A1E254213B60048A9E4 /* VerticalCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalCard.swift; sourceTree = ""; }; 3F568A2E254216550048A9E4 /* FlexibleCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlexibleCard.swift; sourceTree = ""; }; + 3F56F55B2AEA2F67006BDCEA /* ReaderPostBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderPostBuilder.swift; sourceTree = ""; }; 3F593FDC2A81DC6D00B29E86 /* NSError+TestInstance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSError+TestInstance.swift"; sourceTree = ""; }; 3F5B3EAE23A851330060FF1F /* ReaderReblogPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderReblogPresenter.swift; sourceTree = ""; }; 3F5B3EB023A851480060FF1F /* ReaderReblogFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderReblogFormatter.swift; sourceTree = ""; }; @@ -6631,7 +6673,7 @@ 3FA640652670CEFE0064401E /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Platforms/iPhoneOS.platform/Developer/Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; 3FAA18CB25797B85002B1911 /* UnconfiguredView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnconfiguredView.swift; sourceTree = ""; }; 3FAE0651287C8FC500F46508 /* JPScrollViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JPScrollViewDelegate.swift; sourceTree = ""; }; - 3FAF9CC126D01CFE00268EA2 /* DomainsDashboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainsDashboardView.swift; sourceTree = ""; }; + 3FAF9CC126D01CFE00268EA2 /* SiteDomainsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteDomainsView.swift; sourceTree = ""; }; 3FAF9CC426D03C7400268EA2 /* DomainSuggestionViewControllerWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainSuggestionViewControllerWrapper.swift; sourceTree = ""; }; 3FB1928F26C6109F000F5AA3 /* TimeSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeSelectionView.swift; sourceTree = ""; }; 3FB1929426C79EC6000F5AA3 /* Date+Formats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Formats.swift"; sourceTree = ""; }; @@ -6652,6 +6694,7 @@ 3FDDFE9527C8178C00606933 /* SiteStatsInformationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteStatsInformationTests.swift; sourceTree = ""; }; 3FE20C1425CF165700A15525 /* GroupedViewData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupedViewData.swift; sourceTree = ""; }; 3FE20C3625CF211F00A15525 /* ListViewData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListViewData.swift; sourceTree = ""; }; + 3FE6D31D2B0705D400D14923 /* JetpackBrandingVisibilityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackBrandingVisibilityTests.swift; sourceTree = ""; }; 3FE77C8225B0CA89007DE9E5 /* LocalizableStrings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizableStrings.swift; sourceTree = ""; }; 3FEC241425D73E8B007AFE63 /* ConfettiView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfettiView.swift; sourceTree = ""; }; 3FF15A55291B4EEA00E1B4E5 /* MigrationCenterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationCenterView.swift; sourceTree = ""; }; @@ -6659,6 +6702,7 @@ 3FF1A852242D5FCB00373F5D /* WPTabBarController+ReaderTabNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WPTabBarController+ReaderTabNavigation.swift"; sourceTree = ""; }; 3FF717FE291F07AB00323614 /* MigrationCenterViewConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationCenterViewConfiguration.swift; sourceTree = ""; }; 3FFA5ED12876152E00830E28 /* JetpackButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackButton.swift; sourceTree = ""; }; + 3FFB3F212AFC72EC00A742B0 /* DeepLinkSourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLinkSourceTests.swift; sourceTree = ""; }; 3FFDEF7729177D7500B625CE /* MigrationNotificationsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationNotificationsViewModel.swift; sourceTree = ""; }; 3FFDEF7929177D8C00B625CE /* MigrationNotificationsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationNotificationsViewController.swift; sourceTree = ""; }; 3FFDEF7E29177FB100B625CE /* MigrationStepConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationStepConfiguration.swift; sourceTree = ""; }; @@ -6679,7 +6723,6 @@ 40232A9D230A6A740036B0B6 /* AbstractPost+HashHelpers.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "AbstractPost+HashHelpers.m"; sourceTree = ""; }; 40247E012120FE3600AE1C3C /* AutomatedTransferHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutomatedTransferHelper.swift; sourceTree = ""; }; 402B2A7820ACD7690027C1DC /* ActivityStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityStore.swift; sourceTree = ""; }; - 402FFB1B218C27C100FF4A0B /* RegisterDomain.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = RegisterDomain.storyboard; sourceTree = ""; }; 402FFB20218C33BF00FF4A0B /* WordPress 84.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 84.xcdatamodel"; sourceTree = ""; }; 402FFB23218C36CF00FF4A0B /* AztecVerificationPromptHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AztecVerificationPromptHelper.swift; sourceTree = ""; }; 403269912027719C00608441 /* PluginDirectoryAccessoryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginDirectoryAccessoryItem.swift; sourceTree = ""; }; @@ -6732,7 +6775,6 @@ 436D55DE210F866900CEAA33 /* StoryboardLoadable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryboardLoadable.swift; sourceTree = ""; }; 436D55EF2115CB6800CEAA33 /* RegisterDomainDetailsSectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisterDomainDetailsSectionTests.swift; sourceTree = ""; }; 436D55F4211632B700CEAA33 /* RegisterDomainDetailsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisterDomainDetailsViewModelTests.swift; sourceTree = ""; }; - 436D560C2117312600CEAA33 /* RegisterDomainSuggestionsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RegisterDomainSuggestionsViewController.swift; sourceTree = ""; }; 436D56102117312700CEAA33 /* RegisterDomainDetailsViewModel+SectionDefinitions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "RegisterDomainDetailsViewModel+SectionDefinitions.swift"; sourceTree = ""; }; 436D56112117312700CEAA33 /* RegisterDomainDetailsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RegisterDomainDetailsViewModel.swift; sourceTree = ""; }; 436D56122117312700CEAA33 /* RegisterDomainDetailsViewModel+RowDefinitions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "RegisterDomainDetailsViewModel+RowDefinitions.swift"; sourceTree = ""; }; @@ -6800,8 +6842,6 @@ 465F8A09263B692600F4C950 /* wp-block-editor-v1-settings-success-ThemeJSON.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "wp-block-editor-v1-settings-success-ThemeJSON.json"; sourceTree = ""; }; 46638DF5244904A3006E8439 /* GutenbergBlockProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GutenbergBlockProcessor.swift; sourceTree = ""; }; 466653492501552A00165DD4 /* LayoutPreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutPreviewViewController.swift; sourceTree = ""; }; - 467D3DF925E4436000EB9CB0 /* SitePromptView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SitePromptView.swift; sourceTree = ""; }; - 467D3E0B25E4436D00EB9CB0 /* SitePromptView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SitePromptView.xib; sourceTree = ""; }; 4688E6CB26AB571D00A5D894 /* RequestAuthenticatorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestAuthenticatorTests.swift; sourceTree = ""; }; 469CE06B24BCED75003BDC8B /* CategorySectionTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategorySectionTableViewCell.swift; sourceTree = ""; }; 469CE06C24BCED75003BDC8B /* CategorySectionTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = CategorySectionTableViewCell.xib; sourceTree = ""; }; @@ -6836,7 +6876,6 @@ 4A1E77C82988997C006281CC /* PublicizeConnection+Creation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PublicizeConnection+Creation.swift"; sourceTree = ""; }; 4A1E77CB2989F2F7006281CC /* WPAccount+DeduplicateBlogs.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "WPAccount+DeduplicateBlogs.swift"; sourceTree = ""; }; 4A2172F728EAACFF0006F4F1 /* BlogQuery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogQuery.swift; sourceTree = ""; }; - 4A2172FD28F688890006F4F1 /* Blog+Media.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Blog+Media.swift"; sourceTree = ""; }; 4A266B8E282B05210089CF3D /* JSONObjectTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONObjectTests.swift; sourceTree = ""; }; 4A266B90282B13A70089CF3D /* CoreDataTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataTestCase.swift; sourceTree = ""; }; 4A2C73E02A943D8F00ACE79E /* TaggedManagedObjectID.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TaggedManagedObjectID.swift; sourceTree = ""; }; @@ -6849,6 +6888,9 @@ 4A358DE829B5F14C00BFCEBE /* SharingButton+Lookup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SharingButton+Lookup.swift"; sourceTree = ""; }; 4A526BDD296BE9A50007B5BA /* CoreDataService.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CoreDataService.m; sourceTree = ""; }; 4A526BDE296BE9A50007B5BA /* CoreDataService.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CoreDataService.h; sourceTree = ""; }; + 4A535E132AF3368B008B87B9 /* MenusViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenusViewController.swift; sourceTree = ""; }; + 4A5598842B05AC180083C220 /* PagesListTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagesListTests.swift; sourceTree = ""; }; + 4A5DE7372B0D511900363171 /* PageTree.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageTree.swift; sourceTree = ""; }; 4A76A4BA29D4381000AABF4B /* CommentService+LikesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CommentService+LikesTests.swift"; sourceTree = ""; }; 4A76A4BC29D43BFD00AABF4B /* CommentService+MorderationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CommentService+MorderationTests.swift"; sourceTree = ""; }; 4A76A4BE29D4F0A500AABF4B /* reader-post-comments-success.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "reader-post-comments-success.json"; sourceTree = ""; }; @@ -6864,10 +6906,12 @@ 4AA33EFA2999AE3B005B6E23 /* ReaderListTopic+Creation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReaderListTopic+Creation.swift"; sourceTree = ""; }; 4AA33F002999D11A005B6E23 /* ReaderSiteTopic+Lookup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReaderSiteTopic+Lookup.swift"; sourceTree = ""; }; 4AA33F03299A1F93005B6E23 /* ReaderTagTopic+Lookup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReaderTagTopic+Lookup.swift"; sourceTree = ""; }; + 4AA7EE0E2ADF7367007D261D /* PostRepositoryPostsListTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostRepositoryPostsListTests.swift; sourceTree = ""; }; 4AAD69072A6F68A5007FE77E /* MediaRepositoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaRepositoryTests.swift; sourceTree = ""; }; 4AD5656B28E3D0670054C676 /* ReaderPost+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReaderPost+Helper.swift"; sourceTree = ""; }; 4AD5656E28E413160054C676 /* Blog+History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Blog+History.swift"; sourceTree = ""; }; 4AD5657128E543A30054C676 /* BlogQueryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogQueryTests.swift; sourceTree = ""; }; + 4AD862E42AFAEF1700A07557 /* PostsListAPIStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostsListAPIStub.swift; sourceTree = ""; }; 4AEF2DD829A84B2C00345734 /* ReaderSiteServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderSiteServiceTests.swift; sourceTree = ""; }; 4AFB1A802A9C08CE007CE165 /* StoppableProgressIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoppableProgressIndicatorView.swift; sourceTree = ""; }; 4AFB8FBE2824999400A2F4B2 /* ContextManager+Helpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ContextManager+Helpers.swift"; sourceTree = ""; }; @@ -6878,13 +6922,8 @@ 549D51B99FF59CBE21A37CBF /* Pods-JetpackIntents.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JetpackIntents.release.xcconfig"; path = "../Pods/Target Support Files/Pods-JetpackIntents/Pods-JetpackIntents.release.xcconfig"; sourceTree = ""; }; 56885C902A7D15930027C78F /* HTMLEditorScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTMLEditorScreen.swift; sourceTree = ""; }; 56FEDB6A28783D8F00E1EA93 /* WordPress 145.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 145.xcdatamodel"; sourceTree = ""; }; - 570265142298921800F2214C /* PostListTableViewHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostListTableViewHandler.swift; sourceTree = ""; }; - 570265162298960B00F2214C /* PostListTableViewHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostListTableViewHandlerTests.swift; sourceTree = ""; }; 5703A4C522C003DC0028A343 /* WPStyleGuide+Posts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WPStyleGuide+Posts.swift"; sourceTree = ""; }; - 57047A4E22A961BC00B461DF /* PostSearchHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostSearchHeader.swift; sourceTree = ""; }; 570B037622F1FFF6009D8411 /* PostCoordinatorFailedPostsFetcherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = PostCoordinatorFailedPostsFetcherTests.swift; path = Services/PostCoordinatorFailedPostsFetcherTests.swift; sourceTree = ""; }; - 570BFD8A22823D7B007859A8 /* PostActionSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostActionSheet.swift; sourceTree = ""; }; - 570BFD8C22823DE5007859A8 /* PostActionSheetTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostActionSheetTests.swift; sourceTree = ""; }; 570BFD8F2282418A007859A8 /* PostBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostBuilder.swift; sourceTree = ""; }; 57240223234E5BE200227067 /* PostServiceSelfHostedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = PostServiceSelfHostedTests.swift; path = Services/PostServiceSelfHostedTests.swift; sourceTree = ""; }; 57276E70239BDFD200515BE2 /* NotificationCenter+ObserveMultiple.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationCenter+ObserveMultiple.swift"; sourceTree = ""; }; @@ -6894,15 +6933,10 @@ 57569CF1230485680052EE14 /* PostAutoUploadInteractorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = PostAutoUploadInteractorTests.swift; path = Services/PostAutoUploadInteractorTests.swift; sourceTree = ""; }; 575802122357C41200E4C63C /* MediaCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MediaCoordinatorTests.swift; path = Services/MediaCoordinatorTests.swift; sourceTree = ""; }; 575E126222973EBB0041B3EB /* PostCompactCellGhostableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostCompactCellGhostableTests.swift; sourceTree = ""; }; - 575E126E229779E70041B3EB /* RestorePostTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestorePostTableViewCell.swift; sourceTree = ""; }; - 577C2AAA22936DCB00AD1F03 /* PostCardCellGhostableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostCardCellGhostableTests.swift; sourceTree = ""; }; 577C2AB322943FEC00AD1F03 /* PostCompactCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostCompactCell.swift; sourceTree = ""; }; 577C2AB52294401800AD1F03 /* PostCompactCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = PostCompactCell.xib; sourceTree = ""; }; 57889AB723589DF100DAE56D /* PageBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = PageBuilder.swift; path = TestUtilities/PageBuilder.swift; sourceTree = ""; }; 5789E5C722D7D40800333698 /* AztecPostViewControllerAttachmentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AztecPostViewControllerAttachmentTests.swift; path = Aztec/AztecPostViewControllerAttachmentTests.swift; sourceTree = ""; }; - 57AA848E228715DA00D3C2A2 /* PostCardCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostCardCell.swift; sourceTree = ""; }; - 57AA8490228715E700D3C2A2 /* PostCardCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = PostCardCell.xib; sourceTree = ""; }; - 57AA8492228790AA00D3C2A2 /* PostCardCellTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostCardCellTests.swift; sourceTree = ""; }; 57B71D4D230DB5F200789A68 /* BlogBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogBuilder.swift; sourceTree = ""; }; 57BAD50B225CCE1A006139EC /* WPTabBarController+Swift.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WPTabBarController+Swift.swift"; sourceTree = ""; }; 57C2331722FE0EC900A3863B /* PostAutoUploadInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostAutoUploadInteractor.swift; sourceTree = ""; }; @@ -6913,7 +6947,6 @@ 57D66B99234BB206005A2D74 /* PostServiceRemoteFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostServiceRemoteFactory.swift; sourceTree = ""; }; 57D66B9C234BB78B005A2D74 /* PostServiceWPComTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = PostServiceWPComTests.swift; path = Services/PostServiceWPComTests.swift; sourceTree = ""; }; 57D6C83D22945A10003DDC7E /* PostCompactCellTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostCompactCellTests.swift; sourceTree = ""; }; - 57D6C83F229498C4003DDC7E /* InteractivePostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractivePostView.swift; sourceTree = ""; }; 57DF04C0231489A200CC93D6 /* PostCardStatusViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostCardStatusViewModelTests.swift; sourceTree = ""; }; 57E15BC2269B6B7419464B6F /* Pods_Apps_Jetpack.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Apps_Jetpack.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 590E873A1CB8205700D1B734 /* PostListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostListViewController.swift; sourceTree = ""; }; @@ -6934,11 +6967,8 @@ 5960967E1CF7959300848496 /* PostTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostTests.swift; sourceTree = ""; }; 596C035D1B84F21D00899EEB /* ThemeBrowserViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThemeBrowserViewController.swift; sourceTree = ""; }; 596C035F1B84F24000899EEB /* ThemeBrowser.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = ThemeBrowser.storyboard; sourceTree = ""; }; - 597421B01CEB6874005D5F38 /* ConfigurablePostView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ConfigurablePostView.h; sourceTree = ""; }; 5981FE041AB8A89A0009E080 /* WPUserAgentTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WPUserAgentTests.m; sourceTree = ""; }; 598DD1701B97985700146967 /* ThemeBrowserCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThemeBrowserCell.swift; sourceTree = ""; }; - 59A3CADB1CD2FF0C009BFA1B /* BasePageListCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BasePageListCell.h; sourceTree = ""; }; - 59A3CADC1CD2FF0C009BFA1B /* BasePageListCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BasePageListCell.m; sourceTree = ""; }; 59A9AB331B4C33A500A433DC /* ThemeService.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ThemeService.h; sourceTree = ""; }; 59A9AB341B4C33A500A433DC /* ThemeService.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThemeService.m; sourceTree = ""; }; 59A9AB391B4C3ECD00A433DC /* LocalCoreDataServiceTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LocalCoreDataServiceTests.m; sourceTree = ""; }; @@ -6955,14 +6985,10 @@ 5D13FA561AF99C2100F06492 /* PageListSectionHeaderView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = PageListSectionHeaderView.xib; sourceTree = ""; }; 5D146EB9189857ED0068FDC6 /* FeaturedImageViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FeaturedImageViewController.h; sourceTree = ""; usesTabs = 0; }; 5D146EBA189857ED0068FDC6 /* FeaturedImageViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FeaturedImageViewController.m; sourceTree = ""; usesTabs = 0; }; - 5D18FE9C1AFBB17400EFEED0 /* RestorePageTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RestorePageTableViewCell.h; sourceTree = ""; }; - 5D18FE9D1AFBB17400EFEED0 /* RestorePageTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RestorePageTableViewCell.m; sourceTree = ""; }; - 5D18FE9E1AFBB17400EFEED0 /* RestorePageTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = RestorePageTableViewCell.xib; sourceTree = ""; }; 5D1D04731B7A50B100CDE646 /* Reader.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Reader.storyboard; sourceTree = ""; }; 5D1D04741B7A50B100CDE646 /* ReaderStreamViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderStreamViewController.swift; sourceTree = ""; }; 5D229A78199AB74F00685123 /* WordPress 21.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 21.xcdatamodel"; sourceTree = ""; }; 5D2B30B81B7411C700DA15F3 /* ReaderCardDiscoverAttributionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderCardDiscoverAttributionView.swift; sourceTree = ""; }; - 5D2FB2821AE98C4600F1D4ED /* RestorePostTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = RestorePostTableViewCell.xib; sourceTree = ""; }; 5D3D559518F88C3500782892 /* ReaderPostService.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ReaderPostService.h; sourceTree = ""; }; 5D3D559618F88C3500782892 /* ReaderPostService.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ReaderPostService.m; sourceTree = ""; }; 5D3E334C15EEBB6B005FC6F2 /* ReachabilityUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ReachabilityUtils.h; sourceTree = ""; }; @@ -7014,15 +7040,12 @@ 5DA5BF3318E32DCF005F11F9 /* Theme.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Theme.h; sourceTree = ""; }; 5DA5BF3418E32DCF005F11F9 /* Theme.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Theme.m; sourceTree = ""; }; 5DA5BF4B18E331D8005F11F9 /* WordPress 16.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 16.xcdatamodel"; sourceTree = ""; }; - 5DB3BA0318D0E7B600F3F3E9 /* WPPickerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WPPickerView.h; sourceTree = ""; usesTabs = 0; }; - 5DB3BA0418D0E7B600F3F3E9 /* WPPickerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WPPickerView.m; sourceTree = ""; usesTabs = 0; }; 5DB4683918A2E718004A89A9 /* LocationService.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LocationService.h; sourceTree = ""; }; 5DB4683A18A2E718004A89A9 /* LocationService.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LocationService.m; sourceTree = ""; }; 5DB6D8F618F5DA6300956529 /* WordPress 17.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 17.xcdatamodel"; sourceTree = ""; }; 5DB767401588F64D00EBE36C /* postPreview.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; name = postPreview.html; path = Resources/HTML/postPreview.html; sourceTree = ""; }; 5DBCD9D318F35D7500B32229 /* ReaderTopicService.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ReaderTopicService.h; sourceTree = ""; }; 5DBCD9D418F35D7500B32229 /* ReaderTopicService.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ReaderTopicService.m; sourceTree = ""; }; - 5DBFC8A81A9BE07B00E00DE4 /* Posts.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Posts.storyboard; sourceTree = ""; }; 5DBFC8AA1A9C0EEF00E00DE4 /* WPScrollableViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WPScrollableViewController.h; sourceTree = ""; }; 5DE471B71B4C710E00665C44 /* ReaderPostContentProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ReaderPostContentProvider.h; sourceTree = ""; }; 5DE8A0401912D95B00B2FF59 /* ReaderPostServiceTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ReaderPostServiceTest.m; sourceTree = ""; }; @@ -7035,9 +7058,6 @@ 5DF8D25F19E82B1000A2CD95 /* ReaderCommentsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ReaderCommentsViewController.h; sourceTree = ""; }; 5DF8D26019E82B1000A2CD95 /* ReaderCommentsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = ReaderCommentsViewController.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; 5DFA7EC21AF7CB910072023B /* Pages.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Pages.storyboard; sourceTree = ""; }; - 5DFA7EC41AF814E40072023B /* PageListTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PageListTableViewCell.h; sourceTree = ""; }; - 5DFA7EC51AF814E40072023B /* PageListTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PageListTableViewCell.m; sourceTree = ""; }; - 5DFA7EC61AF814E40072023B /* PageListTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = PageListTableViewCell.xib; sourceTree = ""; }; 5E48AA7F709A5B0F2318A7E3 /* Pods-JetpackDraftActionExtension.release-internal.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JetpackDraftActionExtension.release-internal.xcconfig"; path = "../Pods/Target Support Files/Pods-JetpackDraftActionExtension/Pods-JetpackDraftActionExtension.release-internal.xcconfig"; sourceTree = ""; }; 67832AB9D81652460A80BE66 /* Pods-Apps-Jetpack.release-internal.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Apps-Jetpack.release-internal.xcconfig"; path = "../Pods/Target Support Files/Pods-Apps-Jetpack/Pods-Apps-Jetpack.release-internal.xcconfig"; sourceTree = ""; }; 6C1B070FAD875CA331772B57 /* Pods-JetpackStatsWidgets.release-alpha.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JetpackStatsWidgets.release-alpha.xcconfig"; path = "../Pods/Target Support Files/Pods-JetpackStatsWidgets/Pods-JetpackStatsWidgets.release-alpha.xcconfig"; sourceTree = ""; }; @@ -7092,7 +7112,6 @@ 738B9A4921B85CF20005062B /* ModelSettableCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModelSettableCell.swift; sourceTree = ""; }; 738B9A4A21B85CF20005062B /* TableDataCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableDataCoordinator.swift; sourceTree = ""; }; 738B9A4B21B85CF20005062B /* TitleSubtitleHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TitleSubtitleHeader.swift; sourceTree = ""; }; - 738B9A4C21B85CF20005062B /* KeyboardInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardInfo.swift; sourceTree = ""; }; 738B9A4D21B85CF20005062B /* SiteCreationHeaderData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SiteCreationHeaderData.swift; sourceTree = ""; }; 738B9A5B21B85EB00005062B /* UIView+ContentLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+ContentLayout.swift"; sourceTree = ""; }; 738B9A5D21B8632E0005062B /* UITableView+Header.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableView+Header.swift"; sourceTree = ""; }; @@ -7107,7 +7126,6 @@ 73C8F06521BEF76B00DDDF7E /* SiteAssemblyViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteAssemblyViewTests.swift; sourceTree = ""; }; 73C8F06721BF1A5E00DDDF7E /* SiteAssemblyContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteAssemblyContentView.swift; sourceTree = ""; }; 73CB13962289BEFB00265F49 /* Charts+LargeValueFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Charts+LargeValueFormatter.swift"; sourceTree = ""; }; - 73CE3E0D21F7F9D3007C9C85 /* TableViewOffsetCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewOffsetCoordinator.swift; sourceTree = ""; }; 73D5AC5C212622B200ADDDD2 /* NotificationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationViewController.swift; sourceTree = ""; }; 73D5AC662126236600ADDDD2 /* Info-Alpha.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Info-Alpha.plist"; sourceTree = ""; }; 73D5AC672126236600ADDDD2 /* Info-Internal.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Info-Internal.plist"; sourceTree = ""; }; @@ -7194,7 +7212,6 @@ 75305C06D345590B757E3890 /* Pods-Apps-WordPress.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Apps-WordPress.debug.xcconfig"; path = "../Pods/Target Support Files/Pods-Apps-WordPress/Pods-Apps-WordPress.debug.xcconfig"; sourceTree = ""; }; 7D21280C251CF0850086DD2C /* EditPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditPageViewController.swift; sourceTree = ""; }; 7D4D980C25FFE7E600C282E6 /* WordPress 116.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 116.xcdatamodel"; sourceTree = ""; }; - 7E14635620B3BEAB00B95F41 /* WPStyleGuide+Loader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WPStyleGuide+Loader.swift"; sourceTree = ""; }; 7E21C760202BBC8D00837CF5 /* iAd.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = iAd.framework; path = System/Library/Frameworks/iAd.framework; sourceTree = SDKROOT; }; 7E21C764202BBF4400837CF5 /* SearchAdsAttribution.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SearchAdsAttribution.swift; path = iAds/SearchAdsAttribution.swift; sourceTree = ""; }; 7E3AB3DA20F52654001F33B6 /* ActivityContentStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityContentStyles.swift; sourceTree = ""; }; @@ -7209,7 +7226,6 @@ 7E3E7A6320E44ED60075D159 /* SubjectContentGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubjectContentGroup.swift; sourceTree = ""; }; 7E3E7A6520E44F200075D159 /* HeaderContentGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderContentGroup.swift; sourceTree = ""; }; 7E3E9B6F2177C9DC00FD5797 /* GutenbergViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GutenbergViewController.swift; sourceTree = ""; }; - 7E407120237163B8003627FA /* GutenbergStockPhotos.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GutenbergStockPhotos.swift; sourceTree = ""; }; 7E4071392372AD54003627FA /* GutenbergFilesAppMediaSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GutenbergFilesAppMediaSource.swift; sourceTree = ""; }; 7E40716123741375003627FA /* GutenbergNetworking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GutenbergNetworking.swift; sourceTree = ""; }; 7E4123AC20F4097900DF8486 /* FormattableContentFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FormattableContentFactory.swift; sourceTree = ""; }; @@ -7277,7 +7293,6 @@ 7EBB4125206C388100012D98 /* StockPhotosService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StockPhotosService.swift; sourceTree = ""; }; 7EC2116478565023EDB57703 /* Pods-JetpackShareExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JetpackShareExtension.release.xcconfig"; path = "../Pods/Target Support Files/Pods-JetpackShareExtension/Pods-JetpackShareExtension.release.xcconfig"; sourceTree = ""; }; 7EC9FE0A22C627DB00C5A888 /* PostEditorAnalyticsSessionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostEditorAnalyticsSessionTests.swift; sourceTree = ""; }; - 7ECD5B8020C4D823001AEBC5 /* MediaPreviewHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreviewHelper.swift; sourceTree = ""; }; 7EDAB3F320B046FE002D1A76 /* CircularProgressView+ActivityIndicatorType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CircularProgressView+ActivityIndicatorType.swift"; sourceTree = ""; }; 7EF2EE9F210A67B60007A76B /* notifications-unapproved-comment.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "notifications-unapproved-comment.json"; sourceTree = ""; }; 7EF9F65622F03C9200F79BBF /* SiteSettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteSettingsScreen.swift; sourceTree = ""; }; @@ -7300,6 +7315,9 @@ 801D9519291AC0B00051993E /* OverlayFrequencyTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayFrequencyTracker.swift; sourceTree = ""; }; 801D951C291ADB7E0051993E /* OverlayFrequencyTrackerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayFrequencyTrackerTests.swift; sourceTree = ""; }; 80293CF6284450AD0083F946 /* WordPress-Swift.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "WordPress-Swift.h"; sourceTree = ""; }; + 80348F2D2AF870A70045CCD3 /* AllDomainsAddDomainCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDomainsAddDomainCoordinator.swift; sourceTree = ""; }; + 80348F302AF87FEA0045CCD3 /* AllDomainsListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AllDomainsListViewController.swift; sourceTree = ""; }; + 80348F322AF880820045CCD3 /* DomainPurchaseChoicesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DomainPurchaseChoicesView.swift; sourceTree = ""; }; 80379C6D2A5C0D8F00D924AC /* PostTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostTests.swift; sourceTree = ""; }; 803BB9782959543D00B3F6D6 /* RootViewCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootViewCoordinator.swift; sourceTree = ""; }; 803BB97B2959559500B3F6D6 /* RootViewPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootViewPresenter.swift; sourceTree = ""; }; @@ -7353,7 +7371,6 @@ 809A91022A7A4C710063D4FA /* NotificationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationTests.swift; sourceTree = ""; }; 80A2153C29C35197002FE8EB /* StaticScreensTabBarWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticScreensTabBarWrapper.swift; sourceTree = ""; }; 80A2153F29CA68D5002FE8EB /* RemoteFeatureFlag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteFeatureFlag.swift; sourceTree = ""; }; - 80A2154229D1177A002FE8EB /* RemoteConfigDebugViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteConfigDebugViewController.swift; sourceTree = ""; }; 80A2154529D15B88002FE8EB /* RemoteConfigOverrideStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteConfigOverrideStore.swift; sourceTree = ""; }; 80B016CE27FEBDC900D15566 /* DashboardCardTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardCardTests.swift; sourceTree = ""; }; 80B016D02803AB9F00D15566 /* DashboardPostsListCardCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardPostsListCardCell.swift; sourceTree = ""; }; @@ -7372,6 +7389,8 @@ 80D9D04529F760C400FE3400 /* FailableDecodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FailableDecodable.swift; sourceTree = ""; }; 80D9D04829FC0D9000FE3400 /* NSMutableArray+NullableObjects.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSMutableArray+NullableObjects.h"; sourceTree = ""; }; 80D9D04929FC0D9000FE3400 /* NSMutableArray+NullableObjects.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSMutableArray+NullableObjects.m"; sourceTree = ""; }; + 80DB57912AF8B59B00C728FF /* RegisterDomainCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisterDomainCoordinator.swift; sourceTree = ""; }; + 80DB57972AF99E0900C728FF /* BlogListConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogListConfiguration.swift; sourceTree = ""; }; 80EF671E27F135EB0063B138 /* WhatIsNewViewAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhatIsNewViewAppearance.swift; sourceTree = ""; }; 80EF672127F160720063B138 /* DashboardCustomAnnouncementCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardCustomAnnouncementCell.swift; sourceTree = ""; }; 80EF672427F3D63B0063B138 /* DashboardStatsStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardStatsStackView.swift; sourceTree = ""; }; @@ -7520,7 +7539,6 @@ 8B260D7D2444FC9D0010F756 /* PostVisibilitySelectorViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostVisibilitySelectorViewController.swift; sourceTree = ""; }; 8B2D4F5227ECE089009B085C /* dashboard-200-without-posts.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "dashboard-200-without-posts.json"; sourceTree = ""; }; 8B2D4F5427ECE376009B085C /* BlogDashboardPostsParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogDashboardPostsParserTests.swift; sourceTree = ""; }; - 8B33BC9427A0C14C00DB5985 /* BlogDetailsViewController+QuickActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BlogDetailsViewController+QuickActions.swift"; sourceTree = ""; }; 8B36256525A60CCA00D7CCE3 /* BackupListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupListViewController.swift; sourceTree = ""; }; 8B3626F825A665E500D7CCE3 /* UIApplication+mainWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+mainWindow.swift"; sourceTree = ""; }; 8B3DECAA2388506400A459C2 /* SentryStartupEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryStartupEvent.swift; sourceTree = ""; }; @@ -7540,7 +7558,6 @@ 8B749E7125AF522900023F03 /* JetpackCapabilitiesService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackCapabilitiesService.swift; sourceTree = ""; }; 8B749E8F25AF8D2E00023F03 /* JetpackCapabilitiesServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackCapabilitiesServiceTests.swift; sourceTree = ""; }; 8B74A9A7268E3C68003511CE /* RewindStatus+multiSite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RewindStatus+multiSite.swift"; sourceTree = ""; }; - 8B7623372384373E00AB3EE7 /* PageListViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageListViewControllerTests.swift; sourceTree = ""; }; 8B7C97E225A8BFA2004A3373 /* JetpackActivityLogViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackActivityLogViewController.swift; sourceTree = ""; }; 8B7F25A624E6EDB4007D82CC /* TopicsCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopicsCollectionView.swift; sourceTree = ""; }; 8B7F51C824EED804008CF5B5 /* ReaderTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderTracker.swift; sourceTree = ""; }; @@ -7553,7 +7570,6 @@ 8B92D69527CD51FA001F5371 /* DashboardGhostCardCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardGhostCardCell.swift; sourceTree = ""; }; 8B93412E257029F50097D0AC /* FilterChipButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterChipButton.swift; sourceTree = ""; }; 8B93856D22DC08060010BF02 /* PageListSectionHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageListSectionHeaderView.swift; sourceTree = ""; }; - 8B939F4223832E5D00ACCB0F /* PostListViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostListViewControllerTests.swift; sourceTree = ""; }; 8B9E15DAF3E1A369E9BE3407 /* Pods-WordPressUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WordPressUITests.release.xcconfig"; path = "../Pods/Target Support Files/Pods-WordPressUITests/Pods-WordPressUITests.release.xcconfig"; sourceTree = ""; }; 8BA125EA27D8F5E4008B779F /* UIView+PinSubviewPriority.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+PinSubviewPriority.swift"; sourceTree = ""; }; 8BA77BCA2482C52A00E1EBBF /* ReaderCardDiscoverAttributionView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ReaderCardDiscoverAttributionView.xib; sourceTree = ""; }; @@ -7617,8 +7633,6 @@ 8F228848D5DEACE6798CE7E2 /* TimeZoneSearchHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimeZoneSearchHeaderView.swift; sourceTree = ""; }; 8F228AE62B771552F0F971BE /* TimeZoneSearchHeaderView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TimeZoneSearchHeaderView.xib; sourceTree = ""; }; 91138454228373EB00FB02B7 /* GutenbergVideoUploadProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GutenbergVideoUploadProcessor.swift; sourceTree = ""; }; - 912347182213484300BD9F97 /* GutenbergViewController+InformativeDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GutenbergViewController+InformativeDialog.swift"; sourceTree = ""; }; - 9123471A221449E200BD9F97 /* GutenbergInformativeDialogTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GutenbergInformativeDialogTests.swift; sourceTree = ""; }; 912347752216E27200BD9F97 /* GutenbergViewController+Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GutenbergViewController+Localization.swift"; sourceTree = ""; }; 9149D34BF5182F360C84EDB9 /* Pods-JetpackDraftActionExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JetpackDraftActionExtension.debug.xcconfig"; path = "../Pods/Target Support Files/Pods-JetpackDraftActionExtension/Pods-JetpackDraftActionExtension.debug.xcconfig"; sourceTree = ""; }; 91D8364021946EFB008340B2 /* GutenbergMediaPickerHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GutenbergMediaPickerHelper.swift; sourceTree = ""; }; @@ -7765,7 +7779,6 @@ 983002A722FA05D600F03DBB /* InsightsManagementViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsightsManagementViewController.swift; sourceTree = ""; }; 9833A29B257AE7CF006B8234 /* WordPress 105.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 105.xcdatamodel"; sourceTree = ""; }; 9835F16D25E492EE002EFF23 /* CommentsList.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = CommentsList.storyboard; sourceTree = ""; }; - 98390AC2254C984700868F0A /* Tracks+StatsWidgets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Tracks+StatsWidgets.swift"; sourceTree = ""; }; 983DBBA822125DD300753988 /* StatsTableFooter.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = StatsTableFooter.xib; sourceTree = ""; }; 983DBBA922125DD300753988 /* StatsTableFooter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatsTableFooter.swift; sourceTree = ""; }; 98458CB721A39D350025D232 /* StatsNoDataRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsNoDataRow.swift; sourceTree = ""; }; @@ -7870,11 +7883,9 @@ 98E5D4912620C2B40074A56A /* UserProfileSectionHeader.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = UserProfileSectionHeader.xib; sourceTree = ""; }; 98EB126920D2DC2500D2D5B5 /* NoResultsViewController+Model.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NoResultsViewController+Model.swift"; sourceTree = ""; }; 98ED5962265EBD0000A0B33E /* ReaderDetailLikesListController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderDetailLikesListController.swift; sourceTree = ""; }; - 98F1B1292111017900139493 /* NoResultsStockPhotosConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoResultsStockPhotosConfiguration.swift; sourceTree = ""; }; 98F4044E26BB69A000BBD8B9 /* WordPress 131.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 131.xcdatamodel"; sourceTree = ""; }; 98F537A622496CF300B334F9 /* SiteStatsTableHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteStatsTableHeaderView.swift; sourceTree = ""; }; 98F537A822496D0D00B334F9 /* SiteStatsTableHeaderView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SiteStatsTableHeaderView.xib; sourceTree = ""; }; - 98F93181239AF64800E4E96E /* ThisWeekWidgetStats.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThisWeekWidgetStats.swift; sourceTree = ""; }; 98FB6E9F23074CE5002DDC8D /* Common.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Common.xcconfig; sourceTree = ""; }; 98FBA05426B228CB004E610A /* WordPress 129.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 129.xcdatamodel"; sourceTree = ""; }; 98FCFC212231DF43006ECDD4 /* PostStatsTitleCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostStatsTitleCell.swift; sourceTree = ""; }; @@ -7891,7 +7902,6 @@ 9A19D440236C7C7500D393E5 /* StatsGhostTopHeaderCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = StatsGhostTopHeaderCell.xib; sourceTree = ""; }; 9A1A67A522B2AD4E00FF8422 /* CountriesMap.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CountriesMap.swift; path = "Classes/ViewRelated/Stats/Period Stats/Countries/CountriesMap.swift"; sourceTree = SOURCE_ROOT; }; 9A1DA2FB21370DD00082569B /* WordPress 80.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 80.xcdatamodel"; sourceTree = ""; }; - 9A22D9BF214A6BCA00BAEAF2 /* PageListTableViewHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageListTableViewHandler.swift; sourceTree = ""; }; 9A2B28E7219046ED00458F2A /* ShowRevisionsListManger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShowRevisionsListManger.swift; sourceTree = ""; }; 9A2B28ED2191B50500458F2A /* RevisionsTableViewFooter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RevisionsTableViewFooter.swift; sourceTree = ""; }; 9A2B28F42192121400458F2A /* RevisionOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RevisionOperation.swift; sourceTree = ""; }; @@ -8002,6 +8012,7 @@ B0B89DBF2A1E882F003D5295 /* DomainResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainResultView.swift; sourceTree = ""; }; B0CD27CE286F8858009500BF /* JetpackBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackBannerView.swift; sourceTree = ""; }; B0DDC2EB252F7C4F002BAFB3 /* WordPress 100.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 100.xcdatamodel"; sourceTree = ""; }; + B0DE91B42AF9778200D51A02 /* DomainSetupNoticeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainSetupNoticeView.swift; sourceTree = ""; }; B0F2EFBE259378E600C7EB6D /* SiteSuggestionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteSuggestionService.swift; sourceTree = ""; }; B124AFFFB3F0204107FD33D0 /* Pods-JetpackIntents.release-alpha.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JetpackIntents.release-alpha.xcconfig"; path = "../Pods/Target Support Files/Pods-JetpackIntents/Pods-JetpackIntents.release-alpha.xcconfig"; sourceTree = ""; }; B3694C30079615C927D26E9F /* Pods-JetpackStatsWidgets.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JetpackStatsWidgets.release.xcconfig"; path = "../Pods/Target Support Files/Pods-JetpackStatsWidgets/Pods-JetpackStatsWidgets.release.xcconfig"; sourceTree = ""; }; @@ -8066,7 +8077,6 @@ B54E1DEE1A0A7BAA00807537 /* ReplyTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReplyTextView.swift; sourceTree = ""; }; B54E1DEF1A0A7BAA00807537 /* ReplyTextView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ReplyTextView.xib; sourceTree = ""; }; B54E1DF31A0A7BBF00807537 /* NotificationMediaDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationMediaDownloader.swift; sourceTree = ""; }; - B55086201CC15CCB004EADB4 /* PromptViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PromptViewController.swift; sourceTree = ""; }; B5552D7D1CD101A600B26DF6 /* NSExtensionContext+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "NSExtensionContext+Extensions.swift"; path = "WordPressShareExtension/NSExtensionContext+Extensions.swift"; sourceTree = SOURCE_ROOT; }; B5552D7F1CD1028C00B26DF6 /* String+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "String+Extensions.swift"; path = "WordPressShareExtension/String+Extensions.swift"; sourceTree = SOURCE_ROOT; }; B5552D811CD1061F00B26DF6 /* StringExtensionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringExtensionsTests.swift; sourceTree = ""; }; @@ -8239,7 +8249,6 @@ C700FAB1258020DB0090938E /* JetpackScanThreatCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = JetpackScanThreatCell.xib; sourceTree = ""; }; C7124E4C2638528F00929318 /* JetpackPrologueViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = JetpackPrologueViewController.xib; sourceTree = ""; }; C7124E4D2638528F00929318 /* JetpackPrologueViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JetpackPrologueViewController.swift; sourceTree = ""; }; - C7124E912638905B00929318 /* StarFieldView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StarFieldView.swift; sourceTree = ""; }; C7192ECE25E8432D00C3020D /* ReaderTopicsCardCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ReaderTopicsCardCell.xib; sourceTree = ""; }; C71AF532281064DE00F9E99E /* OnboardingQuestionsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingQuestionsCoordinator.swift; sourceTree = ""; }; C71BC73E25A652410023D789 /* JetpackScanStatusViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackScanStatusViewModel.swift; sourceTree = ""; }; @@ -8248,9 +8257,6 @@ C7234A412832C2BA0045C63F /* QRLoginScanningViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = QRLoginScanningViewController.xib; sourceTree = ""; }; C7234A4C2832C47D0045C63F /* QRLoginVerifyAuthorizationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRLoginVerifyAuthorizationViewController.swift; sourceTree = ""; }; C7234A4D2832C47D0045C63F /* QRLoginVerifyAuthorizationViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = QRLoginVerifyAuthorizationViewController.xib; sourceTree = ""; }; - C72A4F67264088E4009CA633 /* JetpackNotFoundErrorViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackNotFoundErrorViewModel.swift; sourceTree = ""; }; - C72A4F7A26408943009CA633 /* JetpackNotWPErrorViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackNotWPErrorViewModel.swift; sourceTree = ""; }; - C72A4F8D26408C73009CA633 /* JetpackNoSitesErrorViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackNoSitesErrorViewModel.swift; sourceTree = ""; }; C72A52CE2649B157009CA633 /* JetpackWindowManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackWindowManager.swift; sourceTree = ""; }; C737553D27C80DD500C6E9A1 /* String+CondenseWhitespace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+CondenseWhitespace.swift"; sourceTree = ""; }; C73868C425C9F9820072532C /* JetpackScanThreatSectionGrouping.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JetpackScanThreatSectionGrouping.swift; sourceTree = ""; }; @@ -8293,9 +8299,6 @@ C7E5F2592799C2B0009BC263 /* blue-icon-app-83.5x83.5@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "blue-icon-app-83.5x83.5@2x.png"; sourceTree = ""; }; C7F1EB4425A4B845009D1AA2 /* WordPress 110.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 110.xcdatamodel"; sourceTree = ""; }; C7F7ABD5261CED7A00CE547F /* JetpackAuthenticationManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JetpackAuthenticationManager.swift; sourceTree = ""; }; - C7F7AC73261CF1F300CE547F /* JetpackLoginErrorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackLoginErrorViewController.swift; sourceTree = ""; }; - C7F7AC74261CF1F300CE547F /* JetpackLoginErrorViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = JetpackLoginErrorViewController.xib; sourceTree = ""; }; - C7F7ACBD261E4F0600CE547F /* JetpackErrorViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackErrorViewModel.swift; sourceTree = ""; }; C7F7BDBC26262A1B00CE547F /* AppDependency.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDependency.swift; sourceTree = ""; }; C7F7BDCF26262A4C00CE547F /* AppDependency.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDependency.swift; sourceTree = ""; }; C7F7BE0626262B9900CE547F /* AuthenticationHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationHandler.swift; sourceTree = ""; }; @@ -8311,15 +8314,12 @@ C81CCD72243BF7A500A83E27 /* TenorPageable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TenorPageable.swift; sourceTree = ""; }; C81CCD73243BF7A500A83E27 /* TenorDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TenorDataSource.swift; sourceTree = ""; }; C81CCD74243BF7A500A83E27 /* TenorMedia.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TenorMedia.swift; sourceTree = ""; }; - C81CCD75243BF7A500A83E27 /* TenorMediaGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TenorMediaGroup.swift; sourceTree = ""; }; - C81CCD76243BF7A600A83E27 /* TenorPicker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TenorPicker.swift; sourceTree = ""; }; C81CCD77243BF7A600A83E27 /* TenorService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TenorService.swift; sourceTree = ""; }; C81CCD78243BF7A600A83E27 /* TenorResultsPage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TenorResultsPage.swift; sourceTree = ""; }; C81CCD79243BF7A600A83E27 /* TenorDataLoader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TenorDataLoader.swift; sourceTree = ""; }; - C81CCD7A243BF7A600A83E27 /* NoResultsTenorConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoResultsTenorConfiguration.swift; sourceTree = ""; }; C81CCD85243C00E000A83E27 /* TenorPageableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TenorPageableTests.swift; sourceTree = ""; }; C82B4C5ECF11C9FEE39CD9A0 /* Pods-WordPressShareExtension.release-internal.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WordPressShareExtension.release-internal.xcconfig"; path = "../Pods/Target Support Files/Pods-WordPressShareExtension/Pods-WordPressShareExtension.release-internal.xcconfig"; sourceTree = ""; }; - C856748E243EF177001A995E /* GutenbergTenorMediaPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GutenbergTenorMediaPicker.swift; sourceTree = ""; }; + C856748E243EF177001A995E /* GutenbergExternalMeidaPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GutenbergExternalMeidaPicker.swift; sourceTree = ""; }; C8567491243F3751001A995E /* tenor-search-response.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "tenor-search-response.json"; sourceTree = ""; }; C8567493243F388F001A995E /* tenor-invalid-search-reponse.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "tenor-invalid-search-reponse.json"; sourceTree = ""; }; C8567495243F3D37001A995E /* TenorResultsPageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TenorResultsPageTests.swift; sourceTree = ""; }; @@ -8329,14 +8329,11 @@ C8FC2DE857126670AE377B5D /* Pods-WordPressScreenshotGeneration.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WordPressScreenshotGeneration.debug.xcconfig"; path = "../Pods/Target Support Files/Pods-WordPressScreenshotGeneration/Pods-WordPressScreenshotGeneration.debug.xcconfig"; sourceTree = ""; }; C9264D275F6288F66C33D2CE /* Pods-WordPressTest.release-internal.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WordPressTest.release-internal.xcconfig"; path = "../Pods/Target Support Files/Pods-WordPressTest/Pods-WordPressTest.release-internal.xcconfig"; sourceTree = ""; }; C94C0B1A25DCFA0100F2F69B /* FilterableCategoriesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterableCategoriesViewController.swift; sourceTree = ""; }; - C995C22129D306DD00ACEF43 /* URL+WidgetSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+WidgetSource.swift"; sourceTree = ""; }; - C995C22529D30AB000ACEF43 /* WidgetUrlSourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetUrlSourceTests.swift; sourceTree = ""; }; C99B039B2602F3CB00CA71EB /* WordPress 117.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 117.xcdatamodel"; sourceTree = ""; }; C99B08FB26081AD600CA71EB /* TemplatePreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplatePreviewViewController.swift; sourceTree = ""; }; C9B4778329C85949008CBF49 /* LockScreenStatsWidgetEntry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockScreenStatsWidgetEntry.swift; sourceTree = ""; }; C9B477A729CC13C6008CBF49 /* LockScreenSiteListProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockScreenSiteListProvider.swift; sourceTree = ""; }; C9B477AB29CC15D9008CBF49 /* WidgetDataReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WidgetDataReader.swift; sourceTree = ""; }; - C9B477AF29CC35C5008CBF49 /* WidgetDataReaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetDataReaderTests.swift; sourceTree = ""; }; C9B477B129CC4949008CBF49 /* HomeWidgetDataFileReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeWidgetDataFileReader.swift; sourceTree = ""; }; C9B477B529CD2EF7008CBF49 /* LockScreenUnconfiguredView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockScreenUnconfiguredView.swift; sourceTree = ""; }; C9B477B829CD2FEE008CBF49 /* LockScreenUnconfiguredViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockScreenUnconfiguredViewModel.swift; sourceTree = ""; }; @@ -8397,9 +8394,6 @@ D67306CD28F2440FF6B0065C /* Pods-JetpackIntents.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JetpackIntents.debug.xcconfig"; path = "../Pods/Target Support Files/Pods-JetpackIntents/Pods-JetpackIntents.debug.xcconfig"; sourceTree = ""; }; D8071630203DA23700B32FD9 /* Accessible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Accessible.swift; sourceTree = ""; }; D809E685203F0215001AA0DE /* OldReaderPostCardCellTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OldReaderPostCardCellTests.swift; sourceTree = ""; }; - D80BC79B207464D200614A59 /* MediaLibraryMediaPickingCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaLibraryMediaPickingCoordinator.swift; sourceTree = ""; }; - D80BC79D20746B4100614A59 /* MediaPickingContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPickingContext.swift; sourceTree = ""; }; - D80BC7A12074739300614A59 /* MediaLibraryStrings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaLibraryStrings.swift; sourceTree = ""; }; D81322B22050F9110067714D /* NotificationName+Names.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationName+Names.swift"; sourceTree = ""; }; D813D67E21AA8BBF0055CCA1 /* ShadowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowView.swift; sourceTree = ""; }; D8160441209C1B0F00ABAFFA /* ReaderSaveForLaterAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderSaveForLaterAction.swift; sourceTree = ""; }; @@ -8438,7 +8432,6 @@ D821C818210037F8002ED995 /* activity-log-activity-content.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "activity-log-activity-content.json"; sourceTree = ""; }; D821C81A21003AE9002ED995 /* FormattableContentGroupTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormattableContentGroupTests.swift; sourceTree = ""; }; D82253DB2199411F0014D0E2 /* SiteAddressService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteAddressService.swift; sourceTree = ""; }; - D82253DD2199418B0014D0E2 /* WebAddressWizardContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebAddressWizardContent.swift; sourceTree = ""; }; D82253E3219956540014D0E2 /* AddressTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressTableViewCell.swift; sourceTree = ""; }; D8225407219AB0520014D0E2 /* SiteInformation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SiteInformation.swift; sourceTree = ""; }; D826D67E211D21C700A5D8FE /* NullMockUserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NullMockUserDefaults.swift; sourceTree = ""; }; @@ -8486,14 +8479,11 @@ D88106F920C0CFEE001D2F00 /* ReaderSaveForLaterRemovedPosts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderSaveForLaterRemovedPosts.swift; sourceTree = ""; }; D88A6491208D7A0A008AE9BC /* MockStockPhotosService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockStockPhotosService.swift; sourceTree = ""; }; D88A6493208D7AD0008AE9BC /* DefaultStockPhotosService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultStockPhotosService.swift; sourceTree = ""; }; - D88A6495208D7B0B008AE9BC /* NullStockPhotosService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NullStockPhotosService.swift; sourceTree = ""; }; - D88A649B208D7D81008AE9BC /* StockPhotosDataSourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StockPhotosDataSourceTests.swift; sourceTree = ""; }; D88A649D208D82D2008AE9BC /* XCTestCase+Wait.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTestCase+Wait.swift"; sourceTree = ""; }; - D88A649F208D8B7D008AE9BC /* StockPhotosMediaGroupTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StockPhotosMediaGroupTests.swift; sourceTree = ""; }; D88A64A1208D8F05008AE9BC /* StockPhotosMediaTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StockPhotosMediaTests.swift; sourceTree = ""; }; D88A64A3208D8FB6008AE9BC /* stock-photos-search-response.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "stock-photos-search-response.json"; sourceTree = ""; }; D88A64A5208D92B1008AE9BC /* stock-photos-media.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "stock-photos-media.json"; sourceTree = ""; }; - D88A64A7208D9733008AE9BC /* ThumbnailCollectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThumbnailCollectionTests.swift; sourceTree = ""; }; + D88A64A7208D9733008AE9BC /* StockPhotosThumbnailCollectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StockPhotosThumbnailCollectionTests.swift; sourceTree = ""; }; D88A64A9208D974D008AE9BC /* thumbnail-collection.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "thumbnail-collection.json"; sourceTree = ""; }; D88A64AB208D9B09008AE9BC /* StockPhotosPageableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StockPhotosPageableTests.swift; sourceTree = ""; }; D88A64AD208D9CF5008AE9BC /* stock-photos-pageable.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "stock-photos-pageable.json"; sourceTree = ""; }; @@ -8501,11 +8491,8 @@ D8A3A5A92069E53900992576 /* AztecMediaPickingCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AztecMediaPickingCoordinator.swift; sourceTree = ""; }; D8A3A5AB2069FE5B00992576 /* StockPhotosStrings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StockPhotosStrings.swift; sourceTree = ""; }; D8A3A5AE206A442800992576 /* StockPhotosDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StockPhotosDataSource.swift; sourceTree = ""; }; - D8A3A5B0206A49A100992576 /* StockPhotosMediaGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StockPhotosMediaGroup.swift; sourceTree = ""; }; D8A3A5B2206A49BF00992576 /* StockPhotosMedia.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StockPhotosMedia.swift; sourceTree = ""; }; - D8A3A5B4206A4C7800992576 /* StockPhotosPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StockPhotosPicker.swift; sourceTree = ""; }; D8A468DF2181C6450094B82F /* site-segment.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "site-segment.json"; sourceTree = ""; }; - D8A468E421828D940094B82F /* SiteVerticalsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SiteVerticalsService.swift; sourceTree = ""; }; D8B6BEB6203E11F2007C8A19 /* Bundle+LoadFromNib.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+LoadFromNib.swift"; sourceTree = ""; }; D8B9B58E204F4EA1003C6042 /* NetworkAware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkAware.swift; sourceTree = ""; }; D8B9B592204F6C93003C6042 /* CommentsViewController+Network.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CommentsViewController+Network.h"; sourceTree = ""; }; @@ -8738,7 +8725,6 @@ E1EBC3721C118ED200F638E0 /* ImmuTableTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImmuTableTest.swift; sourceTree = ""; }; E1EBC3741C118EDE00F638E0 /* ImmuTableTestViewCellWithNib.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ImmuTableTestViewCellWithNib.xib; sourceTree = ""; }; E1ECE34E1FA88DA2007FA37A /* StoreContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreContainer.swift; sourceTree = ""; }; - E1EEFAD91CC4CC5700126533 /* Confirmable.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Confirmable.h; sourceTree = ""; }; E1F47D4C1FE0290C00C1D44E /* PluginListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginListCell.swift; sourceTree = ""; }; E1FD45DF1C030B3800750F4C /* AccountSettingsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountSettingsService.swift; sourceTree = ""; }; E240859A183D82AE002EB0EF /* WPAnimatedBox.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WPAnimatedBox.h; sourceTree = ""; }; @@ -8808,8 +8794,6 @@ E6805D2D1DCD399600168E4F /* WPRichTextEmbed.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = WPRichTextEmbed.swift; path = WPRichText/WPRichTextEmbed.swift; sourceTree = ""; }; E6805D2E1DCD399600168E4F /* WPRichTextImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = WPRichTextImage.swift; path = WPRichText/WPRichTextImage.swift; sourceTree = ""; }; E6805D2F1DCD399600168E4F /* WPRichTextMediaAttachment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = WPRichTextMediaAttachment.swift; path = WPRichText/WPRichTextMediaAttachment.swift; sourceTree = ""; }; - E684383D221F535900752258 /* LoadMoreCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadMoreCounter.swift; sourceTree = ""; }; - E684383F221F5A2200752258 /* PostListExcessiveLoadMoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostListExcessiveLoadMoreTests.swift; sourceTree = ""; }; E68580F51E0D91470090EE63 /* WPHorizontalRuleAttachment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WPHorizontalRuleAttachment.swift; sourceTree = ""; }; E687A0AE249AC02400C8BA18 /* WordPress 97.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 97.xcdatamodel"; sourceTree = ""; }; E690F6EC25E04EAA0015A777 /* WordPress 113.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 113.xcdatamodel"; sourceTree = ""; }; @@ -8817,8 +8801,6 @@ E690F6EE25E05D180015A777 /* InviteLinks+CoreDataProperties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "InviteLinks+CoreDataProperties.swift"; sourceTree = ""; }; E69551F51B8B6AE200CB8E4F /* ReaderStreamViewController+Helper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ReaderStreamViewController+Helper.swift"; sourceTree = ""; }; E699E530210BB719008ED8A7 /* WordPress 77.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 77.xcdatamodel"; sourceTree = ""; }; - E69BA1961BB5D7D300078740 /* WPStyleGuide+ReadableMargins.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "WPStyleGuide+ReadableMargins.h"; sourceTree = ""; }; - E69BA1971BB5D7D300078740 /* WPStyleGuide+ReadableMargins.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "WPStyleGuide+ReadableMargins.m"; sourceTree = ""; }; E6A2158F1D1065F200DE5270 /* AbstractPostTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = AbstractPostTest.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; E6A3384A1BB08E3F00371587 /* ReaderGapMarker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ReaderGapMarker.h; sourceTree = ""; }; E6A3384B1BB08E3F00371587 /* ReaderGapMarker.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ReaderGapMarker.m; sourceTree = ""; }; @@ -8939,9 +8921,20 @@ F373612EEEEF10E500093FF3 /* Pods-Apps-WordPress.release-alpha.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Apps-WordPress.release-alpha.xcconfig"; path = "../Pods/Target Support Files/Pods-Apps-WordPress/Pods-Apps-WordPress.release-alpha.xcconfig"; sourceTree = ""; }; F4026B1C2A1BC88A00CC7781 /* DashboardDomainRegistrationCardCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardDomainRegistrationCardCell.swift; sourceTree = ""; }; F40CC35C2954991C00D75A95 /* WordPress 146.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 146.xcdatamodel"; sourceTree = ""; }; + F413F7792B2A183E00A64A94 /* BlogDashboardDynamicCardCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogDashboardDynamicCardCell.swift; sourceTree = ""; }; + F413F7872B2B253A00A64A94 /* DashboardCard+Personalization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DashboardCard+Personalization.swift"; sourceTree = ""; }; + F4141EE22AE7152F000D2AAE /* AllDomainsListViewController+Strings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AllDomainsListViewController+Strings.swift"; sourceTree = ""; }; + F4141EE52AE71AF0000D2AAE /* AllDomainsListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDomainsListViewModel.swift; sourceTree = ""; }; + F4141EE72AE72DC4000D2AAE /* AllDomainsListTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDomainsListTableViewCell.swift; sourceTree = ""; }; + F4141EE92AE74ADA000D2AAE /* AllDomainsListActivityIndicatorTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDomainsListActivityIndicatorTableViewCell.swift; sourceTree = ""; }; + F4141EEB2AE945C7000D2AAE /* AllDomainsListItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDomainsListItemViewModel.swift; sourceTree = ""; }; F41BDD72290BBDCA00B7F2B0 /* MigrationActionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationActionsView.swift; sourceTree = ""; }; F41BDD782910AFCA00B7F2B0 /* MigrationFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationFlowCoordinator.swift; sourceTree = ""; }; F41BDD7A29114E2400B7F2B0 /* MigrationStep.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationStep.swift; sourceTree = ""; }; + F41D98D62B389735004EC050 /* DashboardDynamicCardAnalyticsEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardDynamicCardAnalyticsEvent.swift; sourceTree = ""; }; + F41D98E02B39C5CE004EC050 /* BlogDashboardDynamicCardCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogDashboardDynamicCardCoordinatorTests.swift; sourceTree = ""; }; + F41D98E22B39C9E7004EC050 /* BlogDashboardDynamicCardCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogDashboardDynamicCardCoordinator.swift; sourceTree = ""; }; + F41D98E72B39E14F004EC050 /* DashboardDynamicCardAnalyticsEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardDynamicCardAnalyticsEventTests.swift; sourceTree = ""; }; F41E32FD287B47A500F89082 /* SuggestionsListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionsListViewModel.swift; sourceTree = ""; }; F41E3300287B5FE500F89082 /* SuggestionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionViewModel.swift; sourceTree = ""; }; F41E4E8B28F18B7B001880C6 /* AppIconListViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconListViewModelTests.swift; sourceTree = ""; }; @@ -9001,6 +8994,11 @@ F44F6ABD2937428B00DC94A2 /* MigrationEmailService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationEmailService.swift; sourceTree = ""; }; F44FB6CA287895AF0001E3CE /* SuggestionsListViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionsListViewModelTests.swift; sourceTree = ""; }; F44FB6D02878A1020001E3CE /* user-suggestions.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "user-suggestions.json"; sourceTree = ""; }; + F46546282AED89790017E3D1 /* AllDomainsListEmptyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDomainsListEmptyView.swift; sourceTree = ""; }; + F465462C2AEF22070017E3D1 /* AllDomainsListViewModel+Strings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AllDomainsListViewModel+Strings.swift"; sourceTree = ""; }; + F46546302AF2F8D20017E3D1 /* DomainsStateViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DomainsStateViewModel.swift; sourceTree = ""; }; + F46546322AF54DCD0017E3D1 /* AllDomainsListItemViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDomainsListItemViewModelTests.swift; sourceTree = ""; }; + F46546342AF550A20017E3D1 /* AllDomainsListItem+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AllDomainsListItem+Helpers.swift"; sourceTree = ""; }; F465976928E4669200D5F49A /* cool-green-icon-app-76@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "cool-green-icon-app-76@2x.png"; sourceTree = ""; }; F465976A28E4669200D5F49A /* cool-green-icon-app-76.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "cool-green-icon-app-76.png"; sourceTree = ""; }; F465976B28E4669200D5F49A /* cool-green-icon-app-60@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "cool-green-icon-app-60@3x.png"; sourceTree = ""; }; @@ -9072,22 +9070,28 @@ F465980628E66A5A00D5F49A /* white-on-blue-icon-app-60@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "white-on-blue-icon-app-60@2x.png"; sourceTree = ""; }; F465980728E66A5B00D5F49A /* white-on-blue-icon-app-83.5@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "white-on-blue-icon-app-83.5@2x.png"; sourceTree = ""; }; F478B151292FC1BC00AA8645 /* MigrationAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationAppearance.swift; sourceTree = ""; }; + F479995D2AFD241E0023F4FB /* RegisterDomainTransferFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisterDomainTransferFooterView.swift; sourceTree = ""; }; F47E154929E84A9300B6E426 /* SiteCreationPurchasingWebFlowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteCreationPurchasingWebFlowController.swift; sourceTree = ""; }; F484D4E92A32B51C0050BE15 /* RootViewPresenter+AppSettingsNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RootViewPresenter+AppSettingsNavigation.swift"; sourceTree = ""; }; F484D4EC2A32C4520050BE15 /* CATransaction+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CATransaction+Extension.swift"; sourceTree = ""; }; F48D44B5298992C30051EAA6 /* BlockedSite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedSite.swift; sourceTree = ""; }; F48D44B7298993900051EAA6 /* WordPress 147.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 147.xcdatamodel"; sourceTree = ""; }; F48D44B92989A58C0051EAA6 /* ReaderSiteService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderSiteService.swift; sourceTree = ""; }; + F48EBF892B2F94DD004CD561 /* BlogDashboardAnalyticPropertiesProviding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogDashboardAnalyticPropertiesProviding.swift; sourceTree = ""; }; + F48EBF8C2B3262D5004CD561 /* dashboard-200-with-multiple-dynamic-cards.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "dashboard-200-with-multiple-dynamic-cards.json"; sourceTree = ""; }; + F48EBF912B333111004CD561 /* dashboard-200-with-only-one-dynamic-card.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "dashboard-200-with-only-one-dynamic-card.json"; sourceTree = ""; }; F49B99FE2937C9B4000CEFCE /* MigrationEmailService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MigrationEmailService.swift; sourceTree = ""; }; F49B9A05293A21BF000CEFCE /* MigrationAnalyticsTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationAnalyticsTracker.swift; sourceTree = ""; }; F49B9A07293A21F4000CEFCE /* MigrationEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationEvent.swift; sourceTree = ""; }; F49D7BEA29DF329500CB93A5 /* UIPopoverPresentationController+PopoverAnchor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIPopoverPresentationController+PopoverAnchor.swift"; sourceTree = ""; }; + F4B0F4822ADED9B5003ABC61 /* DomainsService+AllDomains.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DomainsService+AllDomains.swift"; sourceTree = ""; }; F4BECD1A288EE5220078391A /* SuggestionsViewModelType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SuggestionsViewModelType.swift; sourceTree = ""; }; F4C1FC622A44831300AD7CB0 /* PrivacySettingsAnalyticsTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacySettingsAnalyticsTracker.swift; sourceTree = ""; }; F4C1FC652A44836300AD7CB0 /* PrivacySettingsAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacySettingsAnalytics.swift; sourceTree = ""; }; F4CBE3D329258AD6004FFBB6 /* MeHeaderView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MeHeaderView.h; sourceTree = ""; }; F4CBE3D5292597E3004FFBB6 /* SupportTableViewControllerConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportTableViewControllerConfiguration.swift; sourceTree = ""; }; F4CBE3D829265BC8004FFBB6 /* LogOutActionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogOutActionHandler.swift; sourceTree = ""; }; + F4D1401F2AFD9B9700961797 /* TransferDomainsWebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferDomainsWebViewController.swift; sourceTree = ""; }; F4D36AD4298498E600E6B84C /* ReaderPostBlockingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderPostBlockingController.swift; sourceTree = ""; }; F4D7FD6B2A57030E00642E06 /* CompliancePopoverViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompliancePopoverViewControllerTests.swift; sourceTree = ""; }; F4D829612930E9F300038726 /* MigrationDeleteWordPressViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationDeleteWordPressViewController.swift; sourceTree = ""; }; @@ -9109,6 +9113,8 @@ F4EDAA4B29A516E900622D3D /* ReaderPostService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderPostService.swift; sourceTree = ""; }; F4EF4BAA291D3D4700147B61 /* SiteIconTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteIconTests.swift; sourceTree = ""; }; F4F09CCB2989BF5B00A5F420 /* ReaderSiteService_Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ReaderSiteService_Internal.h; sourceTree = ""; }; + F4F7B2502AF8EBDB00207282 /* DomainDetailsWebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainDetailsWebViewController.swift; sourceTree = ""; }; + F4F7B2522AFA585700207282 /* DomainDetailsWebViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainDetailsWebViewControllerTests.swift; sourceTree = ""; }; F4F9D5E92909622E00502576 /* MigrationWelcomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationWelcomeViewController.swift; sourceTree = ""; }; F4F9D5EB29096CF500502576 /* MigrationHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationHeaderView.swift; sourceTree = ""; }; F4F9D5F1290993D400502576 /* MigrationWelcomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationWelcomeViewModel.swift; sourceTree = ""; }; @@ -9123,7 +9129,6 @@ F52CACCB24512EA700661380 /* EmptyActionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyActionView.swift; sourceTree = ""; }; F532AD60253B81320013B42E /* StoriesIntroDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoriesIntroDataSource.swift; sourceTree = ""; }; F532AE1B253E55D40013B42E /* CreateButtonActionSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateButtonActionSheet.swift; sourceTree = ""; }; - F53FF3A723EA723D001AD596 /* ActionRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionRow.swift; sourceTree = ""; }; F53FF3A923EA725C001AD596 /* SiteIconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteIconView.swift; sourceTree = ""; }; F543AF5623A84E4D0022F595 /* PublishSettingsControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublishSettingsControllerTests.swift; sourceTree = ""; }; F551E7F423F6EA3100751212 /* FloatingActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingActionButton.swift; sourceTree = ""; }; @@ -9137,7 +9142,6 @@ F580C3C023D22E2D0038E243 /* PreviewDeviceLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewDeviceLabel.swift; sourceTree = ""; }; F580C3CA23D8F9B40038E243 /* AbstractPost+Dates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AbstractPost+Dates.swift"; sourceTree = ""; }; F582060123A85495005159A9 /* SiteDateFormatters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteDateFormatters.swift; sourceTree = ""; }; - F5844B6A235EAF3D007C6557 /* PartScreenPresentationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PartScreenPresentationController.swift; sourceTree = ""; }; F59AAC0F235E430E00385EE6 /* ChosenValueRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChosenValueRow.swift; sourceTree = ""; }; F59AAC15235EA46D00385EE6 /* LightNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LightNavigationController.swift; sourceTree = ""; }; F5A34A9825DEF47D00C9654B /* WPMediaPicker+MediaPicker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "WPMediaPicker+MediaPicker.swift"; sourceTree = ""; }; @@ -9194,6 +9198,9 @@ F9C47A8E238C9D6400AAD9ED /* StatsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsScreen.swift; sourceTree = ""; }; FA00863C24EB68B100C863F2 /* FollowCommentsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowCommentsService.swift; sourceTree = ""; }; FA111E372A2F38FC00896FCE /* BlazeCampaignsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeCampaignsViewController.swift; sourceTree = ""; }; + FA141F262AEC1D9E00C9A653 /* PageMenuViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageMenuViewModel.swift; sourceTree = ""; }; + FA141F292AEC23E300C9A653 /* PageListViewController+Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PageListViewController+Menu.swift"; sourceTree = ""; }; + FA141F312AF139A200C9A653 /* PageMenuViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = PageMenuViewModelTests.swift; path = Classes/ViewRelated/Pages/PageMenuViewModelTests.swift; sourceTree = SOURCE_ROOT; }; FA1A543D25A6E2F60033967D /* RestoreWarningView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestoreWarningView.swift; sourceTree = ""; }; FA1A543F25A6E3080033967D /* RestoreWarningView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = RestoreWarningView.xib; sourceTree = ""; }; FA1A55EE25A6F0740033967D /* RestoreStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestoreStatusView.swift; sourceTree = ""; }; @@ -9267,6 +9274,7 @@ FAA9084B27BD60710093FFA8 /* MySiteViewController+QuickStart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MySiteViewController+QuickStart.swift"; sourceTree = ""; }; FAADE3F02615996E00BF29FE /* AppConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConstants.swift; sourceTree = ""; }; FAADE42726159B1300BF29FE /* AppConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConstants.swift; sourceTree = ""; }; + FAAEFADF2B1E29F0004AE802 /* SitePickerViewController+SiteActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SitePickerViewController+SiteActions.swift"; sourceTree = ""; }; FAB37D4527ED84BC00CA993C /* DashboardStatsNudgeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardStatsNudgeView.swift; sourceTree = ""; }; FAB4F32624EDE12A00F259BA /* FollowCommentsServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowCommentsServiceTests.swift; sourceTree = ""; }; FAB8004825AEDC2300D5D54A /* JetpackBackupCompleteViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackBackupCompleteViewController.swift; sourceTree = ""; }; @@ -9292,11 +9300,15 @@ FAC1B81D29B0C2AC00E0C542 /* BlazeOverlayViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeOverlayViewModel.swift; sourceTree = ""; }; FAC1B82629B1F1EE00E0C542 /* BlazePostPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazePostPreviewView.swift; sourceTree = ""; }; FACB36F01C5C2BF800C6DF4E /* ThemeWebNavigationDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThemeWebNavigationDelegate.swift; sourceTree = ""; }; + FACF66C92ADD4703008C3E13 /* PostListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostListCell.swift; sourceTree = ""; }; + FACF66CC2ADD645C008C3E13 /* PostListHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostListHeaderView.swift; sourceTree = ""; }; + FACF66CF2ADD6CD8008C3E13 /* PostListItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostListItemViewModel.swift; sourceTree = ""; }; FAD1263B2A0CF2F50004E24C /* String+NonbreakingSpace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+NonbreakingSpace.swift"; sourceTree = ""; }; FAD2538E26116A1600EDAF88 /* AppStyleGuide.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStyleGuide.swift; sourceTree = ""; }; FAD2544126116CEA00EDAF88 /* AppStyleGuide.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStyleGuide.swift; sourceTree = ""; }; FAD256922611B01700EDAF88 /* UIColor+WordPressColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+WordPressColors.swift"; sourceTree = ""; }; FAD257112611B04D00EDAF88 /* UIColor+JetpackColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+JetpackColors.swift"; sourceTree = ""; }; + FAD3DE802AE2965A00A3B031 /* AbstractPostMenuHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbstractPostMenuHelper.swift; sourceTree = ""; }; FAD7625A29ED780B00C09583 /* JSONDecoderExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONDecoderExtension.swift; sourceTree = ""; }; FAD7626329F1480E00C09583 /* DashboardActivityLogCardCell+ActivityPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DashboardActivityLogCardCell+ActivityPresenter.swift"; sourceTree = ""; }; FAD9457D25B5647B00F011B5 /* JetpackBackupOptionsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackBackupOptionsCoordinator.swift; sourceTree = ""; }; @@ -9316,6 +9328,7 @@ FAE4CA672732C094003BFDFE /* QuickStartPromptViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = QuickStartPromptViewController.xib; sourceTree = ""; }; FAE8EE98273AC06F00A65307 /* QuickStartSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickStartSettings.swift; sourceTree = ""; }; FAE8EE9B273AD0A800A65307 /* QuickStartSettingsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickStartSettingsTests.swift; sourceTree = ""; }; + FAEC116D2AEBEEA600F9DA54 /* AbstractPostMenuViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbstractPostMenuViewModel.swift; sourceTree = ""; }; FAF0FAAB2AA094C0004C3228 /* NoSiteViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoSiteViewModelTests.swift; sourceTree = ""; }; FAF13C5225A57ABD003EE470 /* JetpackRestoreWarningViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackRestoreWarningViewController.swift; sourceTree = ""; }; FAF13E2F25A59240003EE470 /* JetpackRestoreStatusViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackRestoreStatusViewController.swift; sourceTree = ""; }; @@ -9357,6 +9370,9 @@ FE32F001275F602E0040BE67 /* CommentContentRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentContentRenderer.swift; sourceTree = ""; }; FE32F005275F62620040BE67 /* WebCommentContentRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebCommentContentRenderer.swift; sourceTree = ""; }; FE341704275FA157005D5CA7 /* RichCommentContentRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RichCommentContentRenderer.swift; sourceTree = ""; }; + FE34ACCE2B1661EB00108B3C /* DashboardBloganuaryCardCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardBloganuaryCardCell.swift; sourceTree = ""; }; + FE34ACD12B174AE700108B3C /* DashboardBloganuaryCardCellTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardBloganuaryCardCellTests.swift; sourceTree = ""; }; + FE34ACD92B17AA6C00108B3C /* BloganuaryOverlayViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BloganuaryOverlayViewController.swift; sourceTree = ""; }; FE39C133269C37C900EFB827 /* ListTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ListTableViewCell.xib; sourceTree = ""; }; FE39C134269C37C900EFB827 /* ListTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListTableViewCell.swift; sourceTree = ""; }; FE3D057D26C3D5C1002A51B0 /* ShareAppContentPresenterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAppContentPresenterTests.swift; sourceTree = ""; }; @@ -9372,6 +9388,9 @@ FE5096582A17A69F00DDD071 /* TwitterDeprecationTableFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwitterDeprecationTableFooterView.swift; sourceTree = ""; }; FE50965B2A20D0F300DDD071 /* CommentTableHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentTableHeaderView.swift; sourceTree = ""; }; FE59DA9527D1FD0700624D26 /* WordPress 138.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 138.xcdatamodel"; sourceTree = ""; }; + FE5F52D82AF9461200371A3A /* WordPress 153.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 153.xcdatamodel"; sourceTree = ""; }; + FE6AFE422B18EDF200F76520 /* BloganuaryTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BloganuaryTracker.swift; sourceTree = ""; }; + FE6AFE462B1A351F00F76520 /* SOTWCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOTWCardView.swift; sourceTree = ""; }; FE6BB142293227AC001E5F7A /* ContentMigrationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentMigrationCoordinator.swift; sourceTree = ""; }; FE6BB1452932289B001E5F7A /* ContentMigrationCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentMigrationCoordinatorTests.swift; sourceTree = ""; }; FE7B9A862A6A613000488791 /* PrepublishingSocialAccountsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrepublishingSocialAccountsViewController.swift; sourceTree = ""; }; @@ -9405,8 +9424,10 @@ FEDDD46E26A03DE900F8942B /* ListTableViewCell+Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ListTableViewCell+Notifications.swift"; sourceTree = ""; }; FEE48EFB2A4C8312008A48E0 /* Blog+JetpackSocial.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Blog+JetpackSocial.swift"; sourceTree = ""; }; FEE48EFE2A4C9855008A48E0 /* Blog+PublicizeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Blog+PublicizeTests.swift"; sourceTree = ""; }; + FEF207F22AF2882A0025CB2C /* BloggingPromptRemoteObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BloggingPromptRemoteObject.swift; sourceTree = ""; }; FEF28E812ACB3DCE006C6579 /* ReaderDetailNewHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderDetailNewHeaderView.swift; sourceTree = ""; }; FEF4DC5428439357003806BE /* ReminderScheduleCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReminderScheduleCoordinator.swift; sourceTree = ""; }; + FEF7F33F2AFEA0C200F793FC /* blogging-prompts-bloganuary.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "blogging-prompts-bloganuary.json"; sourceTree = ""; }; FEFA263D26C58427009CCB7E /* ShareAppTextActivityItemSourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAppTextActivityItemSourceTests.swift; sourceTree = ""; }; FEFA6AC22A83F4BE004EE5E6 /* PostHelper+JetpackSocial.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PostHelper+JetpackSocial.swift"; sourceTree = ""; }; FEFA6AC52A86824A004EE5E6 /* PostHelperJetpackSocialTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHelperJetpackSocialTests.swift; sourceTree = ""; }; @@ -9439,26 +9460,20 @@ FF2EC3C12209AC19006176E1 /* GutenbergImgUploadProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = GutenbergImgUploadProcessorTests.swift; path = Gutenberg/GutenbergImgUploadProcessorTests.swift; sourceTree = ""; }; FF355D971FB492DD00244E6D /* ExportableAsset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportableAsset.swift; sourceTree = ""; }; FF37F90822385C9F00AFA3DB /* RELEASE-NOTES.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = "RELEASE-NOTES.txt"; path = "../RELEASE-NOTES.txt"; sourceTree = ""; }; - FF4C069E206560E500E0B2BC /* MediaThumbnailCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaThumbnailCoordinator.swift; sourceTree = ""; }; FF4DEAD7244B56E200ACA032 /* CoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreServices.framework; path = System/Library/Frameworks/CoreServices.framework; sourceTree = SDKROOT; }; FF5371621FDFF64F00619A3F /* MediaService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaService.swift; sourceTree = ""; }; FF54D4631D6F3FA900A0DC4D /* GutenbergSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GutenbergSettings.swift; sourceTree = ""; }; FF619DD41C75246900903B65 /* CLPlacemark+Formatting.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CLPlacemark+Formatting.swift"; sourceTree = ""; }; FF695E151B87662500CD6EAA /* WordPress 38.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 38.xcdatamodel"; sourceTree = ""; }; - FF70A3201FD5840500BC270D /* PHAsset+Metadata.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "PHAsset+Metadata.swift"; sourceTree = ""; }; FF70A3211FD5840500BC270D /* UIImage+Export.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImage+Export.swift"; sourceTree = ""; }; FF75933A1BE2423800814D3B /* Photos.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Photos.framework; path = System/Library/Frameworks/Photos.framework; sourceTree = SDKROOT; }; - FF7C89A21E3A1029000472A8 /* MediaLibraryPickerDataSourceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaLibraryPickerDataSourceTests.swift; sourceTree = ""; }; FF7EACDD1B6CE6E100AB6AB9 /* WordPress 37.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 37.xcdatamodel"; sourceTree = ""; }; FF8032651EE9E22200861F28 /* MediaProgressCoordinatorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaProgressCoordinatorTests.swift; sourceTree = ""; }; FF8791BA1FBAF4B400AD86E6 /* MediaHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaHelper.swift; sourceTree = ""; }; FF8A04DF1D9BFE7400523BC4 /* CachedAnimatedImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CachedAnimatedImageView.swift; sourceTree = ""; }; FF8C54AC21F677260003ABCF /* GutenbergMediaInserterHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GutenbergMediaInserterHelper.swift; sourceTree = ""; }; - FF8CD624214184EE00A33A8D /* MediaAssetExporterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaAssetExporterTests.swift; sourceTree = ""; }; FF8DDCDD1B5DB1C10098826F /* SettingTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SettingTableViewCell.h; sourceTree = ""; }; FF8DDCDE1B5DB1C10098826F /* SettingTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SettingTableViewCell.m; sourceTree = ""; }; - FF945F6E1B28242300FB8AC4 /* MediaLibraryPickerDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MediaLibraryPickerDataSource.h; sourceTree = ""; }; - FF945F6F1B28242300FB8AC4 /* MediaLibraryPickerDataSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MediaLibraryPickerDataSource.m; sourceTree = ""; }; FF947A8C1BBE89A100B27B6A /* WordPress 39.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 39.xcdatamodel"; sourceTree = ""; }; FF9A6E7021F9361700D36D14 /* MediaUploadHashTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MediaUploadHashTests.swift; path = Gutenberg/MediaUploadHashTests.swift; sourceTree = ""; }; FFA0B7D61CAC1F9F00533B9D /* MainNavigationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainNavigationTests.swift; sourceTree = ""; }; @@ -9467,7 +9482,6 @@ FFABD7FF213423F1003C65B6 /* LinkSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkSettingsViewController.swift; sourceTree = ""; }; FFABD80721370496003C65B6 /* SelectPostViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectPostViewController.swift; sourceTree = ""; }; FFB1FA9D1BF0EB840090C761 /* UIImage+Exporters.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImage+Exporters.swift"; sourceTree = ""; }; - FFB1FA9F1BF0EC4E0090C761 /* PHAsset+Exporters.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "PHAsset+Exporters.swift"; sourceTree = ""; }; FFB3132E204D59F400C177E7 /* WordPress 73.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 73.xcdatamodel"; sourceTree = ""; }; FFC02B82222687BF00E64FDE /* GutenbergImageLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GutenbergImageLoader.swift; sourceTree = ""; }; FFC6ADD91B56F366002F3C84 /* LocalCoreDataService.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LocalCoreDataService.m; sourceTree = ""; }; @@ -9499,6 +9513,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 3FFB3F202AFC70B400A742B0 /* JetpackStatsWidgetsCore in Frameworks */, 0107E0DE28F97D5000DE87DB /* SwiftUI.framework in Frameworks */, 0107E0DF28F97D5000DE87DB /* WidgetKit.framework in Frameworks */, 35BBACD2917117A95B6F3046 /* Pods_JetpackStatsWidgets.framework in Frameworks */, @@ -9509,6 +9524,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 3F9F23252B0AE1AC00B56061 /* JetpackStatsWidgetsCore in Frameworks */, C649C66318E8B5EF92B8F196 /* Pods_JetpackIntents.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -9522,6 +9538,7 @@ 8298F3921EEF3BA7008EB7F0 /* StoreKit.framework in Frameworks */, 93F2E5441E9E5A570050D489 /* CoreSpotlight.framework in Frameworks */, 93F2E5421E9E5A350050D489 /* QuickLook.framework in Frameworks */, + 08E63FCF2B28E53400747E21 /* DesignSystem in Frameworks */, 93F2E53E1E9E5A010050D489 /* CoreText.framework in Frameworks */, E185474E1DED8D8800D875D7 /* UserNotifications.framework in Frameworks */, FF75933B1BE2423800814D3B /* Photos.framework in Frameworks */, @@ -9533,6 +9550,7 @@ E10B3655158F2D7800419A93 /* CoreGraphics.framework in Frameworks */, E10B3654158F2D4500419A93 /* UIKit.framework in Frameworks */, E10B3652158F2D3F00419A93 /* QuartzCore.framework in Frameworks */, + 0CD9FB892AFA71C2009D9C7A /* DGCharts in Frameworks */, E1A386CB14DB063800954CF8 /* MediaPlayer.framework in Frameworks */, E1A386CA14DB05F700954CF8 /* CoreMedia.framework in Frameworks */, E1A386C814DB05C300954CF8 /* AVFoundation.framework in Frameworks */, @@ -9543,12 +9561,12 @@ 24CE2EB1258D687A0000C297 /* WordPressFlux in Frameworks */, 8355D7D911D260AA00A61362 /* CoreData.framework in Frameworks */, 834CE7341256D0DE0046A4A3 /* CFNetwork.framework in Frameworks */, + 3F9F232B2B0B27DD00B56061 /* JetpackStatsWidgetsCore in Frameworks */, 83043E55126FA31400EC9953 /* MessageUI.framework in Frameworks */, FD3D6D2C1349F5D30061136A /* ImageIO.framework in Frameworks */, B5AA54D51A8E7510003BDD12 /* WebKit.framework in Frameworks */, 93F2E5401E9E5A180050D489 /* libsqlite3.tbd in Frameworks */, FD21397F13128C5300099582 /* libiconv.dylib in Frameworks */, - 3F2B62DC284F4E0B0008CD59 /* Charts in Frameworks */, E19DF741141F7BDD000002F3 /* libz.dylib in Frameworks */, 17A8858D2757B97F0071FCA3 /* AutomatticAbout in Frameworks */, FF4DEAD8244B56E300ACA032 /* CoreServices.framework in Frameworks */, @@ -9637,6 +9655,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 3FFB3F242AFC730C00A742B0 /* JetpackStatsWidgetsCore in Frameworks */, 3F338B71289BD3040014ADC5 /* Nimble in Frameworks */, 3F3B23C22858A1B300CACE60 /* BuildkiteTestCollector in Frameworks */, E8DEE110E4BC3FA1974AB1BB /* Pods_WordPressTest.framework in Frameworks */, @@ -9660,14 +9679,17 @@ FABB26212602FC2C00C8785C /* StoreKit.framework in Frameworks */, FABB26222602FC2C00C8785C /* CoreSpotlight.framework in Frameworks */, FABB26232602FC2C00C8785C /* QuickLook.framework in Frameworks */, + 3F9F232D2B0B281400B56061 /* JetpackStatsWidgetsCore in Frameworks */, FABB26242602FC2C00C8785C /* CoreText.framework in Frameworks */, FABB26252602FC2C00C8785C /* UserNotifications.framework in Frameworks */, FABB26262602FC2C00C8785C /* Photos.framework in Frameworks */, FABB26272602FC2C00C8785C /* NotificationCenter.framework in Frameworks */, FABB26282602FC2C00C8785C /* CoreTelephony.framework in Frameworks */, FABB26292602FC2C00C8785C /* SystemConfiguration.framework in Frameworks */, + 08E63FCD2B28E52B00747E21 /* DesignSystem in Frameworks */, FABB262A2602FC2C00C8785C /* AudioToolbox.framework in Frameworks */, FABB262B2602FC2C00C8785C /* CoreGraphics.framework in Frameworks */, + 0CD9FB872AFA71B9009D9C7A /* DGCharts in Frameworks */, FABB262C2602FC2C00C8785C /* UIKit.framework in Frameworks */, FABB262D2602FC2C00C8785C /* QuartzCore.framework in Frameworks */, FABB262E2602FC2C00C8785C /* MediaPlayer.framework in Frameworks */, @@ -9688,7 +9710,6 @@ FABB263B2602FC2C00C8785C /* libsqlite3.tbd in Frameworks */, FABB263C2602FC2C00C8785C /* libiconv.dylib in Frameworks */, FABB263D2602FC2C00C8785C /* libz.dylib in Frameworks */, - 3F2B62DE284F4E310008CD59 /* Charts in Frameworks */, FABB263F2602FC2C00C8785C /* CoreServices.framework in Frameworks */, 9C86CF3E1EAC13181A593D00 /* Pods_Apps_Jetpack.framework in Frameworks */, ); @@ -9739,7 +9760,6 @@ 3F8EEC4D25B4817000EC9DAE /* StatsWidgets.swift */, 3F8EEC6F25B4849A00EC9DAE /* SiteListProvider.swift */, C9C21D7529BECFAE009F68E5 /* LockScreenWidgets */, - C9FE382029C203EE00D39841 /* Extensions */, C9B477AA29CC15C2008CBF49 /* Helpers */, 3FFDDCB925B8A65F008D5BDD /* Widgets */, 3FB34ABB25672A59001A74A6 /* Model */, @@ -9814,6 +9834,15 @@ path = "Supporting Files"; sourceTree = ""; }; + 017008402B35BC1A00C80490 /* View Models */ = { + isa = PBXGroup; + children = ( + F46546302AF2F8D20017E3D1 /* DomainsStateViewModel.swift */, + 017008442B35C25C00C80490 /* SiteDomainsViewModel.swift */, + ); + path = "View Models"; + sourceTree = ""; + }; 018635822A81098300915532 /* SupportChatBot */ = { isa = PBXGroup; children = ( @@ -9860,6 +9889,14 @@ name = Coordinators; sourceTree = ""; }; + 01B5C3C52AE7FC3B007055BB /* UITesting */ = { + isa = PBXGroup; + children = ( + 01B5C3C62AE7FC61007055BB /* UITestConfigurator.swift */, + ); + name = UITesting; + sourceTree = ""; + }; 01E258002ACC36DC00F09666 /* Plan */ = { isa = PBXGroup; children = ( @@ -9870,6 +9907,14 @@ path = Plan; sourceTree = ""; }; + 01E258072ACC3A9000F09666 /* Helpers */ = { + isa = PBXGroup; + children = ( + 01E258082ACC3AA000F09666 /* iOS17WidgetAPIs.swift */, + ); + path = Helpers; + sourceTree = ""; + }; 027AC51F2278982D0033E56E /* DomainCredit */ = { isa = PBXGroup; children = ( @@ -9949,11 +9994,13 @@ path = "Feature Highlight"; sourceTree = ""; }; - 08240C2C2AB8A28D00E7AEA8 /* Domain Management */ = { + 0830538A2B272F9C00B889FE /* Dynamic */ = { isa = PBXGroup; children = ( + 0830538B2B2732E400B889FE /* DynamicDashboardCard.swift */, + F41D98E22B39C9E7004EC050 /* BlogDashboardDynamicCardCoordinator.swift */, ); - path = "Domain Management"; + path = Dynamic; sourceTree = ""; }; 083ED8CA2A4322A7007F89B3 /* EEUUSCompliance */ = { @@ -9984,7 +10031,6 @@ 086C4D0F1E81F9240011D960 /* Media+Blog.swift */, 4A87854F290F2C7D0083AB78 /* Media+Sync.swift */, FF286C751DE70A4500383A62 /* NSURL+Exporters.swift */, - FFB1FA9F1BF0EC4E0090C761 /* PHAsset+Exporters.swift */, FFB1FA9D1BF0EB840090C761 /* UIImage+Exporters.swift */, ); name = Media; @@ -10004,6 +10050,7 @@ children = ( 086E1FDE1BBB35D2002D86CA /* MenusViewController.h */, 086E1FDF1BBB35D2002D86CA /* MenusViewController.m */, + 4A535E132AF3368B008B87B9 /* MenusViewController.swift */, C3B554502965C32A00A04753 /* MenusViewController+JetpackBannerViewController.swift */, 08216FA81CDBF95100304BA7 /* MenuItemEditingViewController.h */, 08216FA91CDBF95100304BA7 /* MenuItemEditingViewController.m */, @@ -10111,6 +10158,16 @@ name = "View Models"; sourceTree = ""; }; + 08D61F1F2AD0633600BF3D00 /* All Domains */ = { + isa = PBXGroup; + children = ( + 80348F2C2AF8708A0045CCD3 /* Coordinators */, + F4141EF02AE99F14000D2AAE /* View Models */, + F4141EEF2AE99EE2000D2AAE /* Views */, + ); + path = "All Domains"; + sourceTree = ""; + }; 08D978491CD2AF7D0054F19A /* Menus */ = { isa = PBXGroup; children = ( @@ -10124,25 +10181,12 @@ path = Menus; sourceTree = ""; }; - 08EA036529C9B50500B72A87 /* DesignSystem */ = { - isa = PBXGroup; - children = ( - 08EA036629C9B51200B72A87 /* Color+DesignSystem.swift */, - 08EA036829C9B53000B72A87 /* Colors.xcassets */, - 088D58A429E724F300E6C0F4 /* ColorGallery.swift */, - 0880BADB29ED6FF3002D3AB0 /* UIColor+DesignSystem.swift */, - 08799C242A334645005317F7 /* Spacing.swift */, - ); - path = DesignSystem; - sourceTree = ""; - }; 08F8CD281EBD22EF0049D0C0 /* Media */ = { isa = PBXGroup; children = ( 3F2ABE192770EF3E005D8916 /* Blog+VideoLimits.swift */, 08B6E5191F036CAD00268F57 /* MediaFileManager.swift */, 08F8CD291EBD22EF0049D0C0 /* MediaExporter.swift */, - 08C388651ED7705E0057BE49 /* MediaAssetExporter.swift */, 0C8FC9A32A8BD39A0059DCE4 /* ItemProviderMediaExporter.swift */, 08F8CD2E1EBD29440049D0C0 /* MediaImageExporter.swift */, 086103951EE09C91004D7C01 /* MediaVideoExporter.swift */, @@ -10151,9 +10195,12 @@ 7EAD7CCF206D761200BEDCFD /* MediaExternalExporter.swift */, 7E729C27209A087200F76599 /* ImageLoader.swift */, 742B7F39209CB2B6002E3CC9 /* GIFPlaybackStrategy.swift */, + 0C1DB5FE2B095DA50028F200 /* ImageView.swift */, B5EB19EB20C6DACC008372B9 /* ImageDownloader.swift */, FADC40AD2A8D2E8D00C19997 /* ImageDownloader+Gravatar.swift */, + 0C1DB6072B0A419B0028F200 /* ImageDecoder.swift */, 3F2ABE15277037A9005D8916 /* VideoLimitsAlertPresenter.swift */, + 0C7073942A65CB2E00F325CE /* MemoryCache.swift */, ); path = Media; sourceTree = ""; @@ -10164,7 +10211,6 @@ 08B6E51B1F037ADD00268F57 /* MediaFileManagerTests.swift */, 08F8CD2C1EBD245F0049D0C0 /* MediaExporterTests.swift */, 08F8CD301EBD2A960049D0C0 /* MediaImageExporterTests.swift */, - FF8CD624214184EE00A33A8D /* MediaAssetExporterTests.swift */, 08F8CD3A1EBD2D020049D0C0 /* MediaURLExporterTests.swift */, 08E77F461EE9D72F006F9515 /* MediaThumbnailExporterTests.swift */, 0C8FC9A92A8C57000059DCE4 /* ItemProviderMediaExporterTests.swift */, @@ -10180,11 +10226,19 @@ path = Mocks; sourceTree = ""; }; + 0C0AD1082B0CCEC900EC06E6 /* Preview */ = { + isa = PBXGroup; + children = ( + 0C0AD1092B0CCFA400EC06E6 /* MediaPreviewController.swift */, + ); + path = Preview; + sourceTree = ""; + }; 0C23F3332AC49C1000EE6117 /* Views */ = { isa = PBXGroup; children = ( 0CAE8EF12A9E9E8D0073EEB9 /* SiteMediaCollectionCell.swift */, - 0C01A6E92AB37F0F009F7145 /* SiteMediaCollectionCellBadgeView.swift */, + 0C01A6E92AB37F0F009F7145 /* SiteMediaCollectionCellSelectionOverlayView.swift */, 0CAE8EF52A9E9EE30073EEB9 /* SiteMediaCollectionCellViewModel.swift */, 0C23F3352AC4AD3400EE6117 /* SiteMediaSelectionTitleView.swift */, 0C0453272AC73343003079C8 /* SiteMediaVideoDurationView.swift */, @@ -10202,6 +10256,14 @@ path = Controllers; sourceTree = ""; }; + 0C3090202B1290730071C551 /* Helpers */ = { + isa = PBXGroup; + children = ( + 0C308FFD2B1234E70071C551 /* SiteMediaFilterButtonView.swift */, + ); + path = Helpers; + sourceTree = ""; + }; 0C7D48182A4DB91B0023CF84 /* Blaze */ = { isa = PBXGroup; children = ( @@ -10226,8 +10288,10 @@ children = ( 0C8E2F2C2AC4722F0023F9D6 /* SiteMediaViewController.swift */, 0C23F33D2AC4AEF600EE6117 /* SiteMediaPickerViewController.swift */, + 0CD9FB8A2AFADAFE009D9C7A /* SiteMediaPageViewController.swift */, 0C23F33C2AC4AED900EE6117 /* Controllers */, 0C23F3332AC49C1000EE6117 /* Views */, + 0C3090202B1290730071C551 /* Helpers */, ); path = SiteMedia; sourceTree = ""; @@ -10241,6 +10305,41 @@ path = BlogPersonalization; sourceTree = ""; }; + 0CB786102B161BE800DF7625 /* Crop */ = { + isa = PBXGroup; + children = ( + B5FF3BE61CAD881100C1D597 /* ImageCropOverlayView.swift */, + B5A05AD81CA48601002EC787 /* ImageCropViewController.swift */, + B5EEB19E1CA96D19004B6540 /* ImageCropViewController.xib */, + ); + path = Crop; + sourceTree = ""; + }; + 0CD9CCA12AD8313E0044A33C /* Search */ = { + isa = PBXGroup; + children = ( + 0CD9CC9E2AD73A560044A33C /* PostSearchViewController.swift */, + 0CD9CCA22AD831590044A33C /* PostSearchViewModel.swift */, + 0C1531FD2AE17140003CDE13 /* PostSearchViewModel+Highlighter.swift */, + 0CB424ED2ADEE3CD0080B807 /* PostSearchTokenTableCell.swift */, + 0CB424F02ADEE52A0080B807 /* PostSearchToken.swift */, + 0CA10F6C2ADAE86D00CE75AC /* PostSearchSuggestionsService.swift */, + 0CA10FA62ADB76ED00CE75AC /* PostSearchService.swift */, + ); + path = Search; + sourceTree = ""; + }; + 0CE7833F2B08FB1B00B114EB /* External */ = { + isa = PBXGroup; + children = ( + 0CE7833C2B08F3C300B114EB /* ExternalMediaPickerViewController.swift */, + 0CE538C92B0D6E0000834BA2 /* ExternalMediaDataSource.swift */, + 0CE783402B08FB2E00B114EB /* ExternalMediaPickerCollectionCell.swift */, + 0C0AD1052B0C483F00EC06E6 /* ExternalMediaSelectionTitleView.swift */, + ); + path = External; + sourceTree = ""; + }; 1705CEE6260A57F900F23763 /* ContentViews */ = { isa = PBXGroup; children = ( @@ -10406,8 +10505,10 @@ 173BCE711CEB365400AE8817 /* Domains */ = { isa = PBXGroup; children = ( + 017008402B35BC1A00C80490 /* View Models */, + 017C57BA2B2B5555001E7687 /* DomainSelectionViewController.swift */, + F4D1401E2AFD9B8200961797 /* Transfer Domains */, 02BF30512271D76F00616558 /* Domain credit */, - 08240C2C2AB8A28D00E7AEA8 /* Domain Management */, 437EF0C820F79C0C0086129B /* Domain registration */, 3F3DD0B426FD18D400F5F121 /* Utility */, 3F55A01826FCB0BD0049D379 /* Views */, @@ -10420,6 +10521,7 @@ children = ( 1797373620EBAA4100377B4E /* RouteMatcherTests.swift */, 174C116E2624603400346EC6 /* MBarRouteTests.swift */, + 3FFB3F212AFC72EC00A742B0 /* DeepLinkSourceTests.swift */, ); name = "Deep Linking"; sourceTree = ""; @@ -10439,7 +10541,10 @@ 175CC17327205BDC00622FB4 /* Domains */ = { isa = PBXGroup; children = ( - 175CC17427205BFB00622FB4 /* DomainExpiryDateFormatterTests.swift */, + F46546322AF54DCD0017E3D1 /* AllDomainsListItemViewModelTests.swift */, + F46546342AF550A20017E3D1 /* AllDomainsListItem+Helpers.swift */, + F4F7B2522AFA585700207282 /* DomainDetailsWebViewControllerTests.swift */, + 01B7590D2B3EEEA400179AE6 /* SiteDomainsViewModelTests.swift */, ); path = Domains; sourceTree = ""; @@ -10980,12 +11085,14 @@ 31F4F6641A13858F00196A98 /* Me */ = { isa = PBXGroup; children = ( + F4F7B24F2AF8EBCA00207282 /* Domain Details */, F4FB0ACB2925878E00F651F9 /* Views */, 3F29EB70240421F6005313DE /* Account Settings */, 3F29EB6E240420C3005313DE /* App Settings */, 3F29EB6F2404218E005313DE /* Help & Support */, 3F29EB6B24041FFC005313DE /* Me Main */, 3F29EB6D24042093005313DE /* My Profile */, + 08D61F1F2AD0633600BF3D00 /* All Domains */, ); path = Me; sourceTree = ""; @@ -11259,7 +11366,7 @@ 0CED955F2A460F4B0020F420 /* DebugFeatureFlagsView.swift */, F9B862C82478170A008B093C /* EncryptedLogTableViewController.swift */, CECEEB542823164800A28ADE /* MediaCacheSettingsViewController.swift */, - 80A2154229D1177A002FE8EB /* RemoteConfigDebugViewController.swift */, + 0C57510F2B011468001074E5 /* RemoteConfigDebugView.swift */, ); path = "App Settings"; sourceTree = ""; @@ -11372,7 +11479,6 @@ isa = PBXGroup; children = ( 3F3DD0B526FD18EB00F5F121 /* Blog+DomainsDashboardView.swift */, - 175CC16F2720548700622FB4 /* DomainExpiryDateFormatter.swift */, 014ACD132A1E5033008A706C /* WebKitViewController+SandboxStore.swift */, ); path = Utility; @@ -11389,11 +11495,10 @@ 3F43603423F368BF001DEE70 /* Blog Details */ = { isa = PBXGroup; children = ( + FE6AFE452B1A343A00F76520 /* SoTW 2023 */, 462F4E0618369F0B0028D2F8 /* BlogDetailsViewController.h */, 462F4E0718369F0B0028D2F8 /* BlogDetailsViewController.m */, 74989B8B2088E3650054290B /* BlogDetailsViewController+Activity.swift */, - 02AC3091226FFFAA0018D23B /* BlogDetailsViewController+DomainCredit.swift */, - 8B33BC9427A0C14C00DB5985 /* BlogDetailsViewController+QuickActions.swift */, 435D10192130C2AB00BB2AA8 /* BlogDetailsViewController+FancyAlerts.swift */, 02761EBF2270072F009BAF0F /* BlogDetailsViewController+SectionHelpers.swift */, FAFC065027D27241002F0483 /* BlogDetailsViewController+Dashboard.swift */, @@ -11415,6 +11520,7 @@ 74EFB5C7208674250070BD4E /* BlogListViewController+Activity.swift */, D8EB1FD021900810002AE1C4 /* BlogListViewController+SiteCreation.swift */, E11E77591E72932F0072AD40 /* BlogListDataSource.swift */, + 80DB57972AF99E0900C728FF /* BlogListConfiguration.swift */, ); path = "Blog List"; sourceTree = ""; @@ -11474,21 +11580,12 @@ 3F46EEC028BC48D1004F02B2 /* New Landing Screen */ = { isa = PBXGroup; children = ( - 3F46EEC328BC4913004F02B2 /* Model */, 3F46EEC528BC4922004F02B2 /* ViewModel */, 3F46EEC428BC491B004F02B2 /* Views */, ); path = "New Landing Screen"; sourceTree = ""; }; - 3F46EEC328BC4913004F02B2 /* Model */ = { - isa = PBXGroup; - children = ( - 3F46EEC628BC4935004F02B2 /* JetpackPrompt.swift */, - ); - path = Model; - sourceTree = ""; - }; 3F46EEC428BC491B004F02B2 /* Views */ = { isa = PBXGroup; children = ( @@ -11520,6 +11617,7 @@ 3F526D2B2539F9D60069706C /* Views */ = { isa = PBXGroup; children = ( + 01E258072ACC3A9000F09666 /* Helpers */, 3FCF66E825CAF8C50047F337 /* ListStatsView.swift */, 3F5689FF25420DE80048A9E4 /* MultiStatsView.swift */, 3F5689EF254209790048A9E4 /* SingleStatView.swift */, @@ -11535,12 +11633,15 @@ isa = PBXGroup; children = ( B0B89DBF2A1E882F003D5295 /* DomainResultView.swift */, - 3FAF9CC126D01CFE00268EA2 /* DomainsDashboardView.swift */, - 3F3DD0B126FD176800F5F121 /* PresentationCard.swift */, - 3F3DD0AE26FCDA3100F5F121 /* PresentationButton.swift */, + 3FAF9CC126D01CFE00268EA2 /* SiteDomainsView.swift */, + 3F3DD0B126FD176800F5F121 /* SiteDomainsPresentationCard.swift */, 3FA62FD226FE2E4B0020793A /* ShapeWithTextView.swift */, 011896A429D5B72500D34BA9 /* DomainsDashboardCoordinator.swift */, 011896A729D5BBB400D34BA9 /* DomainsDashboardFactory.swift */, + B0DE91B42AF9778200D51A02 /* DomainSetupNoticeView.swift */, + 0162314F2B3B3CAD0010E377 /* PrimaryDomainView.swift */, + 01B759072B3ECAF300179AE6 /* DomainsStateView.swift */, + 01B7590A2B3ED63B00179AE6 /* DomainDetailsWebViewControllerWrapper.swift */, ); path = Views; sourceTree = ""; @@ -11551,7 +11652,9 @@ 3FCF66FA25CAF8E00047F337 /* ListRow.swift */, 3F568A2E254216550048A9E4 /* FlexibleCard.swift */, 3F568A1E254213B60048A9E4 /* VerticalCard.swift */, + 018FF1342AE6771A00F301C3 /* LockScreenVerticalCard.swift */, 3FA59B99258289E30073772F /* StatsValueView.swift */, + 018FF1362AE67C2600F301C3 /* LockScreenFlexibleCard.swift */, ); path = Cards; sourceTree = ""; @@ -11668,7 +11771,7 @@ 3FA59DCC2582E53F0073772F /* Tracks */ = { isa = PBXGroup; children = ( - 98390AC2254C984700868F0A /* Tracks+StatsWidgets.swift */, + 01ABF16F2AD578B3004331BD /* WidgetAnalytics.swift */, ); path = Tracks; sourceTree = ""; @@ -11681,15 +11784,16 @@ 3FA6405A2670CCD40064401E /* Info.plist */, 3F762E9226784A950088CD45 /* Logger.swift */, 3FE39A4326F8391D006E2B3A /* Screens */, - 3FA640592670CCD40064401E /* UITestsFoundation.h */, EA85B7A92A6860370096E097 /* TestObserver.swift */, + 3FA640592670CCD40064401E /* UITestsFoundation.h */, 3F762E9426784B540088CD45 /* WireMock.swift */, 3F107B1829B6F7E0009B3658 /* XCTestCase+Utils.swift */, 3F6A8CDF2A246357009DBC2B /* XCUIApplication+SavePassword.swift */, + D8E7529A2A29DC4C00E73B2D /* XCUIApplication+ScrollDownToElement.swift */, 3FB5C2B227059AC8007D0ECE /* XCUIElement+Scroll.swift */, + 3F03F2BC2B45041E00A9CE99 /* XCUIElement+TapUntil.swift */, 3F762E9A26784D2A0088CD45 /* XCUIElement+Utils.swift */, 3F762E9826784CC90088CD45 /* XCUIElementQuery+Utils.swift */, - D8E7529A2A29DC4C00E73B2D /* XCUIApplication+ScrollDownToElement.swift */, ); path = UITestsFoundation; sourceTree = ""; @@ -11708,7 +11812,6 @@ isa = PBXGroup; children = ( 98BFF57D23984344008A1DCB /* AllTimeWidgetStats.swift */, - 98F93181239AF64800E4E96E /* ThisWeekWidgetStats.swift */, 98E58A2E2360D23400E5534B /* TodayWidgetStats.swift */, 3F6DA04025646F96002AB88F /* HomeWidgetData.swift */, 3F5C861925C9EA2500BABE64 /* HomeWidgetAllTimeData.swift */, @@ -11763,7 +11866,7 @@ BE6DD32F1FD67F3B00E55192 /* TabNavComponent.swift */, EAD08D0D29D45E23001A72F9 /* CommentsScreen.swift */, D82E087729EEB7AF0098F500 /* DomainsScreen.swift */, - 01281E992A0456CB00464F8F /* DomainsSuggestionsScreen.swift */, + 01281E992A0456CB00464F8F /* DomainsSelectionScreen.swift */, 011F52D72A1BECA200B04114 /* PlanSelectionScreen.swift */, D8599FE92A2930CE00065193 /* PagesScreen.swift */, ); @@ -11963,8 +12066,8 @@ isa = PBXGroup; children = ( 3FAF9CC426D03C7400268EA2 /* DomainSuggestionViewControllerWrapper.swift */, - 171096CA270F01EA001BCDD6 /* DomainSuggestionsTableViewController.swift */, - 436D560C2117312600CEAA33 /* RegisterDomainSuggestionsViewController.swift */, + 80DB57912AF8B59B00C728FF /* RegisterDomainCoordinator.swift */, + F479995D2AFD241E0023F4FB /* RegisterDomainTransferFooterView.swift */, ); path = RegisterDomainSuggestions; sourceTree = ""; @@ -12018,14 +12121,12 @@ 437EF0C820F79C0C0086129B /* Domain registration */ = { isa = PBXGroup; children = ( - 402FFB1B218C27C100FF4A0B /* RegisterDomain.storyboard */, 436D560E2117312700CEAA33 /* RegisterDomainDetails */, 436D560B2117312600CEAA33 /* RegisterDomainSuggestions */, 43D74AD220FB5A82004AD934 /* Views */, F47E154929E84A9300B6E426 /* SiteCreationPurchasingWebFlowController.swift */, B07F133D2A16C69800AF7FCF /* PlanSelectionViewController.swift */, 011F52D92A1CA53300B04114 /* CheckoutViewController.swift */, - 08240C2D2AB8A2DD00E7AEA8 /* DomainListCard.swift */, ); path = "Domain registration"; sourceTree = ""; @@ -12193,7 +12294,6 @@ DC13DB7D293FD09F00E33561 /* StatsInsightsStoreTests.swift */, 937250ED267A492D0086075F /* StatsPeriodStoreTests.swift */, 0148CC282859127F00CF5D96 /* StatsWidgetsStoreTests.swift */, - 08A4E12E289D2795001D9EC7 /* UserPersistentStoreTests.swift */, 0147D650294B6EA600AA6410 /* StatsRevampStoreTests.swift */, ); path = Stores; @@ -12222,7 +12322,6 @@ 57E3C98223835A57004741DB /* Controllers */ = { isa = PBXGroup; children = ( - 8B939F4223832E5D00ACCB0F /* PostListViewControllerTests.swift */, F543AF5623A84E4D0022F595 /* PublishSettingsControllerTests.swift */, F5D0A65123CCD3B600B20D27 /* PreviewWebKitViewControllerTests.swift */, ); @@ -12241,6 +12340,7 @@ 3F593FDC2A81DC6D00B29E86 /* NSError+TestInstance.swift */, 57889AB723589DF100DAE56D /* PageBuilder.swift */, 570BFD8F2282418A007859A8 /* PostBuilder.swift */, + 3F56F55B2AEA2F67006BDCEA /* ReaderPostBuilder.swift */, 3F759FBB2A2DB2CF0039A845 /* TestError.swift */, ); name = TestUtilities; @@ -12271,6 +12371,7 @@ 59ECF87A1CB7061D00E68F25 /* PostSharingControllerTests.swift */, F18B43771F849F580089B817 /* PostAttachmentTests.swift */, 8B6BD54F24293FBE00DB8F28 /* PrepublishingNudgesViewControllerTests.swift */, + 0CB424F32ADF3CBE0080B807 /* PostSearchViewModelTests.swift */, ); name = Posts; sourceTree = ""; @@ -12309,6 +12410,7 @@ D87A329520ABD60700F4726F /* ReaderTableContent.swift */, 5D42A401175E76A1005CFF05 /* WPImageViewController.h */, 5D42A402175E76A2005CFF05 /* WPImageViewController.m */, + 0C749D792B0543D0004CB468 /* WPImageViewController+Swift.swift */, C7192ECE25E8432D00C3020D /* ReaderTopicsCardCell.xib */, FE015BB02ADA002400F50D7F /* ReaderTopicsNewCardCell.xib */, F4D36AD4298498E600E6B84C /* ReaderPostBlockingController.swift */, @@ -12323,9 +12425,6 @@ 43AB7C5D1D3E70510066CB6A /* PostListFilterSettings.swift */, 174C9696205A846E00CEEF6E /* PostNoticeViewModel.swift */, 170CE73F2064478600A48191 /* PostNoticeNavigationCoordinator.swift */, - 570BFD8A22823D7B007859A8 /* PostActionSheet.swift */, - 570265142298921800F2214C /* PostListTableViewHandler.swift */, - 57047A4E22A961BC00B461DF /* PostSearchHeader.swift */, F16C35DB23F3F78E00C81331 /* AutoUploadMessageProvider.swift */, F16C35D923F3F76C00C81331 /* PostAutoUploadMessageProvider.swift */, 8B8FE8562343952B00F9AD2E /* PostAutoUploadMessages.swift */, @@ -12460,30 +12559,22 @@ isa = PBXGroup; children = ( 0CAE8EF42A9E9ECC0073EEB9 /* SiteMedia */, + 0CE7833F2B08FB1B00B114EB /* External */, + 0C0AD1082B0CCEC900EC06E6 /* Preview */, C87501EF243AEC290002CD60 /* Tenor */, D8A3A5AD206A059100992576 /* StockPhotos */, + 0CB786102B161BE800DF7625 /* Crop */, FF8A04DF1D9BFE7400523BC4 /* CachedAnimatedImageView.swift */, 7435CE7220A4B9170075A1B9 /* AnimatedImageCache.swift */, - 0C7073942A65CB2E00F325CE /* MemoryCache.swift */, - B5FF3BE61CAD881100C1D597 /* ImageCropOverlayView.swift */, - B5A05AD81CA48601002EC787 /* ImageCropViewController.swift */, - B5EEB19E1CA96D19004B6540 /* ImageCropViewController.xib */, - FF945F6E1B28242300FB8AC4 /* MediaLibraryPickerDataSource.h */, - FF945F6F1B28242300FB8AC4 /* MediaLibraryPickerDataSource.m */, - 17BB26AD1E6D8321008CD031 /* MediaLibraryViewController.swift */, - 173B215427875E0600D4DD6B /* DeviceMediaPermissionsHeader.swift */, + 0CB424F52AE0416D0080B807 /* SolidColorActivityIndicator.swift */, 1782BE831E70063100A91E7D /* MediaItemViewController.swift */, 177074841FB209F100951A4A /* CircularProgressView.swift */, 981D0929211259840014ECAF /* NoResultsViewController+MediaLibrary.swift */, 17D5C3F61FFCF2D800EB70FF /* MediaProgressCoordinatorNoticeViewModel.swift */, 1750BD6C201144DB0050F13A /* MediaNoticeNavigationCoordinator.swift */, - D80BC79B207464D200614A59 /* MediaLibraryMediaPickingCoordinator.swift */, - D80BC7A12074739300614A59 /* MediaLibraryStrings.swift */, 0C8FC9A02A8BC8630059DCE4 /* PHPickerController+Extensions.swift */, 0C8FC9A62A8BFAAD0059DCE4 /* NSItemProvider+Exportable.swift */, 0C0AE7582A8FAD6A007D9D6C /* MediaPickerMenu.swift */, - 7E14635620B3BEAB00B95F41 /* WPStyleGuide+Loader.swift */, - 7ECD5B8020C4D823001AEBC5 /* MediaPreviewHelper.swift */, ); path = Media; sourceTree = ""; @@ -12493,7 +12584,6 @@ children = ( 591232681CCEAA5100B86207 /* AbstractPostListViewController.swift */, 590E873A1CB8205700D1B734 /* PostListViewController.swift */, - E684383D221F535900752258 /* LoadMoreCounter.swift */, ); name = Controllers; sourceTree = ""; @@ -12501,10 +12591,6 @@ 5DF3DD6A1A93772D0051A229 /* Views */ = { isa = PBXGroup; children = ( - 597421B01CEB6874005D5F38 /* ConfigurablePostView.h */, - 57D6C83F229498C4003DDC7E /* InteractivePostView.swift */, - 575E126E229779E70041B3EB /* RestorePostTableViewCell.swift */, - 5D2FB2821AE98C4600F1D4ED /* RestorePostTableViewCell.xib */, 5D732F951AE84E3C00CD89E7 /* PostListFooterView.h */, 5D732F961AE84E3C00CD89E7 /* PostListFooterView.m */, 5D732F981AE84E5400CD89E7 /* PostListFooterView.xib */, @@ -12515,10 +12601,16 @@ 17A28DC42050404C00EA6D9E /* AuthorFilterButton.swift */, 17A28DCA2052FB5D00EA6D9E /* AuthorFilterViewController.swift */, 5727EAF72284F5AC00822104 /* InteractivePostViewDelegate.swift */, - 57AA848E228715DA00D3C2A2 /* PostCardCell.swift */, - 57AA8490228715E700D3C2A2 /* PostCardCell.xib */, 577C2AB322943FEC00AD1F03 /* PostCompactCell.swift */, 577C2AB52294401800AD1F03 /* PostCompactCell.xib */, + FACF66C92ADD4703008C3E13 /* PostListCell.swift */, + FACF66CC2ADD645C008C3E13 /* PostListHeaderView.swift */, + FACF66CF2ADD6CD8008C3E13 /* PostListItemViewModel.swift */, + 0CF0C4222AE98C13006FFAB4 /* AbstractPostHelper.swift */, + 0CFE9AC52AF44A9F00B8F659 /* AbstractPostHelper+Actions.swift */, + FAD3DE802AE2965A00A3B031 /* AbstractPostMenuHelper.swift */, + FAEC116D2AEBEEA600F9DA54 /* AbstractPostMenuViewModel.swift */, + FA141F262AEC1D9E00C9A653 /* PageMenuViewModel.swift */, ); name = Views; sourceTree = ""; @@ -12546,6 +12638,7 @@ 59DCA5201CC68AF3000F245F /* PageListViewController.swift */, 9AF724EE2146813C00F63A61 /* ParentPageSettingsViewController.swift */, 7D21280C251CF0850086DD2C /* EditPageViewController.swift */, + FA141F292AEC23E300C9A653 /* PageListViewController+Menu.swift */, ); name = Controllers; sourceTree = ""; @@ -12553,16 +12646,10 @@ 5DFA7EBE1AF7CB3A0072023B /* Views */ = { isa = PBXGroup; children = ( - 59A3CADB1CD2FF0C009BFA1B /* BasePageListCell.h */, - 59A3CADC1CD2FF0C009BFA1B /* BasePageListCell.m */, + 0C700B852AE1E1300085C2EE /* PageListCell.swift */, + 0C700B882AE1E1940085C2EE /* PageListItemViewModel.swift */, 8B93856D22DC08060010BF02 /* PageListSectionHeaderView.swift */, 5D13FA561AF99C2100F06492 /* PageListSectionHeaderView.xib */, - 5DFA7EC41AF814E40072023B /* PageListTableViewCell.h */, - 5DFA7EC51AF814E40072023B /* PageListTableViewCell.m */, - 5DFA7EC61AF814E40072023B /* PageListTableViewCell.xib */, - 5D18FE9C1AFBB17400EFEED0 /* RestorePageTableViewCell.h */, - 5D18FE9D1AFBB17400EFEED0 /* RestorePageTableViewCell.m */, - 5D18FE9E1AFBB17400EFEED0 /* RestorePageTableViewCell.xib */, 834A49D12A0C23A90042ED3D /* TemplatePageTableViewCell.swift */, ); name = Views; @@ -12716,12 +12803,10 @@ isa = PBXGroup; children = ( 731E88C521C9A10A0055C014 /* ErrorStates */, - 738B9A4C21B85CF20005062B /* KeyboardInfo.swift */, 738B9A4921B85CF20005062B /* ModelSettableCell.swift */, D813D67E21AA8BBF0055CCA1 /* ShadowView.swift */, 738B9A4D21B85CF20005062B /* SiteCreationHeaderData.swift */, 738B9A4A21B85CF20005062B /* TableDataCoordinator.swift */, - 73CE3E0D21F7F9D3007C9C85 /* TableViewOffsetCoordinator.swift */, 738B9A4B21B85CF20005062B /* TitleSubtitleHeader.swift */, 738B9A4821B85CF20005062B /* TitleSubtitleTextfieldHeader.swift */, 738B9A5D21B8632E0005062B /* UITableView+Header.swift */, @@ -12996,7 +13081,6 @@ FF8C54AC21F677260003ABCF /* GutenbergMediaInserterHelper.swift */, 7EA30DB421ADA20F0092F894 /* AztecAttachmentDelegate.swift */, 7EA30DB321ADA20F0092F894 /* EditorMediaUtility.swift */, - 912347182213484300BD9F97 /* GutenbergViewController+InformativeDialog.swift */, 912347752216E27200BD9F97 /* GutenbergViewController+Localization.swift */, FFC02B82222687BF00E64FDE /* GutenbergImageLoader.swift */, 4625B5472537875E00C04AAD /* Collapsable Header */, @@ -13009,9 +13093,8 @@ isa = PBXGroup; children = ( 8B05D29223AA572A0063B9AA /* GutenbergMediaEditorImage.swift */, - 7E407120237163B8003627FA /* GutenbergStockPhotos.swift */, 7E4071392372AD54003627FA /* GutenbergFilesAppMediaSource.swift */, - C856748E243EF177001A995E /* GutenbergTenorMediaPicker.swift */, + C856748E243EF177001A995E /* GutenbergExternalMeidaPicker.swift */, ); path = Utils; sourceTree = ""; @@ -13214,6 +13297,14 @@ name = "Lottie Animations"; sourceTree = ""; }; + 80348F2C2AF8708A0045CCD3 /* Coordinators */ = { + isa = PBXGroup; + children = ( + 80348F2D2AF870A70045CCD3 /* AllDomainsAddDomainCoordinator.swift */, + ); + path = Coordinators; + sourceTree = ""; + }; 803BB97E295957A200B3F6D6 /* Root View */ = { isa = PBXGroup; children = ( @@ -13380,7 +13471,7 @@ 43D74ACD20F906DD004AD934 /* InlineEditableNameValueCell.xib */, 98BAA7C026F925F70073A2F9 /* InlineEditableSingleLineCell.swift */, 98BAA7BF26F925F60073A2F9 /* InlineEditableSingleLineCell.xib */, - 1730D4A21E97E3E400326B7C /* MediaItemTableViewCells.swift */, + 1730D4A21E97E3E400326B7C /* MediaItemHeaderView.swift */, FF00889E204E01AE007CCE66 /* MediaQuotaCell.swift */, FF00889C204DFF77007CCE66 /* MediaQuotaCell.xib */, E14977171C0DC0770057CD60 /* MediaSizeSliderCell.swift */, @@ -13448,6 +13539,7 @@ isa = PBXGroup; children = ( 83BFAE4F2A6EBF9900C7B683 /* DashboardJetpackSocialCardCellTests.swift */, + FE34ACD12B174AE700108B3C /* DashboardBloganuaryCardCellTests.swift */, ); path = Cards; sourceTree = ""; @@ -13508,8 +13600,8 @@ 82301B8E1E787420009C9C4E /* AppRatingUtilityTests.swift */, F551E7F623FC9A5C00751212 /* Collection+RotateTests.swift */, E180BD4B1FB462FF00D0D781 /* CookieJarTests.swift */, - 4A266B90282B13A70089CF3D /* CoreDataTestCase.swift */, E1AB5A391E0C464700574B4E /* DelayTests.swift */, + 4A266B90282B13A70089CF3D /* CoreDataTestCase.swift */, 173D82E6238EE2A7008432DA /* FeatureFlagTests.swift */, E1EBC3721C118ED200F638E0 /* ImmuTableTest.swift */, 4A266B8E282B05210089CF3D /* JSONObjectTests.swift */, @@ -13525,6 +13617,10 @@ 1ABA150722AE5F870039311A /* WordPressUIBundleTests.swift */, FE6BB1452932289B001E5F7A /* ContentMigrationCoordinatorTests.swift */, 0C896DE62A3A832B00D7D4E7 /* SiteVisibilityTests.swift */, + 0CA10FA42ADB286300CE75AC /* StringRankedSearchTests.swift */, + 4AD862E42AFAEF1700A07557 /* PostsListAPIStub.swift */, + 0C1DB60A2B0A9A570028F200 /* ImageDownloaderTests.swift */, + 4A5598842B05AC180083C220 /* PagesListTests.swift */, 93B853211B44165B0064FE72 /* Analytics */, DC06DFF727BD52A100969974 /* BackgroundTasks */, FE32E7EF284496F500744D80 /* Blogging Prompts */, @@ -13640,7 +13736,6 @@ 292CECFF1027259000BD407D /* SFHFKeychainUtils.m */, 594399911B45091000539E21 /* WPAuthTokenIssueSolver.h */, 594399921B45091000539E21 /* WPAuthTokenIssueSolver.m */, - 0807CB711CE670A800CDBDAC /* WPContentSearchHelper.swift */, 5D6C4B051B603E03005E3C43 /* WPContentSyncHelper.swift */, E114D798153D85A800984182 /* WPError.h */, E114D799153D85A800984182 /* WPError.m */, @@ -13662,6 +13757,8 @@ 175721152754D31F00DE38BC /* AppIcon.swift */, 4A2172F728EAACFF0006F4F1 /* BlogQuery.swift */, 801D9519291AC0B00051993E /* OverlayFrequencyTracker.swift */, + 0CA10F722ADB014C00CE75AC /* StringRankedSearch.swift */, + 4A5DE7372B0D511900363171 /* PageTree.swift */, ); path = Utility; sourceTree = ""; @@ -13695,11 +13792,13 @@ 8584FDB719243E550019C02E /* System */ = { isa = PBXGroup; children = ( + 01B5C3C52AE7FC3B007055BB /* UITesting */, 0107E0F128FD6A3100DE87DB /* Constants */, 803BB97E295957A200B3F6D6 /* Root View */, BE87E19E1BD4052F0075D45B /* 3DTouch */, B5FD4520199D0C9A00286FBB /* WordPress-Bridging-Header.h */, 1749965E2271BF08007021BD /* WordPressAppDelegate.swift */, + 0CB54F562AEC320700582080 /* WordPressAppDelegate+PostCoordinatorDelegate.swift */, 43B0BA952229927F00328C69 /* WordPressAppDelegate+openURL.swift */, F1E3536A25B9F74C00992E3A /* WindowManager.swift */, 591A428D1A6DC6F2003807A6 /* WPGUIConstants.h */, @@ -13712,6 +13811,7 @@ 85A1B6721742E7DB00BA5E35 /* Analytics */ = { isa = PBXGroup; children = ( + F41D98D62B389735004EC050 /* DashboardDynamicCardAnalyticsEvent.swift */, 3F28CEA62A4ACA3500B79686 /* AnalyticsEventTracking.swift */, FE7FAABC299A98B90032A6F2 /* EventTracker.swift */, 175CC17B2723103000622FB4 /* WPAnalytics+Domains.swift */, @@ -13765,6 +13865,7 @@ children = ( 011F52BB2A15324800B04114 /* Free to Paid Plans */, 0118968D29D1EAC900D34BA9 /* Domains */, + 0830538A2B272F9C00B889FE /* Dynamic */, FA70024E29DC3B6100E874FD /* Activity Log */, FA98B61429A3B71E0071AAE8 /* Blaze */, 80D9CFF229DCA51C00FE3400 /* Pages */, @@ -13780,6 +13881,7 @@ F4026B1C2A1BC88A00CC7781 /* DashboardDomainRegistrationCardCell.swift */, 83796698299C048E004A92B9 /* DashboardJetpackInstallCardCell.swift */, 83BFAE472A6EBF1F00C7B683 /* DashboardJetpackSocialCardCell.swift */, + F413F7792B2A183E00A64A94 /* BlogDashboardDynamicCardCell.swift */, ); path = Cards; sourceTree = ""; @@ -13798,9 +13900,9 @@ 8B6214E127B1B2D6001DF7B6 /* Service */ = { isa = PBXGroup; children = ( + 0CB4056A29C78F06008EED0A /* BlogDashboardPersonalizationService.swift */, 8B6214E227B1B2F3001DF7B6 /* BlogDashboardService.swift */, 8BBC778A27B5531700DBA087 /* BlogDashboardPersistence.swift */, - 0CB4056A29C78F06008EED0A /* BlogDashboardPersonalizationService.swift */, 8BAC9D9D27BAB97E008EA44C /* BlogDashboardRemoteEntity.swift */, 8B15CDAA27EB89AC00A75749 /* BlogDashboardPostsParser.swift */, ); @@ -13810,6 +13912,7 @@ 8B6214E427B1B420001DF7B6 /* Dashboard */ = { isa = PBXGroup; children = ( + F41D98E62B39D01B004EC050 /* Dynamic Cards */, 8B6214E527B1B446001DF7B6 /* BlogDashboardServiceTests.swift */, 0CB4056D29C7BA63008EED0A /* BlogDashboardPersonalizationServiceTests.swift */, 8BE9AB8727B6B5A300708E45 /* BlogDashboardPersistenceTests.swift */, @@ -13840,19 +13943,11 @@ 8B7623352384372200AB3EE7 /* Pages */ = { isa = PBXGroup; children = ( - 8B7623362384372F00AB3EE7 /* Controllers */, + FA141F312AF139A200C9A653 /* PageMenuViewModelTests.swift */, ); path = Pages; sourceTree = ""; }; - 8B7623362384372F00AB3EE7 /* Controllers */ = { - isa = PBXGroup; - children = ( - 8B7623372384373E00AB3EE7 /* PageListViewControllerTests.swift */, - ); - path = Controllers; - sourceTree = ""; - }; 8B7F51C724EED488008CF5B5 /* Analytics */ = { isa = PBXGroup; children = ( @@ -13980,6 +14075,8 @@ 8BEE845927B1DC9D0001A93C /* dashboard-200-with-drafts-and-scheduled.json */, 8B45C12527B2A27400EA3257 /* dashboard-200-with-drafts-only.json */, 8B2D4F5227ECE089009B085C /* dashboard-200-without-posts.json */, + F48EBF8C2B3262D5004CD561 /* dashboard-200-with-multiple-dynamic-cards.json */, + F48EBF912B333111004CD561 /* dashboard-200-with-only-one-dynamic-card.json */, ); path = Dashboard; sourceTree = ""; @@ -13988,7 +14085,6 @@ isa = PBXGroup; children = ( 8B074A4F27AC3A64003A2EB8 /* BlogDashboardViewModel.swift */, - 8BEE846027B1DE0E0001A93C /* DashboardCardModel.swift */, 806E53E027E01C7F0064315E /* DashboardStatsViewModel.swift */, ); path = ViewModel; @@ -14067,13 +14163,8 @@ 5749984522FA0EB900CE86ED /* Utils */, 937E3AB51E3EBE1600CDA01A /* PostEditorStateTests.swift */, E10F3DA01E5C2CE0008FAADA /* PostListFilterTests.swift */, - E684383F221F5A2200752258 /* PostListExcessiveLoadMoreTests.swift */, - 570BFD8C22823DE5007859A8 /* PostActionSheetTests.swift */, - 57AA8492228790AA00D3C2A2 /* PostCardCellTests.swift */, - 577C2AAA22936DCB00AD1F03 /* PostCardCellGhostableTests.swift */, 57D6C83D22945A10003DDC7E /* PostCompactCellTests.swift */, 575E126222973EBB0041B3EB /* PostCompactCellGhostableTests.swift */, - 570265162298960B00F2214C /* PostListTableViewHandlerTests.swift */, ); name = Post; sourceTree = ""; @@ -14091,6 +14182,8 @@ 93FA59DA18D88BDB001446BC /* Services */ = { isa = PBXGroup; children = ( + FEF207F02AF287860025CB2C /* BloggingPrompts */, + F4B0F4812ADED999003ABC61 /* Domains */, F4EDAA4F29A66DA900622D3D /* Reader Post */, 5D44EB361986D8BA008B7175 /* ReaderSiteService.h */, F4F09CCB2989BF5B00A5F420 /* ReaderSiteService_Internal.h */, @@ -14108,7 +14201,6 @@ F12FA5D82428FA8F0054DA21 /* AuthenticationService.swift */, FA4BC0CF2996A589005EB077 /* BlazeService.swift */, 46F584812624DCC80010A723 /* BlockEditorSettingsService.swift */, - FEA6517A281C491C002EA086 /* BloggingPromptsService.swift */, 822D60B81F4CCC7A0016C46D /* BlogJetpackSettingsService.swift */, 93C1148318EDF6E100DAC95C /* BlogService.h */, 93C1148418EDF6E100DAC95C /* BlogService.m */, @@ -14123,7 +14215,6 @@ FEFC0F882731182C001F7F1D /* CommentService+Replies.swift */, AB2211D125ED68E300BF72FC /* CommentServiceRemoteFactory.swift */, E16A76F21FC4766900A661E3 /* CredentialsService.swift */, - 1702BBDF1CF3034E00766A33 /* DomainsService.swift */, 7E7BEF7222E1DD27009A880D /* EditorSettingsService.swift */, FA00863C24EB68B100C863F2 /* FollowCommentsService.swift */, B5772AC31C9C7A070031F97E /* GravatarService.swift */, @@ -14149,8 +14240,6 @@ FF5371621FDFF64F00619A3F /* MediaService.swift */, FF8791BA1FBAF4B400AD86E6 /* MediaHelper.swift */, E1C5457D1C6B962D001CEB0E /* MediaSettings.swift */, - FF4C069E206560E500E0B2BC /* MediaThumbnailCoordinator.swift */, - 087EBFA71F02313E001F7ACE /* MediaThumbnailService.swift */, 0C75E26D2A9F63CB00B784E5 /* MediaImageService.swift */, 08AAD69D1CBEA47D002B2418 /* MenusService.h */, 08AAD69E1CBEA47D002B2418 /* MenusService.m */, @@ -14205,7 +14294,6 @@ FA4ADAD71C50687400F858D7 /* SiteManagementService.swift */, D8CB561F2181A8CE00554EAE /* SiteSegmentsService.swift */, B0F2EFBE259378E600C7EB6D /* SiteSuggestionService.swift */, - D8A468E421828D940094B82F /* SiteVerticalsService.swift */, B03B9233250BC593000A40AF /* SuggestionService.swift */, 59A9AB331B4C33A500A433DC /* ThemeService.h */, 59A9AB341B4C33A500A433DC /* ThemeService.m */, @@ -14447,6 +14535,7 @@ 98A047702821BFB6001B4E2D /* Blogging Prompts */ = { isa = PBXGroup; children = ( + FE34ACD82B17A7CA00108B3C /* Bloganuary */, 98A047712821CEBF001B4E2D /* BloggingPromptsViewController.swift */, 98A047742821D069001B4E2D /* BloggingPromptsViewController.storyboard */, 98517E5828220411001FFD45 /* BloggingPromptTableViewCell.swift */, @@ -14576,7 +14665,6 @@ 9A22D9BE214A6B9800BAEAF2 /* Utils */ = { isa = PBXGroup; children = ( - 9A22D9BF214A6BCA00BAEAF2 /* PageListTableViewHandler.swift */, F16C35D523F33DE400C81331 /* PageAutoUploadMessageProvider.swift */, ); name = Utils; @@ -14804,6 +14892,7 @@ F57402A5235FF71F00374346 /* Scheduling */, 4349B0A6218A2E810034118A /* Revisions */, 5D1EBF56187C9B95003393F8 /* Categories */, + 0CD9CCA12AD8313E0044A33C /* Search */, 5DF3DD691A9377220051A229 /* Controllers */, 5DF3DD6B1A93773B0051A229 /* Style */, 5D09CBA61ACDE532007A23BD /* Utils */, @@ -14822,17 +14911,15 @@ 32CA6EBF2390C61F00B51347 /* PostListEditorPresenter.swift */, 430693731DD25F31009398A2 /* PostPost.storyboard */, 43D54D121DCAA070007F575F /* PostPostViewController.swift */, - 5DBFC8A81A9BE07B00E00DE4 /* Posts.storyboard */, 5D62BAD818AAAE9B0044E5F7 /* PostSettingsViewController_Internal.h */, ACBAB5FC0E121C7300F38795 /* PostSettingsViewController.h */, ACBAB5FD0E121C7300F38795 /* PostSettingsViewController.m */, + 0CFE9AC82AF52D3B00B8F659 /* PostSettingsViewController+Swift.swift */, FFEECFFB2084DE2B009B8CDB /* PostSettingsViewController+FeaturedImageUpload.swift */, 83914BD32A2EA03A0017A588 /* PostSettingsViewController+JetpackSocial.swift */, 8B260D7D2444FC9D0010F756 /* PostVisibilitySelectorViewController.swift */, 593F26601CAB00CA00F14073 /* PostSharingController.swift */, E155EC711E9B7DCE009D7F63 /* PostTagPickerViewController.swift */, - 5DB3BA0318D0E7B600F3F3E9 /* WPPickerView.h */, - 5DB3BA0418D0E7B600F3F3E9 /* WPPickerView.m */, C9F1D4B92706EEEB00BDF917 /* HomepageEditorNavigationBarManager.swift */, ); path = Post; @@ -14998,7 +15085,6 @@ B5EEDB961C91F10400676B2B /* Blog+Interface.swift */, E11C4B6F2010930B00A6619C /* Blog+Jetpack.swift */, 17D9362224729FB6008B2205 /* Blog+HomepageSettings.swift */, - 4A2172FD28F688890006F4F1 /* Blog+Media.swift */, B5D889401BEBE30A007C4684 /* BlogSettings.swift */, B55F1AA71C10936600FD04D4 /* BlogSettings+Discussion.swift */, 8217380A1FE05EE600BEC94C /* BlogSettings+DateAndTimeFormat.swift */, @@ -15060,8 +15146,6 @@ isa = PBXGroup; children = ( DC590CFE26F205C400EB0F73 /* Time Zone */, - E1EEFAD91CC4CC5700126533 /* Confirmable.h */, - B55086201CC15CCB004EADB4 /* PromptViewController.swift */, E185042E1EE6ABD9005C234C /* Restorer.swift */, 3F43602E23F31D48001DEE70 /* ScenePresenter.swift */, E14B40FE1C58B93F005046F6 /* SettingsCommon.swift */, @@ -15276,7 +15360,6 @@ AE2F3127270B6DE200B2A9C2 /* NSMutableAttributedString+ApplyAttributesToQuotes.swift */, B574CE141B5E8EA800A84FFD /* NSMutableAttributedString+Helpers.swift */, E1CFC1561E0AC8FF001DF9E9 /* Pattern.swift */, - FF70A3201FD5840500BC270D /* PHAsset+Metadata.swift */, 83C972DF281C45AB0049E1FE /* Post+BloggingPrompts.swift */, FFD12D5D1FE1998D00F20A00 /* Progress+Helpers.swift */, AE2F3124270B6DA000B2A9C2 /* Scanner+QuotedText.swift */, @@ -15288,13 +15371,13 @@ B5969E2120A49E86005E9DF1 /* UIAlertController+Helpers.swift */, 1707CE411F3121750020B7FE /* UICollectionViewCell+Tint.swift */, E1823E6B1E42231C00C19F53 /* UIEdgeInsets.swift */, + 0C3090212B12A5C90071C551 /* UIButton+Extensions.swift */, B58C4EC9207C5E1900E32E4D /* UIImage+Assets.swift */, FF70A3211FD5840500BC270D /* UIImage+Export.swift */, FADC40A72A8BC2A200C19997 /* UIImage+Gravatar.swift */, B5E94D141FE04815000E7C20 /* UIImageView+SiteIcon.swift */, 1790A4521E28F0ED00AE54C2 /* UINavigationController+Helpers.swift */, 177E7DAC1DD0D1E600890467 /* UINavigationController+SplitViewFullscreen.swift */, - 171CC15724FCEBF7008B7180 /* UINavigationBar+Appearance.swift */, 7326A4A7221C8F4100B4EB8C /* UIStackView+Subviews.swift */, 8BF0B606247D88EB009A7457 /* UITableViewCell+enableDisable.swift */, D829C33A21B12EFE00B09F12 /* UIView+Borders.swift */, @@ -15303,6 +15386,7 @@ 9A162F2421C26F5F00FDC035 /* UIViewController+ChildViewController.swift */, F1E72EB9267790100066FF91 /* UIViewController+Dismissal.swift */, 9F3EFCA2208E308900268758 /* UIViewController+Notice.swift */, + 0CD9FB7D2AF9C4DB009D9C7A /* UIBarButtonItem+Extensions.swift */, 9A4E939E2268D9B400E14823 /* UIViewController+NoResults.swift */, 0845B8C51E833C56001BA771 /* URL+Helpers.swift */, F5E1BBDF253B74240091E9A6 /* URLQueryItem+Parameters.swift */, @@ -15461,6 +15545,7 @@ 570B037622F1FFF6009D8411 /* PostCoordinatorFailedPostsFetcherTests.swift */, 57569CF1230485680052EE14 /* PostAutoUploadInteractorTests.swift */, 4A2C73F62A9585B000ACE79E /* PostRepositoryTests.swift */, + 4AA7EE0E2ADF7367007D261D /* PostRepositoryPostsListTests.swift */, 57D66B9C234BB78B005A2D74 /* PostServiceWPComTests.swift */, 57240223234E5BE200227067 /* PostServiceSelfHostedTests.swift */, 575802122357C41200E4C63C /* MediaCoordinatorTests.swift */, @@ -15751,6 +15836,7 @@ CC7CB97222B1510900642EE9 /* SignupTests.swift */, EAD2BF4127594DAB00A847BB /* StatsTests.swift */, 6E5BA46826A59D620043A6F2 /* SupportScreenTests.swift */, + 1D0402722B10FA9100888C30 /* AppSettingsTests.swift */, ); path = Tests; sourceTree = ""; @@ -15818,6 +15904,7 @@ isa = PBXGroup; children = ( C3C2F84528AC8BC700937E45 /* JetpackBannerScrollVisibilityTests.swift */, + 3FE6D31D2B0705D400D14923 /* JetpackBrandingVisibilityTests.swift */, ); path = Jetpack; sourceTree = ""; @@ -15862,8 +15949,8 @@ C59D3D480E6410BC00AA591D /* Categories */ = { isa = PBXGroup; children = ( - 08C388681ED78EE70057BE49 /* Media+WPMediaAsset.h */, - 08C388691ED78EE70057BE49 /* Media+WPMediaAsset.m */, + 08C388681ED78EE70057BE49 /* Media+Extensions.h */, + 08C388691ED78EE70057BE49 /* Media+Extensions.m */, B57B99DC19A2DBF200506504 /* NSObject+Helpers.h */, B57B99DD19A2DBF200506504 /* NSObject+Helpers.m */, 8261B4CB1EA8E13700668298 /* SVProgressHUD+Dismiss.h */, @@ -15874,8 +15961,6 @@ 5D97C2F215CAF8D8009B44DD /* UINavigationController+KeyboardFix.m */, E2AA87A318523E5300886693 /* UIView+Subviews.h */, E2AA87A418523E5300886693 /* UIView+Subviews.m */, - E69BA1961BB5D7D300078740 /* WPStyleGuide+ReadableMargins.h */, - E69BA1971BB5D7D300078740 /* WPStyleGuide+ReadableMargins.m */, 31EC15061A5B6675009FC8B3 /* WPStyleGuide+Suggestions.h */, 31EC15071A5B6675009FC8B3 /* WPStyleGuide+Suggestions.m */, 8067340827E3A50900ABC95E /* UIViewController+RemoveQuickStart.h */, @@ -15912,7 +15997,6 @@ isa = PBXGroup; children = ( 3F46EEC028BC48D1004F02B2 /* New Landing Screen */, - C7124E912638905B00929318 /* StarFieldView.swift */, C7124E4D2638528F00929318 /* JetpackPrologueViewController.swift */, C7124E4C2638528F00929318 /* JetpackPrologueViewController.xib */, C7D30C642638B07A00A1695B /* JetpackPrologueStyleGuide.swift */, @@ -15949,27 +16033,6 @@ path = Coordinators; sourceTree = ""; }; - C72A4F66264088D1009CA633 /* View Models */ = { - isa = PBXGroup; - children = ( - C7F7ACBD261E4F0600CE547F /* JetpackErrorViewModel.swift */, - C72A4F67264088E4009CA633 /* JetpackNotFoundErrorViewModel.swift */, - C72A4F7A26408943009CA633 /* JetpackNotWPErrorViewModel.swift */, - C72A4F8D26408C73009CA633 /* JetpackNoSitesErrorViewModel.swift */, - ); - path = "View Models"; - sourceTree = ""; - }; - C72A4FB12641837A009CA633 /* Login Error */ = { - isa = PBXGroup; - children = ( - C72A4F66264088D1009CA633 /* View Models */, - C7F7AC73261CF1F300CE547F /* JetpackLoginErrorViewController.swift */, - C7F7AC74261CF1F300CE547F /* JetpackLoginErrorViewController.xib */, - ); - path = "Login Error"; - sourceTree = ""; - }; C72A52CD2649B14B009CA633 /* System */ = { isa = PBXGroup; children = ( @@ -16052,7 +16115,6 @@ isa = PBXGroup; children = ( F4F9D5E82909615D00502576 /* WordPress-to-Jetpack Migration */, - C72A4FB12641837A009CA633 /* Login Error */, ); path = ViewRelated; sourceTree = ""; @@ -16097,15 +16159,13 @@ isa = PBXGroup; children = ( C81CCD5D243AEC8200A83E27 /* TenorAPI */, - C81CCD7A243BF7A600A83E27 /* NoResultsTenorConfiguration.swift */, C81CCD79243BF7A600A83E27 /* TenorDataLoader.swift */, C81CCD73243BF7A500A83E27 /* TenorDataSource.swift */, C81CCD74243BF7A500A83E27 /* TenorMedia.swift */, - C81CCD75243BF7A500A83E27 /* TenorMediaGroup.swift */, C81CCD72243BF7A500A83E27 /* TenorPageable.swift */, - C81CCD76243BF7A600A83E27 /* TenorPicker.swift */, C81CCD78243BF7A600A83E27 /* TenorResultsPage.swift */, C81CCD77243BF7A600A83E27 /* TenorService.swift */, + 0C1DB60C2B0BDA740028F200 /* TenorWelcomeView.swift */, C81CCD71243BF7A500A83E27 /* TenorStrings.swift */, ); path = Tenor; @@ -16146,14 +16206,6 @@ path = Views; sourceTree = ""; }; - C9FE382029C203EE00D39841 /* Extensions */ = { - isa = PBXGroup; - children = ( - C995C22129D306DD00ACEF43 /* URL+WidgetSource.swift */, - ); - path = Extensions; - sourceTree = ""; - }; C9FE382529C204A500D39841 /* ViewProvider */ = { isa = PBXGroup; children = ( @@ -16174,15 +16226,6 @@ path = Models; sourceTree = ""; }; - C9FE383329C2063900D39841 /* Widgets */ = { - isa = PBXGroup; - children = ( - C9B477AF29CC35C5008CBF49 /* WidgetDataReaderTests.swift */, - C995C22529D30AB000ACEF43 /* WidgetUrlSourceTests.swift */, - ); - path = Widgets; - sourceTree = ""; - }; C9FE383B29C2A3A300D39841 /* Configs */ = { isa = PBXGroup; children = ( @@ -16235,6 +16278,7 @@ children = ( EA78189327596B2F00554DFA /* ContactUsScreen.swift */, 6EC71EC22689A67400ACC0A0 /* SupportScreen.swift */, + 1D0402752B10FB9E00888C30 /* AppSettingsScreen.swift */, ); path = Me; sourceTree = ""; @@ -16337,12 +16381,8 @@ D8380CA72194287B00250609 /* Web Address */ = { isa = PBXGroup; children = ( - 08EA036529C9B50500B72A87 /* DesignSystem */, D82253E3219956540014D0E2 /* AddressTableViewCell.swift */, D853723921952DAF0076F461 /* WebAddressStep.swift */, - D82253DD2199418B0014D0E2 /* WebAddressWizardContent.swift */, - 467D3DF925E4436000EB9CB0 /* SitePromptView.swift */, - 467D3E0B25E4436D00EB9CB0 /* SitePromptView.xib */, 08CBC77829AE6CC4000026E7 /* SiteCreationEmptySiteTemplate.swift */, F4FE743329C3767300AC2729 /* AddressTableViewCell+ViewModel.swift */, ); @@ -16380,10 +16420,8 @@ children = ( D88A64AF208DA093008AE9BC /* StockPhotosResultsPageTests.swift */, D88A64AB208D9B09008AE9BC /* StockPhotosPageableTests.swift */, - D88A64A7208D9733008AE9BC /* ThumbnailCollectionTests.swift */, + D88A64A7208D9733008AE9BC /* StockPhotosThumbnailCollectionTests.swift */, D88A64A1208D8F05008AE9BC /* StockPhotosMediaTests.swift */, - D88A649F208D8B7D008AE9BC /* StockPhotosMediaGroupTests.swift */, - D88A649B208D7D81008AE9BC /* StockPhotosDataSourceTests.swift */, D88A6491208D7A0A008AE9BC /* MockStockPhotosService.swift */, ); name = "Stock Photos"; @@ -16397,12 +16435,8 @@ D8A3A5AE206A442800992576 /* StockPhotosDataSource.swift */, 7EBB4125206C388100012D98 /* StockPhotosService.swift */, D88A6493208D7AD0008AE9BC /* DefaultStockPhotosService.swift */, - D88A6495208D7B0B008AE9BC /* NullStockPhotosService.swift */, - D8A3A5B0206A49A100992576 /* StockPhotosMediaGroup.swift */, D8A3A5B2206A49BF00992576 /* StockPhotosMedia.swift */, - D8A3A5B4206A4C7800992576 /* StockPhotosPicker.swift */, - 98F1B1292111017900139493 /* NoResultsStockPhotosConfiguration.swift */, - D80BC79D20746B4100614A59 /* MediaPickingContext.swift */, + 0CE538CF2B0E317000834BA2 /* StockPhotosWelcomeView.swift */, D83CA3A420842CAF0060E310 /* Pageable.swift */, D83CA3A620842CD90060E310 /* ResultsPage.swift */, D83CA3A820842D190060E310 /* StockPhotosPageable.swift */, @@ -16587,7 +16621,6 @@ 852416D01A12ED2D0030700C /* Utility */, BE20F5E11B2F738E0020694C /* ViewRelated */, 3F3D8548251E63DF001CA4D2 /* What's New */, - C9FE383329C2063900D39841 /* Widgets */, FF9839A71CD3960600E85258 /* WordPressAPI */, 3FB6D13A2ACFF63800768C07 /* MySiteViewModelTests.swift */, ); @@ -16776,6 +16809,7 @@ F127FFD724213B5600B9D41A /* atomic-get-authentication-cookie-success.json */, 93CD939219099BE70049096E /* authtoken.json */, FE003F61282E73E6006F8D1D /* blogging-prompts-fetch-success.json */, + FEF7F33F2AFEA0C200F793FC /* blogging-prompts-bloganuary.json */, FEFC0F8D27313DCF001F7F1D /* comments-v2-success.json */, 4A76A4BE29D4F0A500AABF4B /* reader-post-comments-success.json */, FEFC0F8F27315634001F7F1D /* empty-array.json */, @@ -16900,7 +16934,6 @@ 9A09F914230C3E9700F42AB7 /* StoreFetchingStatus.swift */, 24ADA24B24F9A4CB001B5DAE /* RemoteFeatureFlagStore.swift */, 3F3CA64F25D3003C00642A89 /* StatsWidgetsStore.swift */, - 08A4E128289D202F001D9EC7 /* UserPersistentStore.swift */, 08A4E12B289D2337001D9EC7 /* UserPersistentRepository.swift */, 08E39B4428A3DEB200874CB8 /* UserPersistentStoreFactory.swift */, 0878580228B4CF950069F96C /* UserPersistentRepositoryUtility.swift */, @@ -17242,6 +17275,41 @@ path = Font; sourceTree = ""; }; + F413F7832B2B251A00A64A94 /* Models */ = { + isa = PBXGroup; + children = ( + 8BF9E03227B1A8A800915B27 /* DashboardCard.swift */, + 8BEE846027B1DE0E0001A93C /* DashboardCardModel.swift */, + F413F7872B2B253A00A64A94 /* DashboardCard+Personalization.swift */, + F48EBF892B2F94DD004CD561 /* BlogDashboardAnalyticPropertiesProviding.swift */, + ); + path = Models; + sourceTree = ""; + }; + F4141EEF2AE99EE2000D2AAE /* Views */ = { + isa = PBXGroup; + children = ( + 80348F322AF880820045CCD3 /* DomainPurchaseChoicesView.swift */, + 80348F302AF87FEA0045CCD3 /* AllDomainsListViewController.swift */, + 08240C2D2AB8A2DD00E7AEA8 /* AllDomainsListCardView.swift */, + F4141EE22AE7152F000D2AAE /* AllDomainsListViewController+Strings.swift */, + F4141EE72AE72DC4000D2AAE /* AllDomainsListTableViewCell.swift */, + F4141EE92AE74ADA000D2AAE /* AllDomainsListActivityIndicatorTableViewCell.swift */, + F46546282AED89790017E3D1 /* AllDomainsListEmptyView.swift */, + ); + path = Views; + sourceTree = ""; + }; + F4141EF02AE99F14000D2AAE /* View Models */ = { + isa = PBXGroup; + children = ( + F4141EE52AE71AF0000D2AAE /* AllDomainsListViewModel.swift */, + F4141EEB2AE945C7000D2AAE /* AllDomainsListItemViewModel.swift */, + F465462C2AEF22070017E3D1 /* AllDomainsListViewModel+Strings.swift */, + ); + path = "View Models"; + sourceTree = ""; + }; F41BDD772910AFB900B7F2B0 /* Navigation */ = { isa = PBXGroup; children = ( @@ -17252,6 +17320,15 @@ path = Navigation; sourceTree = ""; }; + F41D98E62B39D01B004EC050 /* Dynamic Cards */ = { + isa = PBXGroup; + children = ( + F41D98E02B39C5CE004EC050 /* BlogDashboardDynamicCardCoordinatorTests.swift */, + F41D98E72B39E14F004EC050 /* DashboardDynamicCardAnalyticsEventTests.swift */, + ); + path = "Dynamic Cards"; + sourceTree = ""; + }; F41E4E8F28F1949D001880C6 /* App Icons */ = { isa = PBXGroup; children = ( @@ -17622,6 +17699,15 @@ path = Analytics; sourceTree = ""; }; + F4B0F4812ADED999003ABC61 /* Domains */ = { + isa = PBXGroup; + children = ( + 1702BBDF1CF3034E00766A33 /* DomainsService.swift */, + F4B0F4822ADED9B5003ABC61 /* DomainsService+AllDomains.swift */, + ); + path = Domains; + sourceTree = ""; + }; F4C1FC612A4480F700AD7CB0 /* Privacy Settings */ = { isa = PBXGroup; children = ( @@ -17632,6 +17718,14 @@ path = "Privacy Settings"; sourceTree = ""; }; + F4D1401E2AFD9B8200961797 /* Transfer Domains */ = { + isa = PBXGroup; + children = ( + F4D1401F2AFD9B9700961797 /* TransferDomainsWebViewController.swift */, + ); + path = "Transfer Domains"; + sourceTree = ""; + }; F4D829602930E9DD00038726 /* Delete WordPress */ = { isa = PBXGroup; children = ( @@ -17672,6 +17766,14 @@ path = "Reader Post"; sourceTree = ""; }; + F4F7B24F2AF8EBCA00207282 /* Domain Details */ = { + isa = PBXGroup; + children = ( + F4F7B2502AF8EBDB00207282 /* DomainDetailsWebViewController.swift */, + ); + path = "Domain Details"; + sourceTree = ""; + }; F4F9D5E82909615D00502576 /* WordPress-to-Jetpack Migration */ = { isa = PBXGroup; children = ( @@ -17724,7 +17826,6 @@ F5AE43E325DD02C0003675F4 /* StoryEditor.swift */, F504D2AA25D60C5900A2764C /* StoryPoster.swift */, F504D2AB25D60C5900A2764C /* StoryMediaLoader.swift */, - 0C8B8C0E2ACDBE1900CCE50F /* DisabledVideoOverlay.swift */, ); path = Stories; sourceTree = ""; @@ -17732,7 +17833,6 @@ F53FF3A623EA722F001AD596 /* Detail Header */ = { isa = PBXGroup; children = ( - F53FF3A723EA723D001AD596 /* ActionRow.swift */, F1112AB1255C2D4600F1F746 /* BlogDetailHeaderView.swift */, F53FF3A923EA725C001AD596 /* SiteIconView.swift */, ); @@ -17761,7 +17861,6 @@ F5660D08235D1CDD00020B1E /* CalendarMonthView.swift */, 03216EC5279946CA00D444CA /* SchedulingDatePickerViewController.swift */, 03216ECB27995F3500D444CA /* SchedulingViewControllerPresenter.swift */, - F5844B6A235EAF3D007C6557 /* PartScreenPresentationController.swift */, F59AAC15235EA46D00385EE6 /* LightNavigationController.swift */, F57402A6235FF9C300374346 /* SchedulingDate+Helpers.swift */, ); @@ -18000,12 +18099,12 @@ FA73D7D7278D9E6300DF24B3 /* Blog Dashboard */ = { isa = PBXGroup; children = ( + F413F7832B2B251A00A64A94 /* Models */, 8B4DDF23278F3AED0022494D /* Cards */, 8B6214E127B1B2D6001DF7B6 /* Service */, 8BEE845B27B1DD8E0001A93C /* ViewModel */, 8BC81D6327CFC0C60057F790 /* Helpers */, FA73D7D5278D9E5D00DF24B3 /* BlogDashboardViewController.swift */, - 8BF9E03227B1A8A800915B27 /* DashboardCard.swift */, 80D9CFF929E5E6FE00FE3400 /* DashboardCardTableView.swift */, ); path = "Blog Dashboard"; @@ -18015,6 +18114,7 @@ isa = PBXGroup; children = ( FA73D7E42798765B00DF24B3 /* SitePickerViewController.swift */, + FAAEFADF2B1E29F0004AE802 /* SitePickerViewController+SiteActions.swift */, FA73D7E827987BA500DF24B3 /* SitePickerViewController+SiteIcon.swift */, FA73D7EB27987E4500DF24B3 /* SitePickerViewController+QuickStart.swift */, 17C1D67B2670E3DC006C8970 /* SiteIconPickerView.swift */, @@ -18244,6 +18344,7 @@ FEAC916D28001FC4005026E7 /* AvatarTrainView.swift */, 83B1D036282C62620061D911 /* BloggingPromptsAttribution.swift */, FE18495727F5ACBA00D26879 /* DashboardPromptsCardCell.swift */, + FE34ACCE2B1661EB00108B3C /* DashboardBloganuaryCardCell.swift */, ); path = Prompts; sourceTree = ""; @@ -18267,6 +18368,15 @@ path = ContentRenderer; sourceTree = ""; }; + FE34ACD82B17A7CA00108B3C /* Bloganuary */ = { + isa = PBXGroup; + children = ( + FE34ACD92B17AA6C00108B3C /* BloganuaryOverlayViewController.swift */, + FE6AFE422B18EDF200F76520 /* BloganuaryTracker.swift */, + ); + path = Bloganuary; + sourceTree = ""; + }; FE3D057C26C3D5A1002A51B0 /* Sharing */ = { isa = PBXGroup; children = ( @@ -18286,6 +18396,14 @@ path = Migration; sourceTree = ""; }; + FE6AFE452B1A343A00F76520 /* SoTW 2023 */ = { + isa = PBXGroup; + children = ( + FE6AFE462B1A351F00F76520 /* SOTWCardView.swift */, + ); + path = "SoTW 2023"; + sourceTree = ""; + }; FE6BB14129322798001E5F7A /* Migration */ = { isa = PBXGroup; children = ( @@ -18349,6 +18467,15 @@ path = "Blogging Prompts"; sourceTree = ""; }; + FEF207F02AF287860025CB2C /* BloggingPrompts */ = { + isa = PBXGroup; + children = ( + FEF207F22AF2882A0025CB2C /* BloggingPromptRemoteObject.swift */, + FEA6517A281C491C002EA086 /* BloggingPromptsService.swift */, + ); + path = BloggingPrompts; + sourceTree = ""; + }; FF2716901CAAC87B0006E2D4 /* UITests */ = { isa = PBXGroup; children = ( @@ -18398,7 +18525,6 @@ isa = PBXGroup; children = ( C81CCD68243AEDEC00A83E27 /* Tenor */, - FF7C89A21E3A1029000472A8 /* MediaLibraryPickerDataSourceTests.swift */, ); path = MediaPicker; sourceTree = ""; @@ -18417,7 +18543,6 @@ FF9A6E7021F9361700D36D14 /* MediaUploadHashTests.swift */, FF2EC3C12209AC19006176E1 /* GutenbergImgUploadProcessorTests.swift */, FF1B11E6238FE27A0038B93E /* GutenbergGalleryUploadProcessorTests.swift */, - 9123471A221449E200BD9F97 /* GutenbergInformativeDialogTests.swift */, 1D19C56529C9DB0A00FB0087 /* GutenbergVideoPressUploadProcessorTests.swift */, FF0B2566237A023C004E255F /* GutenbergVideoUploadProcessorTests.swift */, 4629E4222440C8160002E15C /* GutenbergCoverUploadProcessorTests.swift */, @@ -18456,6 +18581,9 @@ dependencies = ( ); name = JetpackStatsWidgets; + packageProductDependencies = ( + 3FFB3F1F2AFC70B400A742B0 /* JetpackStatsWidgetsCore */, + ); productName = WordPressHomeWidgetTodayExtension; productReference = 0107E0EA28F97D5000DE87DB /* JetpackStatsWidgets.appex */; productType = "com.apple.product-type.app-extension"; @@ -18475,6 +18603,9 @@ dependencies = ( ); name = JetpackIntents; + packageProductDependencies = ( + 3F9F23242B0AE1AC00B56061 /* JetpackStatsWidgetsCore */, + ); productName = JetpackIntents; productReference = 0107E15428FE9DB200DE87DB /* JetpackIntents.appex */; productType = "com.apple.product-type.app-extension"; @@ -18508,8 +18639,10 @@ packageProductDependencies = ( 24CE2EB0258D687A0000C297 /* WordPressFlux */, 17A8858C2757B97F0071FCA3 /* AutomatticAbout */, - 3F2B62DB284F4E0B0008CD59 /* Charts */, 3F411B6E28987E3F002513AE /* Lottie */, + 0CD9FB882AFA71C2009D9C7A /* DGCharts */, + 3F9F232A2B0B27DD00B56061 /* JetpackStatsWidgetsCore */, + 08E63FCE2B28E53400747E21 /* DesignSystem */, ); productName = WordPress; productReference = 1D6058910D05DD3D006BFB54 /* WordPress.app */; @@ -18696,7 +18829,7 @@ E16AB92514D978240047A2E5 /* Sources */, E16AB92614D978240047A2E5 /* Frameworks */, E16AB92714D978240047A2E5 /* Resources */, - 4CEE641DB5EC328B91BF3706 /* [CP] Embed Pods Frameworks */, + E42C39F1A003092A4AE8F2A2 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -18707,6 +18840,7 @@ packageProductDependencies = ( 3F3B23C12858A1B300CACE60 /* BuildkiteTestCollector */, 3F338B70289BD3040014ADC5 /* Nimble */, + 3FFB3F232AFC730C00A742B0 /* JetpackStatsWidgetsCore */, ); productName = WordPressTest; productReference = E16AB92A14D978240047A2E5 /* WordPressTest.xctest */; @@ -18765,8 +18899,10 @@ packageProductDependencies = ( FABB1FA62602FC2C00C8785C /* WordPressFlux */, 17A8858E2757BEC00071FCA3 /* AutomatticAbout */, - 3F2B62DD284F4E310008CD59 /* Charts */, 3F44DD57289C379C006334CD /* Lottie */, + 0CD9FB862AFA71B9009D9C7A /* DGCharts */, + 3F9F232C2B0B281400B56061 /* JetpackStatsWidgetsCore */, + 08E63FCC2B28E52B00747E21 /* DesignSystem */, ); productName = WordPress; productReference = FABB26522602FC2C00C8785C /* Jetpack.app */; @@ -18982,10 +19118,10 @@ 3FF1442E266F3C2400138163 /* XCRemoteSwiftPackageReference "ScreenObject" */, 3FC2C33B26C4CF0A00C6D98F /* XCRemoteSwiftPackageReference "XCUITestHelpers" */, 17A8858B2757B97F0071FCA3 /* XCRemoteSwiftPackageReference "AutomatticAbout-swift" */, - 3F2B62DA284F4E0B0008CD59 /* XCRemoteSwiftPackageReference "Charts" */, 3F3B23C02858A1B300CACE60 /* XCRemoteSwiftPackageReference "test-collector-swift" */, 3F411B6D28987E3F002513AE /* XCRemoteSwiftPackageReference "lottie-ios" */, 3F338B6F289BD3040014ADC5 /* XCRemoteSwiftPackageReference "Nimble" */, + 0CD9FB852AFA71B9009D9C7A /* XCRemoteSwiftPackageReference "Charts" */, ); productRefGroup = 19C28FACFE9D520D11CA2CBB /* Products */; projectDirPath = ""; @@ -19075,7 +19211,6 @@ 1761F17726209AEE000815EF /* wordpress-dark-icon-app-76x76.png in Resources */, 08BA4BC7298A9AD500015BD2 /* JetpackInstallPluginLogoAnimation_rtl.json in Resources */, 98F537A922496D0D00B334F9 /* SiteStatsTableHeaderView.xib in Resources */, - 5DBFC8A91A9BE07B00E00DE4 /* Posts.storyboard in Resources */, 1761F18426209AEE000815EF /* jetpack-green-icon-app-76x76@2x.png in Resources */, 98B11B8B2216536C00B7F2D7 /* StatsChildRowsView.xib in Resources */, 2FAE970C0E33B21600CA8540 /* xhtml1-transitional.dtd in Resources */, @@ -19140,7 +19275,6 @@ B558541419631A1000FAF6C3 /* Notifications.storyboard in Resources */, 983DBBAA22125DD500753988 /* StatsTableFooter.xib in Resources */, 820ADD701F3A1F88002D7F93 /* ThemeBrowserSectionHeaderView.xib in Resources */, - 467D3E0C25E4436D00EB9CB0 /* SitePromptView.xib in Resources */, 17222D8F261DDDF90047B163 /* blue-classic-icon-app-60x60@3x.png in Resources */, 746A6F571E71C691003B67E3 /* DeleteSite.storyboard in Resources */, 8091019529078CFE00FCB4EA /* JetpackFullscreenOverlayViewController.xib in Resources */, @@ -19156,7 +19290,6 @@ 1761F17A26209AEE000815EF /* wordpress-dark-icon-app-60x60@3x.png in Resources */, 80535DBC294ABBF000873161 /* JetpackAllFeaturesLogosAnimation_ltr.json in Resources */, 98563DDE21BF30C40006F5E9 /* TabbedTotalsCell.xib in Resources */, - 5DFA7EC81AF814E40072023B /* PageListTableViewCell.xib in Resources */, B5C66B781ACF073900F68370 /* NoteBlockImageTableViewCell.xib in Resources */, FEA088032696E81F00193358 /* ListTableHeaderView.xib in Resources */, B59F34A1207678480069992D /* SignupEpilogue.storyboard in Resources */, @@ -19226,7 +19359,6 @@ 17C1D6F526711ED0006C8970 /* Emoji.txt in Resources */, B5C66B761ACF072C00F68370 /* NoteBlockCommentTableViewCell.xib in Resources */, 17222D94261DDDF90047B163 /* pink-icon-app-76x76@2x.png in Resources */, - 57AA8491228715E700D3C2A2 /* PostCardCell.xib in Resources */, 3F4D035328A5BFCE00F0A4FD /* JetpackWordPressLogoAnimation_ltr.json in Resources */, 3223393E24FEC2A700BDD4BF /* ReaderDetailFeaturedImageView.xib in Resources */, E18165FD14E4428B006CE885 /* loader.html in Resources */, @@ -19263,7 +19395,6 @@ 8BE6F92A27EE26D30008BDC7 /* BlogDashboardPostCardGhostCell.xib in Resources */, CE1CCB2F2050502B000EE3AC /* MyProfileHeaderView.xib in Resources */, 98BC522D27F62B6300D6E8C2 /* BloggingPromptsFeatureDescriptionView.xib in Resources */, - 5D18FEA01AFBB17400EFEED0 /* RestorePageTableViewCell.xib in Resources */, 17222DAB261DDDF90047B163 /* blue-icon-app-60x60@3x.png in Resources */, 747D09862034837C0085EABF /* WordPressShare.js in Resources */, 9A76C32F22AFDA2100F5D819 /* world-map.svg in Resources */, @@ -19287,7 +19418,6 @@ E1B912811BB00EFD003C25B9 /* People.storyboard in Resources */, 9826AE8321B5C6A700C851FA /* LatestPostSummaryCell.xib in Resources */, 8BF281F927CE8E4100AF8CF3 /* DashboardGhostCardContent.xib in Resources */, - 5D2FB2831AE98C4600F1D4ED /* RestorePostTableViewCell.xib in Resources */, 1724DDCC1C6121D00099D273 /* Plans.storyboard in Resources */, 40A2778120191B5E00D078D5 /* PluginDirectoryCollectionViewCell.xib in Resources */, 8BD8201924BCCE8600FF25FD /* ReaderWelcomeBanner.xib in Resources */, @@ -19298,13 +19428,11 @@ 435B762A2297484200511813 /* ColorPalette.xcassets in Resources */, FAB800C225AEE3D200D5D54A /* RestoreCompleteView.xib in Resources */, 1761F18526209AEE000815EF /* pride-icon-app-60x60@3x.png in Resources */, - 402FFB1C218C27C100FF4A0B /* RegisterDomain.storyboard in Resources */, 5D732F991AE84E5400CD89E7 /* PostListFooterView.xib in Resources */, 17222DB0261DDDF90047B163 /* spectrum-classic-icon-app-83.5x83.5@2x.png in Resources */, 4D520D4F22972BC9002F5924 /* acknowledgements.html in Resources */, 17222D91261DDDF90047B163 /* blue-classic-icon-app-76x76@2x.png in Resources */, 8BDA5A70247C36C100AB124C /* ReaderDetailViewController.storyboard in Resources */, - 08EA036929C9B53000B72A87 /* Colors.xcassets in Resources */, 17222DA0261DDDF90047B163 /* pink-classic-icon-app-60x60@2x.png in Resources */, 1761F17C26209AEE000815EF /* open-source-icon-app-60x60@3x.png in Resources */, 5D6C4AFF1B603CE9005E3C43 /* EditCommentViewController.xib in Resources */, @@ -19453,6 +19581,8 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + F48EBF952B333B31004CD561 /* dashboard-200-with-multiple-dynamic-cards.json in Resources */, + F48EBF942B333550004CD561 /* dashboard-200-with-only-one-dynamic-card.json in Resources */, E1EBC3751C118EDE00F638E0 /* ImmuTableTestViewCellWithNib.xib in Resources */, E15027631E03E51500B847E3 /* notes-action-unsupported.json in Resources */, F4426FDB287F066400218003 /* site-suggestions.json in Resources */, @@ -19534,6 +19664,7 @@ 46B30B872582CA2200A25E66 /* domain-suggestions.json in Resources */, FE3D058026C3E0F2002A51B0 /* share-app-link-success.json in Resources */, 933D1F6E1EA7A402009FB462 /* TestAssets.xcassets in Resources */, + FEF7F3402AFEA0C200F793FC /* blogging-prompts-bloganuary.json in Resources */, B5EFB1D11B33630C007608A3 /* notifications-settings.json in Resources */, 17C3F8BF25E4438200EFFE12 /* notifications-button-text-content.json in Resources */, E15027611E03E51500B847E3 /* notes-action-delete.json in Resources */, @@ -19619,7 +19750,6 @@ 01A8508C2A8A126400BD8A97 /* support_chat_widget.css in Resources */, FABB1FCC2602FC2C00C8785C /* SiteStatsTableHeaderView.xib in Resources */, F41E4EEF28F247D3001880C6 /* white-on-green-icon-app-60@2x.png in Resources */, - FABB1FCD2602FC2C00C8785C /* Posts.storyboard in Resources */, F41E4EB628F225DB001880C6 /* stroke-dark-icon-app-83.5@2x.png in Resources */, FABB1FCE2602FC2C00C8785C /* StatsChildRowsView.xib in Resources */, F41E4EEE28F247D3001880C6 /* white-on-green-icon-app-76.png in Resources */, @@ -19668,11 +19798,9 @@ FABB20022602FC2C00C8785C /* StatsTableFooter.xib in Resources */, F41E4EB728F225DB001880C6 /* stroke-dark-icon-app-60@2x.png in Resources */, F41E4EB928F225DB001880C6 /* stroke-dark-icon-app-76@2x.png in Resources */, - C7F7AC76261CF1F300CE547F /* JetpackLoginErrorViewController.xib in Resources */, F46597F628E669D400D5F49A /* spectrum-on-white-icon-app-60@2x.png in Resources */, F46597A828E6600800D5F49A /* jetpack-light-icon-app-60@2x.png in Resources */, FABB20052602FC2C00C8785C /* ThemeBrowserSectionHeaderView.xib in Resources */, - FABB20072602FC2C00C8785C /* SitePromptView.xib in Resources */, 0133A7C22A8E4F6100B36E58 /* support_chat_widget_page.css in Resources */, FABB20082602FC2C00C8785C /* DeleteSite.storyboard in Resources */, FABB200A2602FC2C00C8785C /* Noticons.ttf in Resources */, @@ -19681,7 +19809,6 @@ F41E4E9828F20802001880C6 /* white-on-pink-icon-app-60@3x.png in Resources */, 98B88467261E4E4E007ED7F8 /* LikeUserTableViewCell.xib in Resources */, FABB200F2602FC2C00C8785C /* TopTotalsCell.xib in Resources */, - 08EA036B29C9C3A000B72A87 /* Colors.xcassets in Resources */, FABB20102602FC2C00C8785C /* PostPost.storyboard in Resources */, FABB20112602FC2C00C8785C /* SpaceMono-Bold.ttf in Resources */, F46597E828E6698D00D5F49A /* spectrum-on-black-icon-app-60@2x.png in Resources */, @@ -19691,7 +19818,6 @@ F46597B328E6605E00D5F49A /* neu-green-icon-app-60@2x.png in Resources */, F46597E728E6698D00D5F49A /* spectrum-on-black-icon-app-76@2x.png in Resources */, FABB20162602FC2C00C8785C /* TabbedTotalsCell.xib in Resources */, - FABB20172602FC2C00C8785C /* PageListTableViewCell.xib in Resources */, 8BE6F92E27EE27E10008BDC7 /* BlogDashboardPostCardGhostCell.xib in Resources */, FABB20182602FC2C00C8785C /* NoteBlockImageTableViewCell.xib in Resources */, FABB201B2602FC2C00C8785C /* SignupEpilogue.storyboard in Resources */, @@ -19760,7 +19886,6 @@ FABB204F2602FC2C00C8785C /* NoteBlockCommentTableViewCell.xib in Resources */, F465978828E65E1800D5F49A /* blue-on-white-icon-app-60@3x.png in Resources */, F41E4EF028F247D3001880C6 /* white-on-green-icon-app-76@2x.png in Resources */, - FABB20502602FC2C00C8785C /* PostCardCell.xib in Resources */, FABB20552602FC2C00C8785C /* ReaderDetailFeaturedImageView.xib in Resources */, F41E4EA028F20AB8001880C6 /* white-on-celadon-icon-app-60@3x.png in Resources */, F465979328E65F8A00D5F49A /* celadon-on-white-icon-app-83.5@2x.png in Resources */, @@ -19786,7 +19911,6 @@ FABB20702602FC2C00C8785C /* ReaderListStreamHeader.xib in Resources */, F465979B28E65FC800D5F49A /* dark-green-icon-app-76@2x.png in Resources */, FABB20722602FC2C00C8785C /* MyProfileHeaderView.xib in Resources */, - FABB20742602FC2C00C8785C /* RestorePageTableViewCell.xib in Resources */, FABB20762602FC2C00C8785C /* WordPressShare.js in Resources */, C7234A512832C47D0045C63F /* QRLoginVerifyAuthorizationViewController.xib in Resources */, FE3E427926A868EC00C596CE /* ListTableHeaderView.xib in Resources */, @@ -19819,7 +19943,6 @@ FABB208B2602FC2C00C8785C /* LatestPostSummaryCell.xib in Resources */, F41E4ECF28F23E00001880C6 /* green-on-white-icon-app-83.5@2x.png in Resources */, F46597C728E668B900D5F49A /* neumorphic-light-icon-app-76@2x.png in Resources */, - FABB208D2602FC2C00C8785C /* RestorePostTableViewCell.xib in Resources */, FABB208E2602FC2C00C8785C /* Plans.storyboard in Resources */, FABB208F2602FC2C00C8785C /* PluginDirectoryCollectionViewCell.xib in Resources */, F41E4ECE28F23E00001880C6 /* green-on-white-icon-app-76@2x.png in Resources */, @@ -19829,7 +19952,6 @@ FABB20962602FC2C00C8785C /* ColorPalette.xcassets in Resources */, FABB20972602FC2C00C8785C /* RestoreCompleteView.xib in Resources */, F465977B28E6598900D5F49A /* black-on-white-icon-app-76@2x.png in Resources */, - FABB20992602FC2C00C8785C /* RegisterDomain.storyboard in Resources */, FABB209A2602FC2C00C8785C /* PostListFooterView.xib in Resources */, 98E5501A265C977E00B4BE9A /* ReaderDetailLikesView.xib in Resources */, FABB209B2602FC2C00C8785C /* acknowledgements.html in Resources */, @@ -20242,26 +20364,6 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-JetpackShareExtension/Pods-JetpackShareExtension-resources.sh\"\n"; showEnvVarsInLog = 0; }; - 4CEE641DB5EC328B91BF3706 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-WordPressTest/Pods-WordPressTest-frameworks.sh", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/Gutenberg/Gutenberg.framework/Gutenberg", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/Gutenberg/hermes.framework/hermes", - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Gutenberg.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-WordPressTest/Pods-WordPressTest-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; 4F4D5C2BB6478A3E90ADC3C5 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -20437,7 +20539,6 @@ "${BUILT_PRODUCTS_DIR}/MediaEditor/MediaEditor.framework/MediaEditorHub.storyboardc", "${PODS_CONFIGURATION_BUILD_DIR}/MediaEditor/MediaEditor.bundle", "${PODS_ROOT}/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/WPMediaPicker/WPMediaPicker.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/WordPress-Aztec-iOS/WordPress-Aztec-iOS.bundle", "${PODS_ROOT}/WordPress-Editor-iOS/WordPressEditor/WordPressEditor/Assets/aztec.png", "${PODS_CONFIGURATION_BUILD_DIR}/WordPressAuthenticator/WordPressAuthenticatorResources.bundle", @@ -20457,7 +20558,6 @@ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MediaEditorHub.storyboardc", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MediaEditor.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SVProgressHUD.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/WPMediaPicker.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/WordPress-Aztec-iOS.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/aztec.png", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/WordPressAuthenticatorResources.bundle", @@ -20613,6 +20713,26 @@ shellPath = /bin/sh; shellScript = "\"$SRCROOT/../Scripts/BuildPhases/CopyGutenbergJS.sh\"\n"; }; + E42C39F1A003092A4AE8F2A2 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-WordPressTest/Pods-WordPressTest-frameworks.sh", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/Gutenberg/Gutenberg.framework/Gutenberg", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/Gutenberg/hermes.framework/hermes", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Gutenberg.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-WordPressTest/Pods-WordPressTest-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; F9C5CF0222CD5DB0007CEF56 /* Copy Alternate Internal Icons (if needed) */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -20736,7 +20856,6 @@ "${BUILT_PRODUCTS_DIR}/MediaEditor/MediaEditor.framework/MediaEditorHub.storyboardc", "${PODS_CONFIGURATION_BUILD_DIR}/MediaEditor/MediaEditor.bundle", "${PODS_ROOT}/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/WPMediaPicker/WPMediaPicker.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/WordPress-Aztec-iOS/WordPress-Aztec-iOS.bundle", "${PODS_ROOT}/WordPress-Editor-iOS/WordPressEditor/WordPressEditor/Assets/aztec.png", "${PODS_CONFIGURATION_BUILD_DIR}/WordPressAuthenticator/WordPressAuthenticatorResources.bundle", @@ -20756,7 +20875,6 @@ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MediaEditorHub.storyboardc", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MediaEditor.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SVProgressHUD.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/WPMediaPicker.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/WordPress-Aztec-iOS.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/aztec.png", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/WordPressAuthenticatorResources.bundle", @@ -20813,7 +20931,6 @@ files = ( C9B4778729C85949008CBF49 /* LockScreenStatsWidgetEntry.swift in Sources */, 0107E0B428F97D5000DE87DB /* Constants.m in Sources */, - C995C22329D306E100ACEF43 /* URL+WidgetSource.swift in Sources */, 01CE5012290A890B00A9C2E0 /* TracksConfiguration.swift in Sources */, C9C21D7C29BED18C009F68E5 /* LockScreenStatsWidgetsView.swift in Sources */, C9FE382F29C204E700D39841 /* LockScreenSingleStatViewModel.swift in Sources */, @@ -20826,10 +20943,12 @@ 0107E0BA28F97D5000DE87DB /* TodayWidgetStats.swift in Sources */, C9B477AD29CC15D9008CBF49 /* WidgetDataReader.swift in Sources */, C9FE384129C2A3D200D39841 /* LockScreenTodayViewsStatWidgetConfig.swift in Sources */, + 01E258092ACC3AA000F09666 /* iOS17WidgetAPIs.swift in Sources */, 0188FE402AA613850093EDA5 /* LockScreenMultiStatView.swift in Sources */, 0107E16128FFE99300DE87DB /* WidgetConfiguration.swift in Sources */, 0107E0BB28F97D5000DE87DB /* StatsWidgetsService.swift in Sources */, C9B477B729CD2EF7008CBF49 /* LockScreenUnconfiguredView.swift in Sources */, + 018FF1372AE67C2600F301C3 /* LockScreenFlexibleCard.swift in Sources */, 0107E0BC28F97D5000DE87DB /* StatsWidgetsView.swift in Sources */, 01D2FF6B2AA782720038E040 /* LockScreenAllTimePostsBestViewsStatWidgetConfig.swift in Sources */, 0107E0BD28F97D5000DE87DB /* AppLocalizedString.swift in Sources */, @@ -20843,7 +20962,6 @@ 0107E0C128F97D5000DE87DB /* FlexibleCard.swift in Sources */, 0107E0C228F97D5000DE87DB /* VerticalCard.swift in Sources */, C9B477A929CC13CB008CBF49 /* LockScreenSiteListProvider.swift in Sources */, - 0107E0C328F97D5000DE87DB /* ThisWeekWidgetStats.swift in Sources */, 0107E0C428F97D5000DE87DB /* HomeWidgetAllTimeData.swift in Sources */, 0107E0C528F97D5000DE87DB /* GroupedViewData.swift in Sources */, C9C21D7829BECFC7009F68E5 /* LockScreenStatsWidget.swift in Sources */, @@ -20869,11 +20987,11 @@ 0188FE4C2AA62F800093EDA5 /* LockScreenTodayLikesCommentsStatWidgetConfig.swift in Sources */, 0107E0D228F97D5000DE87DB /* UnconfiguredView.swift in Sources */, 0107E1852900059300DE87DB /* LocalizationConfiguration.swift in Sources */, - 0107E0D328F97D5000DE87DB /* Tracks+StatsWidgets.swift in Sources */, 0107E0D428F97D5000DE87DB /* HomeWidgetData.swift in Sources */, 01D2FF652AA77F790038E040 /* LockScreenTodayViewsVisitorsStatWidgetConfig.swift in Sources */, 0107E0D528F97D5000DE87DB /* HomeWidgetTodayData.swift in Sources */, 0107E0D628F97D5000DE87DB /* AllTimeWidgetStats.swift in Sources */, + 018FF1352AE6771A00F301C3 /* LockScreenVerticalCard.swift in Sources */, 0107E0D728F97D5000DE87DB /* Sites.intentdefinition in Sources */, 0107E0D828F97D5000DE87DB /* LocalizableStrings.swift in Sources */, C9FE383229C2053300D39841 /* LockScreenSingleStatView.swift in Sources */, @@ -20892,7 +21010,6 @@ files = ( 0167F4B62AAA0342005B9E42 /* WidgetConfiguration.swift in Sources */, 0107E13B28FE9DB200DE87DB /* Sites.intentdefinition in Sources */, - 0107E13C28FE9DB200DE87DB /* ThisWeekWidgetStats.swift in Sources */, 0107E16F28FFEF4500DE87DB /* AppConfiguration.swift in Sources */, 0107E13D28FE9DB200DE87DB /* HomeWidgetAllTimeData.swift in Sources */, 0107E13E28FE9DB200DE87DB /* SitesDataProvider.swift in Sources */, @@ -20923,7 +21040,6 @@ FA98A2502833F1DC003B9233 /* QuickStartChecklistConfigurable.swift in Sources */, 7462BFD02028C49800B552D8 /* ShareNoticeViewModel.swift in Sources */, 17A4A36920EE51870071C2CA /* Routes+Reader.swift in Sources */, - 0807CB721CE670A800CDBDAC /* WPContentSearchHelper.swift in Sources */, C395FB262821FE7B00AE7C11 /* RemoteSiteDesign+Thumbnail.swift in Sources */, 9A73B7152362FBAE004624A8 /* SiteStatsViewModel+AsyncBlock.swift in Sources */, 74AF4D7C1FE417D200E3EBFE /* PostUploadOperation.swift in Sources */, @@ -20938,6 +21054,7 @@ 175A650C20B6F7280023E71B /* ReaderSaveForLater+Analytics.swift in Sources */, 9A2B28F52192121400458F2A /* RevisionOperation.swift in Sources */, F4FB0ACD292587D500F651F9 /* MeHeaderViewConfiguration.swift in Sources */, + 80DB57982AF99E0900C728FF /* BlogListConfiguration.swift in Sources */, 80D9CFFA29E5E6FE00FE3400 /* DashboardCardTableView.swift in Sources */, 984B4EF320742FCC00F87888 /* ZendeskUtils.swift in Sources */, F580C3CB23D8F9B40038E243 /* AbstractPost+Dates.swift in Sources */, @@ -21024,10 +21141,12 @@ 7E4123BE20F4097B00DF8486 /* NotificationContentRangeFactory.swift in Sources */, FA332AD429C1FC7A00182FBB /* MovedToJetpackViewModel.swift in Sources */, FA3FBF8B2A2772340012FC90 /* DashboardActivityLogViewModel.swift in Sources */, + 0C700B892AE1E1940085C2EE /* PageListItemViewModel.swift in Sources */, E6D170371EF9D8D10046D433 /* SiteInfo.swift in Sources */, E16A76F31FC4766900A661E3 /* CredentialsService.swift in Sources */, F9B862C92478170A008B093C /* EncryptedLogTableViewController.swift in Sources */, 4070D75E20E6B4E4007CEBDA /* ActivityDateFormatting.swift in Sources */, + FAAEFAE02B1E29F0004AE802 /* SitePickerViewController+SiteActions.swift in Sources */, E2AA87A518523E5300886693 /* UIView+Subviews.m in Sources */, FEFC0F892731182C001F7F1D /* CommentService+Replies.swift in Sources */, 803BB98F29667BAF00B3F6D6 /* JetpackBrandingTextProvider.swift in Sources */, @@ -21042,7 +21161,6 @@ 9A162F2521C26F5F00FDC035 /* UIViewController+ChildViewController.swift in Sources */, 086C4D101E81F9240011D960 /* Media+Blog.swift in Sources */, 08E6E07E2A4C405500B807B0 /* CompliancePopoverViewModel.swift in Sources */, - 088D58A529E724F300E6C0F4 /* ColorGallery.swift in Sources */, 08216FCB1CDBF96000304BA7 /* MenuItemEditingHeaderView.m in Sources */, 17BD4A0820F76A4700975AC3 /* Routes+Banners.swift in Sources */, 1702BBDC1CEDEA6B00766A33 /* BadgeLabel.swift in Sources */, @@ -21058,7 +21176,6 @@ C3835559288B02B00062E402 /* JetpackBannerWrapperViewController.swift in Sources */, E63BBC961C5168BE00598BE8 /* SharingAuthorizationHelper.m in Sources */, 98880A4A22B2E5E400464538 /* TwoColumnCell.swift in Sources */, - 4A2172FE28F688890006F4F1 /* Blog+Media.swift in Sources */, ACBAB5FE0E121C7300F38795 /* PostSettingsViewController.m in Sources */, 08CC677E1C49B65A00153AD7 /* MenuItem.m in Sources */, E62AFB6C1DC8E593007484FC /* WPRichTextFormatter.swift in Sources */, @@ -21073,6 +21190,7 @@ 0C7E09202A4286A00052324C /* PostMetaButton.m in Sources */, 9826AE8221B5C6A700C851FA /* LatestPostSummaryCell.swift in Sources */, F47E154A29E84A9300B6E426 /* SiteCreationPurchasingWebFlowController.swift in Sources */, + FAEC116E2AEBEEA600F9DA54 /* AbstractPostMenuViewModel.swift in Sources */, 433432521E9ED18900915988 /* LoginEpilogueViewController.swift in Sources */, 0C7E09242A4286F40052324C /* PostMetaButton+Swift.swift in Sources */, 4395A1592106389800844E8E /* QuickStartTours.swift in Sources */, @@ -21081,6 +21199,7 @@ F5A34BCB25DF244F00C9654B /* KanvasCameraAnalyticsHandler.swift in Sources */, 8BD36E022395CAEA00EFFF1C /* MediaEditorOperation+Description.swift in Sources */, 839B150B2795DEE0009F5E77 /* UIView+Margins.swift in Sources */, + FE34ACCF2B1661EB00108B3C /* DashboardBloganuaryCardCell.swift in Sources */, 17F52DB72315233300164966 /* WPStyleGuide+FilterTabBar.swift in Sources */, E1DD4CCB1CAE41B800C3863E /* PagedViewController.swift in Sources */, B549BA681CF7447E0086C608 /* InvitePersonViewController.swift in Sources */, @@ -21115,6 +21234,7 @@ 8B85AEDA259230FC00ADBEC9 /* ABTest.swift in Sources */, 178DDD06266D68A3006C68C4 /* BloggingRemindersFlowIntroViewController.swift in Sources */, E1E5EE37231E47A80018E9E3 /* ContextManager+ErrorHandling.swift in Sources */, + 0CE7833D2B08F3C300B114EB /* ExternalMediaPickerViewController.swift in Sources */, F52CACCC24512EA700661380 /* EmptyActionView.swift in Sources */, D8212CBF20AA7B7F008E8AE8 /* ReaderShowAttributionAction.swift in Sources */, FFABD800213423F1003C65B6 /* LinkSettingsViewController.swift in Sources */, @@ -21160,6 +21280,7 @@ 8B074A5027AC3A64003A2EB8 /* BlogDashboardViewModel.swift in Sources */, F5A34A9925DEF47D00C9654B /* WPMediaPicker+MediaPicker.swift in Sources */, 9A8ECE132254A3260043C8DA /* JetpackInstallError+Blocking.swift in Sources */, + FEF207F52AF2904D0025CB2C /* BloggingPromptRemoteObject.swift in Sources */, 43AF2F972107D3810069C012 /* QuickStartTourState.swift in Sources */, FA4B202F29A619130089FE68 /* BlazeFlowCoordinator.swift in Sources */, 5D5A6E931B613CA400DAF819 /* OldReaderPostCardCell.swift in Sources */, @@ -21181,7 +21302,7 @@ E66969DC1B9E55C300EC9C00 /* ReaderTopicToReaderListTopic37to38.swift in Sources */, 011F52D32A1B84FF00B04114 /* PlansTracker.swift in Sources */, 9A2D0B23225CB92B009E585F /* BlogService+JetpackConvenience.swift in Sources */, - C856748F243EF177001A995E /* GutenbergTenorMediaPicker.swift in Sources */, + C856748F243EF177001A995E /* GutenbergExternalMeidaPicker.swift in Sources */, 83C972E0281C45AB0049E1FE /* Post+BloggingPrompts.swift in Sources */, B50C0C5F1EF42A4A00372C65 /* AztecPostViewController.swift in Sources */, 01E2580B2ACDC72C00F09666 /* PlanWizardContentViewModel.swift in Sources */, @@ -21200,10 +21321,11 @@ F5B9151F244653C100179876 /* TabbedViewController.swift in Sources */, FF00889B204DF3ED007CCE66 /* Blog+Quota.swift in Sources */, C533CF350E6D3ADA000C3DE8 /* CommentsViewController.m in Sources */, + 0C1531FE2AE17140003CDE13 /* PostSearchViewModel+Highlighter.swift in Sources */, FAFF153D1C98962E007D1C90 /* SiteSettingsViewController+SiteManagement.swift in Sources */, D816C1EE20E0892200C4D82F /* Follow.swift in Sources */, 0C896DE22A3A767200D7D4E7 /* SiteVisibility+Extensions.swift in Sources */, - 3F3DD0B226FD176800F5F121 /* PresentationCard.swift in Sources */, + 3F3DD0B226FD176800F5F121 /* SiteDomainsPresentationCard.swift in Sources */, 822D60B91F4CCC7A0016C46D /* BlogJetpackSettingsService.swift in Sources */, F446B843296F2DED008B94B7 /* MigrationState.swift in Sources */, 830A58D82793AB4500CDE94F /* LoginEpilogueAnimator.swift in Sources */, @@ -21232,7 +21354,7 @@ 8B7F25A724E6EDB4007D82CC /* TopicsCollectionView.swift in Sources */, D8CB56202181A8CE00554EAE /* SiteSegmentsService.swift in Sources */, E11E775A1E72932F0072AD40 /* BlogListDataSource.swift in Sources */, - 1730D4A31E97E3E400326B7C /* MediaItemTableViewCells.swift in Sources */, + 1730D4A31E97E3E400326B7C /* MediaItemHeaderView.swift in Sources */, 8BFE36FD230F16580061EBA8 /* AbstractPost+fixLocalMediaURLs.swift in Sources */, 1750BD6D201144DB0050F13A /* MediaNoticeNavigationCoordinator.swift in Sources */, E66969E41B9E68B200EC9C00 /* ReaderPostToReaderPost37to38.swift in Sources */, @@ -21252,16 +21374,17 @@ 08D978561CD2AF7D0054F19A /* MenuItem+ViewDesign.m in Sources */, 3254366C24ABA82100B2C5F5 /* ReaderInterestsStyleGuide.swift in Sources */, 988056032183CCE50083B643 /* SiteStatsInsightsTableViewController.swift in Sources */, + 0C1DB60D2B0BDA740028F200 /* TenorWelcomeView.swift in Sources */, 577C2AB422943FEC00AD1F03 /* PostCompactCell.swift in Sources */, 315FC2C51A2CB29300E7CDA2 /* MeHeaderView.m in Sources */, E1222B631F877FD700D23173 /* WebProgressView.swift in Sources */, 4A072CD229093704006235BE /* AsyncBlockOperation.swift in Sources */, E185042F1EE6ABD9005C234C /* Restorer.swift in Sources */, + 0CA10F6D2ADAE86D00CE75AC /* PostSearchSuggestionsService.swift in Sources */, 02761EC02270072F009BAF0F /* BlogDetailsViewController+SectionHelpers.swift in Sources */, 984B138E21F65F870004B6A2 /* SiteStatsPeriodTableViewController.swift in Sources */, 433ADC1D223B2A7F00ED9DE1 /* TextBundleWrapper.m in Sources */, 2F605FAA25145F7200F99544 /* WPCategoryTree.swift in Sources */, - 5DFA7EC71AF814E40072023B /* PageListTableViewCell.m in Sources */, 5D62BAD718AA88210044E5F7 /* PageSettingsViewController.m in Sources */, 46241C3C2540D483002B8A12 /* SiteDesignContentCollectionViewController.swift in Sources */, 17D2FDC21C6A468A00944265 /* PlanComparisonViewController.swift in Sources */, @@ -21279,12 +21402,12 @@ 987535632282682D001661B4 /* DetailDataCell.swift in Sources */, E17E67031FA22C93009BDC9A /* PluginViewModel.swift in Sources */, B5E167F419C08D18009535AA /* NSCalendar+Helpers.swift in Sources */, + 0C0AD10A2B0CCFA400EC06E6 /* MediaPreviewController.swift in Sources */, 80A2154629D15B88002FE8EB /* RemoteConfigOverrideStore.swift in Sources */, F4DDE2C229C92F0D00C02A76 /* CrashLogging+Singleton.swift in Sources */, 4629E4212440C5B20002E15C /* GutenbergCoverUploadProcessor.swift in Sources */, FF00889F204E01AE007CCE66 /* MediaQuotaCell.swift in Sources */, 982DDF94263238A6002B3904 /* LikeUserPreferredBlog+CoreDataClass.swift in Sources */, - F5844B6B235EAF3D007C6557 /* PartScreenPresentationController.swift in Sources */, E6805D311DCD399600168E4F /* WPRichTextImage.swift in Sources */, 7E4123C120F4097B00DF8486 /* FormattableContentRange.swift in Sources */, 08D978581CD2AF7D0054F19A /* MenuItemSourceHeaderView.m in Sources */, @@ -21298,12 +21421,13 @@ BE1071FC1BC75E7400906AFF /* WPStyleGuide+Blog.swift in Sources */, B56695B01D411EEB007E342F /* KeyboardDismissHelper.swift in Sources */, C9B477B229CC4949008CBF49 /* HomeWidgetDataFileReader.swift in Sources */, + F4AA1E5E2AF66D3300EBA201 /* AllDomainsListItemViewModel.swift in Sources */, 4A2C73F42A95856000ACE79E /* PostRepository.swift in Sources */, F5B9D7F0245BA938002BB2C7 /* FancyAlertViewController+CreateButtonAnnouncement.swift in Sources */, FF54D4641D6F3FA900A0DC4D /* GutenbergSettings.swift in Sources */, 3FAF9CC526D03C7400268EA2 /* DomainSuggestionViewControllerWrapper.swift in Sources */, + F4D140202AFD9B9700961797 /* TransferDomainsWebViewController.swift in Sources */, FE4DC5A7293A79F1008F322F /* WordPressExportRoute.swift in Sources */, - D80BC79E20746B4100614A59 /* MediaPickingContext.swift in Sources */, E6D2E16C1B8B423B0000ED14 /* ReaderStreamHeader.swift in Sources */, FFD12D5E1FE1998D00F20A00 /* Progress+Helpers.swift in Sources */, FAFC065127D27241002F0483 /* BlogDetailsViewController+Dashboard.swift in Sources */, @@ -21324,14 +21448,12 @@ 59DD94341AC479ED0032DD6B /* WPLogger.m in Sources */, FAB8FD5025AEB0F500D5D54A /* JetpackBackupStatusCoordinator.swift in Sources */, 9A38DC6C218899FB006A409B /* RevisionDiff.swift in Sources */, - FF4C069F206560E500E0B2BC /* MediaThumbnailCoordinator.swift in Sources */, 9A4697B221B002AD00468B64 /* RevisionDiffsPageManager.swift in Sources */, 4A9314E42979FA4700360232 /* PostCategory+Creation.swift in Sources */, F5660D07235D114500020B1E /* CalendarCollectionView.swift in Sources */, 7E3E7A6420E44ED60075D159 /* SubjectContentGroup.swift in Sources */, E151C0C61F3889DF00710A83 /* PluginListRow.swift in Sources */, 8217380B1FE05EE600BEC94C /* BlogSettings+DateAndTimeFormat.swift in Sources */, - 57047A4F22A961BC00B461DF /* PostSearchHeader.swift in Sources */, FA4BC0D02996A589005EB077 /* BlazeService.swift in Sources */, 088B89891DA6F93B000E8DEF /* ReaderPostCardContentLabel.swift in Sources */, C3C2F84828AC8EBF00937E45 /* JetpackBannerScrollVisibility.swift in Sources */, @@ -21350,15 +21472,12 @@ C7F7BE4C2626301500CE547F /* AuthenticationHandler.swift in Sources */, E66E2A691FE432BC00788F22 /* TitleBadgeDisclosureCell.swift in Sources */, 7E4123BF20F4097B00DF8486 /* FormattableContent.swift in Sources */, - D82253DF2199418B0014D0E2 /* WebAddressWizardContent.swift in Sources */, 8BD34F0B27D14B3C005E931C /* Blog+DashboardState.swift in Sources */, D853723C21952DC90076F461 /* SiteSegmentsStep.swift in Sources */, 32E1BFDA24A66F2A007A08F0 /* ReaderInterestsCollectionViewFlowLayout.swift in Sources */, 836498C828172C5900A2C170 /* WPStyleGuide+BloggingPrompts.swift in Sources */, FE003F5E282D61BA006F8D1D /* BloggingPrompt+CoreDataClass.swift in Sources */, - FF70A3221FD5840500BC270D /* PHAsset+Metadata.swift in Sources */, 5DA5BF4418E32DCF005F11F9 /* Theme.m in Sources */, - D8A3A5B1206A49A100992576 /* StockPhotosMediaGroup.swift in Sources */, 7E4123BC20F4097B00DF8486 /* DefaultFormattableContentAction.swift in Sources */, 7E504D4A21A5B8D400E341A8 /* PostEditorNavigationBarManager.swift in Sources */, F49B9A0A293A3249000CEFCE /* MigrationAnalyticsTracker.swift in Sources */, @@ -21384,6 +21503,7 @@ 4A2C73E12A943D9000ACE79E /* TaggedManagedObjectID.swift in Sources */, B52C4C7D199D4CD3009FD823 /* NoteBlockUserTableViewCell.swift in Sources */, E64384831C628FCC0052ADB5 /* WPStyleGuide+Sharing.swift in Sources */, + 0CA10FA82ADB7C5200CE75AC /* PostSearchService.swift in Sources */, F504D2B025D60C5900A2764C /* StoryPoster.swift in Sources */, 981C82B62193A7B900A06E84 /* Double+Stats.swift in Sources */, 175507B327A062980038ED28 /* PublicizeConnectionURLMatcher.swift in Sources */, @@ -21405,7 +21525,6 @@ DC772AF5282009BA00664C02 /* StatsLineChartView.swift in Sources */, 74729CAE205722E300D1394D /* AbstractPost+Searchable.swift in Sources */, 2906F812110CDA8900169D56 /* EditCommentViewController.m in Sources */, - 98F93182239AF64800E4E96E /* ThisWeekWidgetStats.swift in Sources */, 0C0AE7592A8FAD6A007D9D6C /* MediaPickerMenu.swift in Sources */, F16C35DA23F3F76C00C81331 /* PostAutoUploadMessageProvider.swift in Sources */, 91D8364121946EFB008340B2 /* GutenbergMediaPickerHelper.swift in Sources */, @@ -21472,6 +21591,7 @@ E66969E21B9E67A000EC9C00 /* ReaderTopicToReaderSiteTopic37to38.swift in Sources */, 80535DC0294B7D3200873161 /* BlogDetailsViewController+JetpackBrandingMenuCard.swift in Sources */, B543D2B520570B5A00D3D4CC /* WordPressComSyncService.swift in Sources */, + 017008452B35C25C00C80490 /* SiteDomainsViewModel.swift in Sources */, E14A52371E39F43E00EE203E /* AppRatingsUtility.swift in Sources */, 46638DF6244904A3006E8439 /* GutenbergBlockProcessor.swift in Sources */, 46241C0F2540BD01002B8A12 /* SiteDesignStep.swift in Sources */, @@ -21500,7 +21620,6 @@ 8370D10A11FA499A009D650F /* WPTableViewActivityCell.m in Sources */, E1B912891BB01288003C25B9 /* PeopleViewController.swift in Sources */, 3FEC241525D73E8B007AFE63 /* ConfettiView.swift in Sources */, - 436D561F2117312700CEAA33 /* RegisterDomainSuggestionsViewController.swift in Sources */, 8BC12F7523201917004DDA72 /* AbstractPost+MarkAsFailedAndDraftIfNeeded.swift in Sources */, B0B68A9D252FA91E0001B28C /* UserSuggestion+CoreDataProperties.swift in Sources */, DC772AF3282009BA00664C02 /* StatsLineChartConfiguration.swift in Sources */, @@ -21510,7 +21629,6 @@ E6D2E1651B8AAD7E0000ED14 /* ReaderSiteStreamHeader.swift in Sources */, F126FE0020A33BDB0010EB6E /* VideoUploadProcessor.swift in Sources */, E166FA1B1BB0656B00374B5B /* PeopleCellViewModel.swift in Sources */, - 73CE3E0E21F7F9D3007C9C85 /* TableViewOffsetCoordinator.swift in Sources */, 436D56292117312700CEAA33 /* RegisterDomainDetailsViewController.swift in Sources */, F4EDAA4C29A516EA00622D3D /* ReaderPostService.swift in Sources */, F5E29036243E4F5F00C19CA5 /* FilterProvider.swift in Sources */, @@ -21528,7 +21646,7 @@ 8B36256625A60CCA00D7CCE3 /* BackupListViewController.swift in Sources */, FAA4013427B52455009E1137 /* DashboardQuickActionCell.swift in Sources */, 32A218D8251109DB00D1AE6C /* ReaderReportPostAction.swift in Sources */, - 0C01A6EA2AB37F0F009F7145 /* SiteMediaCollectionCellBadgeView.swift in Sources */, + 0C01A6EA2AB37F0F009F7145 /* SiteMediaCollectionCellSelectionOverlayView.swift in Sources */, FA681F8A25CA946B00DAA544 /* BaseRestoreStatusFailedViewController.swift in Sources */, 081E4B4C281C019A0085E89C /* TooltipAnchor.swift in Sources */, 3F851415260D0A3300A4B938 /* UnifiedPrologueEditorContentView.swift in Sources */, @@ -21538,7 +21656,6 @@ 436D56222117312700CEAA33 /* RegisterDomainDetailsViewModel.swift in Sources */, DC76668326FD9AC9009254DD /* TimeZoneRow.swift in Sources */, C76F48DC25BA202600BFEC87 /* JetpackScanHistoryViewController.swift in Sources */, - 7E407121237163B8003627FA /* GutenbergStockPhotos.swift in Sources */, FA4B203B29AE62C00089FE68 /* BlazeOverlayViewController.swift in Sources */, 0C8E2F2D2AC4722F0023F9D6 /* SiteMediaViewController.swift in Sources */, F11C9F74243B3C3E00921DDC /* MediaHost+Blog.swift in Sources */, @@ -21549,7 +21666,6 @@ 8313B9FA2995A03C000AF26E /* JetpackRemoteInstallCardView.swift in Sources */, DC772B0A28201F5300664C02 /* ViewsVisitorsLineChartCell.swift in Sources */, 98656BD82037A1770079DE67 /* SignupEpilogueViewController.swift in Sources */, - C81CCD80243BF7A600A83E27 /* TenorPicker.swift in Sources */, 74EFB5C8208674250070BD4E /* BlogListViewController+Activity.swift in Sources */, 738B9A5721B85CF20005062B /* TableDataCoordinator.swift in Sources */, 80D9D00029E85EBF00FE3400 /* PageEditorPresenter.swift in Sources */, @@ -21561,6 +21677,7 @@ E6F2788021BC1A4A008B4DB5 /* Plan.swift in Sources */, B5AC00681BE3C4E100F8E7C3 /* DiscussionSettingsViewController.swift in Sources */, B0637543253E7E7A00FD45D2 /* GutenbergSuggestionsViewController.swift in Sources */, + F4F7B2542AFA5D8600207282 /* AllDomainsListCardView.swift in Sources */, D816C1F620E0896F00C4D82F /* TrashComment.swift in Sources */, 8384C64128AAC82600EABE26 /* KeychainUtils.swift in Sources */, 08AAD69F1CBEA47D002B2418 /* MenusService.m in Sources */, @@ -21570,7 +21687,6 @@ 98B52AE121F7AF4A006FF6B4 /* StatsDataHelper.swift in Sources */, 8BAD272C241FEF3300E9D105 /* PrepublishingViewController.swift in Sources */, 4A1E77C92988997C006281CC /* PublicizeConnection+Creation.swift in Sources */, - 9A22D9C0214A6BCA00BAEAF2 /* PageListTableViewHandler.swift in Sources */, F10D634F26F0B78E00E46CC7 /* Blog+Organization.swift in Sources */, DCCDF75B283BEFEA00AA347E /* SiteStatsInsightsDetailsTableViewController.swift in Sources */, 80A2154029CA68D5002FE8EB /* RemoteFeatureFlag.swift in Sources */, @@ -21578,7 +21694,6 @@ B59D994F1C0790CC0003D795 /* SettingsListEditorViewController.swift in Sources */, 9A4E215C21F75BBE00EFF212 /* QuickStartChecklistManager.swift in Sources */, FAC1B81E29B0C2AC00E0C542 /* BlazeOverlayViewModel.swift in Sources */, - C81CCD84243BF7A600A83E27 /* NoResultsTenorConfiguration.swift in Sources */, C9B477AE29CC35A0008CBF49 /* WidgetDataReader.swift in Sources */, FEF28E822ACB3DCE006C6579 /* ReaderDetailNewHeaderView.swift in Sources */, 9A8ECE0F2254A3260043C8DA /* JetpackRemoteInstallViewController.swift in Sources */, @@ -21595,24 +21710,26 @@ D817799420ABFDB300330998 /* ReaderPostCellActions.swift in Sources */, 402B2A7920ACD7690027C1DC /* ActivityStore.swift in Sources */, E62AFB6A1DC8E593007484FC /* NSAttributedString+WPRichText.swift in Sources */, - 8B33BC9527A0C14C00DB5985 /* BlogDetailsViewController+QuickActions.swift in Sources */, 98812966219CE42A0075FF33 /* StatsTotalRow.swift in Sources */, 46D6114F2555DAED00B0B7BB /* SiteCreationAnalyticsHelper.swift in Sources */, 98A047722821CEBF001B4E2D /* BloggingPromptsViewController.swift in Sources */, B54866CA1A0D7042004AC79D /* NSAttributedString+Helpers.swift in Sources */, 011896A229D5AF0700D34BA9 /* BlogDashboardCardConfigurable.swift in Sources */, C81CCD81243BF7A600A83E27 /* TenorService.swift in Sources */, + 0C749D7A2B0543D0004CB468 /* WPImageViewController+Swift.swift in Sources */, 8B51844525893F140085488D /* FilterBarView.swift in Sources */, E105205B1F2B1CF400A948F6 /* BlogToBlogMigration_61_62.swift in Sources */, FE3E83E526A58646008CE851 /* ListSimpleOverlayView.swift in Sources */, 98B88452261E4E09007ED7F8 /* LikeUserTableViewCell.swift in Sources */, E16FB7E31F8B61040004DD9F /* WebKitViewController.swift in Sources */, + 016231502B3B3CAD0010E377 /* PrimaryDomainView.swift in Sources */, 8067340A27E3A50900ABC95E /* UIViewController+RemoveQuickStart.m in Sources */, 3F8CBE0B24EEB0EA00F71234 /* AnnouncementsDataSource.swift in Sources */, FE7FAABE299A998E0032A6F2 /* EventTracker.swift in Sources */, 7305138321C031FC006BD0A1 /* AssembledSiteView.swift in Sources */, 321955C324BF77E400E3F316 /* ReaderTopicService+FollowedInterests.swift in Sources */, 3234BB342530EA980068DA40 /* ReaderRecommendedSiteCardCell.swift in Sources */, + 0C1DB6082B0A419B0028F200 /* ImageDecoder.swift in Sources */, 93414DE51E2D25AE003143A3 /* PostEditorState.swift in Sources */, 1707CE421F3121750020B7FE /* UICollectionViewCell+Tint.swift in Sources */, 011F52C32A153A3400B04114 /* FreeToPaidPlansDashboardCardHelper.swift in Sources */, @@ -21636,7 +21753,6 @@ 0CED95602A460F4B0020F420 /* DebugFeatureFlagsView.swift in Sources */, FA73D7D6278D9E5D00DF24B3 /* BlogDashboardViewController.swift in Sources */, 4349B0AF218A477F0034118A /* RevisionsTableViewCell.swift in Sources */, - 738B9A5921B85CF20005062B /* KeyboardInfo.swift in Sources */, E15644F31CE0E5A500D96E64 /* PlanDetailViewModel.swift in Sources */, FA8E2FE527C6AE4500DA0982 /* QuickStartChecklistView.swift in Sources */, 80D9D04629F760C400FE3400 /* FailableDecodable.swift in Sources */, @@ -21653,7 +21769,6 @@ 91138455228373EB00FB02B7 /* GutenbergVideoUploadProcessor.swift in Sources */, FA73D7E927987BA500DF24B3 /* SitePickerViewController+SiteIcon.swift in Sources */, E125445612BF5B3900D87A0A /* PostCategory.m in Sources */, - 08C388661ED7705E0057BE49 /* MediaAssetExporter.swift in Sources */, E6C892D61C601D55007AD612 /* SharingButtonsViewController.swift in Sources */, F115308121B17E66002F1D65 /* EditorFactory.swift in Sources */, 982DDF92263238A6002B3904 /* LikeUser+CoreDataProperties.swift in Sources */, @@ -21697,6 +21812,7 @@ 1782BE841E70063100A91E7D /* MediaItemViewController.swift in Sources */, F41E32FE287B47A500F89082 /* SuggestionsListViewModel.swift in Sources */, FA4B203829A8C48F0089FE68 /* AbstractPost+Blaze.swift in Sources */, + 0C1DB5FF2B095DA50028F200 /* ImageView.swift in Sources */, E10A2E9B134E8AD3007643F9 /* PostAnnotation.m in Sources */, 2481B17F260D4D4E00AE59DB /* WPAccount+Lookup.swift in Sources */, 7E3E9B702177C9DC00FD5797 /* GutenbergViewController.swift in Sources */, @@ -21706,6 +21822,7 @@ 738B9A4F21B85CF20005062B /* SiteCreator.swift in Sources */, F5660D09235D1CDD00020B1E /* CalendarMonthView.swift in Sources */, 08A250F828D9E87600F50420 /* CommentDetailInfoViewController.swift in Sources */, + F4B0F4832ADED9B5003ABC61 /* DomainsService+AllDomains.swift in Sources */, FA1CEAC225CA9C2A005E7038 /* RestoreStatusFailedView.swift in Sources */, 9848DF8121B8BB5700B99DA4 /* PostingActivityLegend.swift in Sources */, B5DB8AF41C949DC20059196A /* WPImmuTableRows.swift in Sources */, @@ -21717,7 +21834,7 @@ E1B62A7B13AA61A100A6FCA4 /* WPWebViewController.m in Sources */, D813D67F21AA8BBF0055CCA1 /* ShadowView.swift in Sources */, D865721321869C590023A99C /* Wizard.swift in Sources */, - 08C3886A1ED78EE70057BE49 /* Media+WPMediaAsset.m in Sources */, + 08C3886A1ED78EE70057BE49 /* Media+Extensions.m in Sources */, FF2EC3C02209A144006176E1 /* GutenbergImgUploadProcessor.swift in Sources */, E151C0C81F388A2000710A83 /* PluginListViewModel.swift in Sources */, E1B9128B1BB0129C003C25B9 /* WPStyleGuide+People.swift in Sources */, @@ -21738,10 +21855,10 @@ F93735F122D534FE00A3C312 /* LoggingURLRedactor.swift in Sources */, 9874766F219630240080967F /* SiteStatsTableViewCells.swift in Sources */, E6E27D621C6144DB0063F821 /* SharingButton.swift in Sources */, - 570BFD8B22823D7B007859A8 /* PostActionSheet.swift in Sources */, E6374DC01C444D8B00F24720 /* PublicizeConnection.swift in Sources */, C81CCD7C243BF7A600A83E27 /* TenorPageable.swift in Sources */, E6C0ED3B231DA23400A08B57 /* AccountService+MergeDuplicates.swift in Sources */, + F413F77A2B2A183E00A64A94 /* BlogDashboardDynamicCardCell.swift in Sources */, 0845B8C61E833C56001BA771 /* URL+Helpers.swift in Sources */, 17D975AF1EF7F6F100303D63 /* WPStyleGuide+Aztec.swift in Sources */, E14B40FF1C58B93F005046F6 /* SettingsCommon.swift in Sources */, @@ -21751,6 +21868,7 @@ 02D75D9922793EA2003FF09A /* BlogDetailsSectionFooterView.swift in Sources */, 08E39B4528A3DEB200874CB8 /* UserPersistentStoreFactory.swift in Sources */, C81CCD67243AECA200A83E27 /* TenorGIFCollection.swift in Sources */, + FA141F2A2AEC23E300C9A653 /* PageListViewController+Menu.swift in Sources */, 4AD5656C28E3D0670054C676 /* ReaderPost+Helper.swift in Sources */, E137B1661F8B77D4006AC7FC /* WebNavigationDelegate.swift in Sources */, 937D9A0F19F83812007B9D5F /* WordPress-22-23.xcmappingmodel in Sources */, @@ -21770,13 +21888,13 @@ 5D1D04761B7A50B100CDE646 /* ReaderStreamViewController.swift in Sources */, B57B92BD1B73B08100DFF00B /* SeparatorsView.swift in Sources */, C789952525816F96001B7B43 /* JetpackScanCoordinator.swift in Sources */, - E69BA1981BB5D7D300078740 /* WPStyleGuide+ReadableMargins.m in Sources */, 7E4123C320F4097B00DF8486 /* FormattableContentStyles.swift in Sources */, 735A9681228E421F00461135 /* StatsBarChartConfiguration.swift in Sources */, C3643ACF28AC049D00FC5FD3 /* SharingViewController.swift in Sources */, 2F161B0622CC2DC70066A5C5 /* LoadingStatusView.swift in Sources */, 080C44A91CE14A9F00B3A02F /* MenuDetailsViewController.m in Sources */, DCF892C9282FA37100BB71E1 /* SiteStatsBaseTableViewController.swift in Sources */, + 0C5751102B011468001074E5 /* RemoteConfigDebugView.swift in Sources */, 2FA6511B21F26A57009AA935 /* InlineErrorRetryTableViewCell.swift in Sources */, 08216FCF1CDBF96000304BA7 /* MenuItemSourceCell.m in Sources */, 462F4E0A18369F0B0028D2F8 /* BlogDetailsViewController.m in Sources */, @@ -21789,7 +21907,6 @@ D8212CC920AA87E5008E8AE8 /* ReaderMenuAction.swift in Sources */, 593F26611CAB00CA00F14073 /* PostSharingController.swift in Sources */, 2FA37B1A215724E900C80377 /* LongPressGestureLabel.swift in Sources */, - 98F1B12A2111017A00139493 /* NoResultsStockPhotosConfiguration.swift in Sources */, FF70A3231FD5840500BC270D /* UIImage+Export.swift in Sources */, D81322B32050F9110067714D /* NotificationName+Names.swift in Sources */, F4BECD1B288EE5220078391A /* SuggestionsViewModelType.swift in Sources */, @@ -21807,12 +21924,14 @@ 85DA8C4418F3F29A0074C8A4 /* WPAnalyticsTrackerWPCom.m in Sources */, B50C0C5E1EF42A4A00372C65 /* AztecAttachmentViewController.swift in Sources */, 43FB3F411EBD215C00FC8A62 /* LoginEpilogueBlogCell.swift in Sources */, + 0CFE9AC62AF44A9F00B8F659 /* AbstractPostHelper+Actions.swift in Sources */, 57D66B9A234BB206005A2D74 /* PostServiceRemoteFactory.swift in Sources */, 3FC8D19B244F43B500495820 /* ReaderTabItemsStore.swift in Sources */, 98EB126A20D2DC2500D2D5B5 /* NoResultsViewController+Model.swift in Sources */, - 570265152298921800F2214C /* PostListTableViewHandler.swift in Sources */, B5969E2220A49E86005E9DF1 /* UIAlertController+Helpers.swift in Sources */, + 0CE783412B08FB2E00B114EB /* ExternalMediaPickerCollectionCell.swift in Sources */, 9A4E61F821A2C3BC0017A925 /* RevisionDiff+CoreData.swift in Sources */, + 01ABF1702AD578B3004331BD /* WidgetAnalytics.swift in Sources */, 7E7947AB210BAC5E005BB851 /* NotificationCommentRange.swift in Sources */, FE25C235271F23000084E1DB /* ReaderCommentsNotificationSheetViewController.swift in Sources */, C32A6A2C2832BF02002E9394 /* SiteDesignCategoryThumbnailSize.swift in Sources */, @@ -21829,7 +21948,6 @@ FE39C136269C37C900EFB827 /* ListTableViewCell.swift in Sources */, 803C493B283A7C0C00003E9B /* QuickStartChecklistHeader.swift in Sources */, 93C1147F18EC5DD500DAC95C /* AccountService.m in Sources */, - FF945F701B28242300FB8AC4 /* MediaLibraryPickerDataSource.m in Sources */, 986C90882231AD6200FC31E1 /* PostStatsViewModel.swift in Sources */, FAFC064E27D2360B002F0483 /* QuickStartCell.swift in Sources */, 9A4A8F4B235758EF00088CE4 /* StatsStore+Cache.swift in Sources */, @@ -21840,24 +21958,24 @@ 3236F77224ABB6C90088E8F3 /* ReaderInterestsDataSource.swift in Sources */, 7E4123CA20F4184200DF8486 /* ActivityContentGroup.swift in Sources */, 08F8CD2F1EBD29440049D0C0 /* MediaImageExporter.swift in Sources */, - 912347192213484300BD9F97 /* GutenbergViewController+InformativeDialog.swift in Sources */, 2FA6511721F26A24009AA935 /* ChangePasswordViewController.swift in Sources */, D816C1F020E0893A00C4D82F /* LikeComment.swift in Sources */, 982DA9A7263B1E2F00E5743B /* CommentService+Likes.swift in Sources */, 80F8DAC1282B6546007434A0 /* WPAnalytics+QuickStart.swift in Sources */, 59E1D46E1CEF77B500126697 /* Page.swift in Sources */, 321955BF24BE234C00E3F316 /* ReaderInterestsCoordinator.swift in Sources */, + 4A5DE7382B0D511900363171 /* PageTree.swift in Sources */, FAE4327425874D140039EB8C /* ReaderSavedPostCellActions.swift in Sources */, 7E4123C220F4097B00DF8486 /* FormattableContentFormatter.swift in Sources */, 9887560C2810BA7A00AD7589 /* BloggingPromptsIntroductionPresenter.swift in Sources */, 7E21C765202BBF4400837CF5 /* SearchAdsAttribution.swift in Sources */, 5DF8D26119E82B1000A2CD95 /* ReaderCommentsViewController.m in Sources */, + 01B759082B3ECAF300179AE6 /* DomainsStateView.swift in Sources */, C3DD4DCE28BE5D4D0046C68E /* SplashPrologueViewController.swift in Sources */, 1716AEFC25F2927600CF49EC /* MySiteViewController.swift in Sources */, F18CB8962642E58700B90794 /* FixedSizeImageView.swift in Sources */, 7E3E7A6020E44E490075D159 /* FooterContentGroup.swift in Sources */, 46F584B82624E6380010A723 /* BlockEditorSettings+GutenbergEditorSettings.swift in Sources */, - 59A3CADD1CD2FF0C009BFA1B /* BasePageListCell.m in Sources */, E114D79A153D85A800984182 /* WPError.m in Sources */, 7E53AB0220FE5EAE005796FE /* ContentRouter.swift in Sources */, E16273E11B2ACEB600088AF7 /* BlogToBlog32to33.swift in Sources */, @@ -21879,12 +21997,12 @@ C3FF78E828354A91008FA600 /* SiteDesignSectionLoader.swift in Sources */, 803BB989295B80D300B3F6D6 /* RootViewPresenter+EditorNavigation.swift in Sources */, B58C4ECA207C5E1A00E32E4D /* UIImage+Assets.swift in Sources */, - E684383E221F535900752258 /* LoadMoreCounter.swift in Sources */, E6158ACA1ECDF518005FA441 /* LoginEpilogueUserInfo.swift in Sources */, C3234F5427EBBACA004ADB29 /* SiteIntentVertical.swift in Sources */, 983DBBAB22125DD500753988 /* StatsTableFooter.swift in Sources */, 85D239AE1AE5A5FC0074768D /* BlogSyncFacade.m in Sources */, 0CAE8EF22A9E9E8D0073EEB9 /* SiteMediaCollectionCell.swift in Sources */, + 0CB54F572AEC320700582080 /* WordPressAppDelegate+PostCoordinatorDelegate.swift in Sources */, 8B0732F3242BF99B00E7FBD3 /* PrepublishingNavigationController.swift in Sources */, 5D97C2F315CAF8D8009B44DD /* UINavigationController+KeyboardFix.m in Sources */, 4034FDEA2007C42400153B87 /* ExpandableCell.swift in Sources */, @@ -21906,7 +22024,6 @@ 9A341E5321997A1F0036662E /* BlogService+BlogAuthors.swift in Sources */, 17A28DC52050404C00EA6D9E /* AuthorFilterButton.swift in Sources */, 08216FD31CDBF96000304BA7 /* MenuItemTagsViewController.m in Sources */, - 0C8B8C0F2ACDBE1900CCE50F /* DisabledVideoOverlay.swift in Sources */, 5D3E334E15EEBB6B005FC6F2 /* ReachabilityUtils.m in Sources */, C81CCD7D243BF7A600A83E27 /* TenorDataSource.swift in Sources */, 9F3EFCA1208E305E00268758 /* ReaderTopicService+Subscriptions.swift in Sources */, @@ -21921,6 +22038,7 @@ 4AA33EF829963ABE005B6E23 /* ReaderAbstractTopic+Lookup.swift in Sources */, 73BFDA8A211D054800907245 /* Notifiable.swift in Sources */, 404B35D322E9BA0800AD0B37 /* RegisterDomainDetailsViewModel+CountryDialCodes.swift in Sources */, + F41D98D72B389735004EC050 /* DashboardDynamicCardAnalyticsEvent.swift in Sources */, D8EB1FD121900810002AE1C4 /* BlogListViewController+SiteCreation.swift in Sources */, 80B016D12803AB9F00D15566 /* DashboardPostsListCardCell.swift in Sources */, E64595F0256B5D7800F7F90C /* CommentAnalytics.swift in Sources */, @@ -21953,7 +22071,6 @@ E11C4B72201096EF00A6619C /* JetpackState.swift in Sources */, 437542E31DD4E19E00D6B727 /* EditPostViewController.swift in Sources */, 595CB3761D2317D50082C7E9 /* PostListFilter.swift in Sources */, - 08EA036729C9B51200B72A87 /* Color+DesignSystem.swift in Sources */, 08E77F451EE87FCF006F9515 /* MediaThumbnailExporter.swift in Sources */, F1112AB2255C2D4600F1F746 /* BlogDetailHeaderView.swift in Sources */, B5EB19EC20C6DACC008372B9 /* ImageDownloader.swift in Sources */, @@ -21981,7 +22098,7 @@ 17523381246C4F9200870B4A /* HomepageSettingsViewController.swift in Sources */, E62CE58E26B1D14200C9D147 /* AccountService+Cookies.swift in Sources */, 3F4A4C232AD3FA2E00DE5DF8 /* MySiteViewModel.swift in Sources */, - C81CCD7F243BF7A600A83E27 /* TenorMediaGroup.swift in Sources */, + FACF66D02ADD6CD8008C3E13 /* PostListItemViewModel.swift in Sources */, B0F2EFBF259378E600C7EB6D /* SiteSuggestionService.swift in Sources */, 4388FF0020A4E19C00783948 /* NotificationsViewController+PushPrimer.swift in Sources */, 800035BD291DD0D7007D2D26 /* JetpackFullscreenOverlayGeneralViewModel+Analytics.swift in Sources */, @@ -21994,6 +22111,7 @@ 17A4A36C20EE55320071C2CA /* ReaderCoordinator.swift in Sources */, FAB800B225AEE3C600D5D54A /* RestoreCompleteView.swift in Sources */, B532D4EE199D4418006E4DF6 /* NoteBlockImageTableViewCell.swift in Sources */, + 0CD9CCA32AD831590044A33C /* PostSearchViewModel.swift in Sources */, 93FA59DD18D88C1C001446BC /* PostCategoryService.m in Sources */, 436D564F211E122D00CEAA33 /* RegisterDomainDetailsServiceProxy.swift in Sources */, F5B8A60F23CE56A1001B7359 /* PreviewDeviceSelectionViewController.swift in Sources */, @@ -22004,14 +22122,11 @@ E1BEEC631C4E35A8000B4FA0 /* Animator.swift in Sources */, 981C3494218388CA00FC2683 /* SiteStatsDashboardViewController.swift in Sources */, E1C5457E1C6B962D001CEB0E /* MediaSettings.swift in Sources */, - 173B215527875E0600D4DD6B /* DeviceMediaPermissionsHeader.swift in Sources */, 02BF30532271D7F000616558 /* DomainCreditRedemptionSuccessViewController.swift in Sources */, 93C4864F181043D700A24725 /* ActivityLogDetailViewController.m in Sources */, B57B99DE19A2DBF200506504 /* NSObject+Helpers.m in Sources */, E1E49CE41C4902EE002393A4 /* ImmuTableViewController.swift in Sources */, - D88A6496208D7B0B008AE9BC /* NullStockPhotosService.swift in Sources */, 9A162F2321C26D7500FDC035 /* RevisionPreviewViewController.swift in Sources */, - 467D3DFA25E4436000EB9CB0 /* SitePromptView.swift in Sources */, FAB8AA2225AF031200F9F8A0 /* BaseRestoreCompleteViewController.swift in Sources */, E616E4B31C480896002C024E /* SharingService.swift in Sources */, F5E032E82408D537003AF350 /* BottomSheetPresentationController.swift in Sources */, @@ -22037,6 +22152,7 @@ E14200781C117A2E00B3B115 /* ManagedAccountSettings.swift in Sources */, 0C23F3362AC4AD3400EE6117 /* SiteMediaSelectionTitleView.swift in Sources */, 401AC82722DD2387006D78D4 /* Blog+Plans.swift in Sources */, + B0DE91B52AF9778200D51A02 /* DomainSetupNoticeView.swift in Sources */, 08B6D6F31C8F7DCE0052C52B /* PostType.m in Sources */, FE06AC8526C3C2F800B69DE4 /* ShareAppTextActivityItemSource.swift in Sources */, 59A9AB351B4C33A500A433DC /* ThemeService.m in Sources */, @@ -22063,6 +22179,7 @@ F1DB8D292288C14400906E2F /* Uploader.swift in Sources */, FA3A281B2A39C8FF00206D74 /* BlazeCampaignSingleStatView.swift in Sources */, 3FB1929026C6109F000F5AA3 /* TimeSelectionView.swift in Sources */, + 4A535E142AF3368B008B87B9 /* MenusViewController.swift in Sources */, FAB8AB8B25AFFE7500F9F8A0 /* JetpackRestoreService.swift in Sources */, 821738091FE04A9E00BEC94C /* DateAndTimeFormatSettingsViewController.swift in Sources */, 5D6C4B081B603E03005E3C43 /* WPContentSyncHelper.swift in Sources */, @@ -22072,13 +22189,13 @@ 803BB983295957F600B3F6D6 /* MySitesCoordinator+RootViewPresenter.swift in Sources */, 8BBC778B27B5531700DBA087 /* BlogDashboardPersistence.swift in Sources */, F5E29038243FAB0300C19CA5 /* FilterTableData.swift in Sources */, + 0CE538D02B0E317000834BA2 /* StockPhotosWelcomeView.swift in Sources */, E6A3384C1BB08E3F00371587 /* ReaderGapMarker.m in Sources */, 011896A529D5B72500D34BA9 /* DomainsDashboardCoordinator.swift in Sources */, E1CFC1571E0AC8FF001DF9E9 /* Pattern.swift in Sources */, 930F09171C7D110E00995926 /* ShareExtensionService.swift in Sources */, F5A34BCC25DF244F00C9654B /* KanvasCameraCustomUI.swift in Sources */, C3A1166A29807E3F00B0CB6E /* ReaderBlockUserAction.swift in Sources */, - 7ECD5B8120C4D823001AEBC5 /* MediaPreviewHelper.swift in Sources */, FE06AC8326C3BD0900B69DE4 /* ShareAppContentPresenter.swift in Sources */, F5A738BD244DF75400EDE065 /* OffsetTableViewHandler.swift in Sources */, 177E7DAD1DD0D1E600890467 /* UINavigationController+SplitViewFullscreen.swift in Sources */, @@ -22111,6 +22228,7 @@ 2F08ECFC2283A4FB000F8E11 /* PostService+UnattachedMedia.swift in Sources */, E61084BF1B9B47BA008050C5 /* ReaderDefaultTopic.swift in Sources */, 8BB185D624B66FE600A4CCE8 /* ReaderCard+CoreDataClass.swift in Sources */, + FACF66CD2ADD645C008C3E13 /* PostListHeaderView.swift in Sources */, D816C1F220E0894D00C4D82F /* ReplyToComment.swift in Sources */, E1BB92321FDAAFFA00F2D817 /* TextWithAccessoryButtonCell.swift in Sources */, C81CCD70243AFAE600A83E27 /* TenorResponse.swift in Sources */, @@ -22123,12 +22241,14 @@ 174C11932624C78900346EC6 /* Routes+Start.swift in Sources */, 4AA33F04299A1F93005B6E23 /* ReaderTagTopic+Lookup.swift in Sources */, FA347AED26EB6E300096604B /* GrowAudienceCell.swift in Sources */, + FE6AFE472B1A351F00F76520 /* SOTWCardView.swift in Sources */, FE32F006275F62620040BE67 /* WebCommentContentRenderer.swift in Sources */, B5CC05F61962150600975CAC /* Constants.m in Sources */, 4326191522FCB9DC003C7642 /* MurielColor.swift in Sources */, 436D562B2117312700CEAA33 /* RegisterDomainDetailsErrorSectionFooter.swift in Sources */, 7E729C28209A087300F76599 /* ImageLoader.swift in Sources */, - 3FAF9CC226D01CFE00268EA2 /* DomainsDashboardView.swift in Sources */, + 3FAF9CC226D01CFE00268EA2 /* SiteDomainsView.swift in Sources */, + 0CFE9AC92AF52D3B00B8F659 /* PostSettingsViewController+Swift.swift in Sources */, 73C8F06821BF1A5E00DDDF7E /* SiteAssemblyContentView.swift in Sources */, 178DDD30266D7576006C68C4 /* BloggingRemindersFlowCompletionViewController.swift in Sources */, 5D146EBB189857ED0068FDC6 /* FeaturedImageViewController.m in Sources */, @@ -22151,13 +22271,13 @@ 9815D0B326B49A0600DF7226 /* Comment+CoreDataProperties.swift in Sources */, 08D978551CD2AF7D0054F19A /* Menu+ViewDesign.m in Sources */, F44293D628E3BA1700D340AF /* AppIconListViewModel.swift in Sources */, - 02AC3092226FFFAA0018D23B /* BlogDetailsViewController+DomainCredit.swift in Sources */, 80D9CFF729E5010300FE3400 /* PagesCardViewModel.swift in Sources */, E60BD231230A3DD400727E82 /* KeyringAccountHelper.swift in Sources */, FEDA1AD8269D475D0038EC98 /* ListTableViewCell+Comments.swift in Sources */, E142007A1C117A3B00B3B115 /* ManagedAccountSettings+CoreDataProperties.swift in Sources */, B535209D1AF7EB9F00B33BA8 /* PushAuthenticationService.swift in Sources */, F41E3301287B5FE500F89082 /* SuggestionViewModel.swift in Sources */, + FA141F272AEC1D9E00C9A653 /* PageMenuViewModel.swift in Sources */, 1762B6DC2845510400F270A5 /* StatsReferrersChartViewModel.swift in Sources */, 3F851428260D1EA300A4B938 /* CircledIcon.swift in Sources */, 3101866B1A373B01008F7DF6 /* WPTabBarController.m in Sources */, @@ -22165,6 +22285,7 @@ 85B125461B0294F6008A3D95 /* UIAlertControllerProxy.m in Sources */, 73FF7030221F43CD00541798 /* StatsBarChartView.swift in Sources */, F5E1BBE0253B74240091E9A6 /* URLQueryItem+Parameters.swift in Sources */, + F48EBF8A2B2F94DD004CD561 /* BlogDashboardAnalyticPropertiesProviding.swift in Sources */, 0857C27A1CE5375F0014AE99 /* MenuItemView.m in Sources */, B5E51B7B203477DF00151ECD /* WordPressAuthenticationManager.swift in Sources */, FA4ADAD81C50687400F858D7 /* SiteManagementService.swift in Sources */, @@ -22176,6 +22297,7 @@ 469EB16824D9AD8B00C764CB /* CollapsableHeaderFilterBar.swift in Sources */, 40247E022120FE3600AE1C3C /* AutomatedTransferHelper.swift in Sources */, 8BF281FC27CEB69C00AF8CF3 /* DashboardFailureCardCell.swift in Sources */, + 017C57BB2B2B5555001E7687 /* DomainSelectionViewController.swift in Sources */, 082AB9D91C4EEEF4000CA523 /* PostTagService.m in Sources */, 3F6975FF242D941E001F1807 /* ReaderTabViewModel.swift in Sources */, E62079E11CF7A61200F5CD46 /* ReaderSearchSuggestionService.swift in Sources */, @@ -22208,6 +22330,7 @@ 57BAD50C225CCE1A006139EC /* WPTabBarController+Swift.swift in Sources */, E1468DE71E794A4D0044D80F /* LanguageSelectorViewController.swift in Sources */, E14694071F3459E2004052C8 /* PluginListViewController.swift in Sources */, + FAD3DE812AE2965A00A3B031 /* AbstractPostMenuHelper.swift in Sources */, FF0D8146205809C8000EE505 /* PostCoordinator.swift in Sources */, B0B89DC02A1E882F003D5295 /* DomainResultView.swift in Sources */, 7EA30DB621ADA20F0092F894 /* AztecAttachmentDelegate.swift in Sources */, @@ -22221,6 +22344,7 @@ E61084C11B9B47BA008050C5 /* ReaderSiteTopic.swift in Sources */, 0878580328B4CF950069F96C /* UserPersistentRepositoryUtility.swift in Sources */, FE18495827F5ACBA00D26879 /* DashboardPromptsCardCell.swift in Sources */, + 80DB57922AF8B59B00C728FF /* RegisterDomainCoordinator.swift in Sources */, FAB8004925AEDC2300D5D54A /* JetpackBackupCompleteViewController.swift in Sources */, 9A8ECE0C2254A3260043C8DA /* JetpackLoginViewController.swift in Sources */, 46F583A92624CE790010A723 /* BlockEditorSettings+CoreDataClass.swift in Sources */, @@ -22237,19 +22361,16 @@ 8BF0B607247D88EB009A7457 /* UITableViewCell+enableDisable.swift in Sources */, 937F3E321AD6FDA7006BA498 /* WPAnalyticsTrackerAutomatticTracks.m in Sources */, 83914BD12A2E89F30017A588 /* JetpackSocialNoConnectionView.swift in Sources */, - 171096CB270F01EA001BCDD6 /* DomainSuggestionsTableViewController.swift in Sources */, 469CE07124BCFB04003BDC8B /* CollapsableHeaderCollectionViewCell.swift in Sources */, 7EB5824720EC41B200002702 /* NotificationContentFactory.swift in Sources */, 80535DBE294AC89200873161 /* JetpackBrandingMenuCardPresenter.swift in Sources */, F53FF3AA23EA725C001AD596 /* SiteIconView.swift in Sources */, FA3536F525B01A2C0005A3A0 /* JetpackRestoreCompleteViewController.swift in Sources */, 7E7947AD210BAC7B005BB851 /* FormattableNoticonRange.swift in Sources */, - 08240C2E2AB8A2DD00E7AEA8 /* DomainListCard.swift in Sources */, 46F583AB2624CE790010A723 /* BlockEditorSettings+CoreDataProperties.swift in Sources */, DCF892CC282FA3BB00BB71E1 /* SiteStatsImmuTableRows.swift in Sources */, 08D553662821286300AA1E8D /* Tooltip.swift in Sources */, FAB985C12697550C00B172A3 /* NoResultsViewController+StatsModule.swift in Sources */, - B55086211CC15CCB004EADB4 /* PromptViewController.swift in Sources */, FE3D058326C419C4002A51B0 /* ShareAppContentPresenter+TableView.swift in Sources */, 82FC612C1FA8B7FC00A1757E /* ActivityListRow.swift in Sources */, 436110E022C4241A000773AD /* UIColor+MurielColorsObjC.swift in Sources */, @@ -22257,6 +22378,7 @@ 46183CF5251BD658004F9AFD /* PageTemplateLayout+CoreDataProperties.swift in Sources */, FA20751427A86B73001A644D /* UIScrollView+Helpers.swift in Sources */, 981D464825B0D4E7000AA65C /* ReaderSeenAction.swift in Sources */, + FE34ACDC2B17AA9400108B3C /* BloganuaryOverlayViewController.swift in Sources */, F57402A7235FF9C300374346 /* SchedulingDate+Helpers.swift in Sources */, 4395A15D2106718900844E8E /* QuickStartChecklistCell.swift in Sources */, 57C2331822FE0EC900A3863B /* PostAutoUploadInteractor.swift in Sources */, @@ -22264,12 +22386,12 @@ 013A8CB62AB83B40004FF5D0 /* DashboardDomainsCardSearchView.swift in Sources */, F11C9F78243B3C9600921DDC /* MediaHost+ReaderPostContentProvider.swift in Sources */, FA1A55EF25A6F0740033967D /* RestoreStatusView.swift in Sources */, - 175CC1702720548700622FB4 /* DomainExpiryDateFormatter.swift in Sources */, 985793C822F23D7000643DBF /* CustomizeInsightsCell.swift in Sources */, 93E6336F272C1074009DACF8 /* LoginEpilogueCreateNewSiteCell.swift in Sources */, 8261B4CC1EA8E13700668298 /* SVProgressHUD+Dismiss.m in Sources */, 329F8E5624DDAC61002A5311 /* DynamicHeightCollectionView.swift in Sources */, FECA442F28350B7800D01F15 /* PromptRemindersScheduler.swift in Sources */, + 0CB424EE2ADEE3CD0080B807 /* PostSearchTokenTableCell.swift in Sources */, 436D56242117312700CEAA33 /* RegisterDomainDetailsViewModel+RowList.swift in Sources */, C81CCD65243AECA200A83E27 /* TenorGIF.swift in Sources */, 596C035E1B84F21D00899EEB /* ThemeBrowserViewController.swift in Sources */, @@ -22288,7 +22410,7 @@ 4353BFB221A376BF0009CED3 /* UntouchableWindow.swift in Sources */, FE32F002275F602E0040BE67 /* CommentContentRenderer.swift in Sources */, FF0AAE0A1A150A560089841D /* WPProgressTableViewCell.m in Sources */, - D8A3A5B5206A4C7800992576 /* StockPhotosPicker.swift in Sources */, + 0C3090222B12A5C90071C551 /* UIButton+Extensions.swift in Sources */, 17F11EDB268623BA00D1BBA7 /* BloggingRemindersScheduleFormatter.swift in Sources */, 98DCF4A5275945E00008630F /* ReaderDetailNoCommentCell.swift in Sources */, 176E194725C465F70058F1C5 /* UnifiedPrologueViewController.swift in Sources */, @@ -22312,6 +22434,7 @@ C81CCD82243BF7A600A83E27 /* TenorResultsPage.swift in Sources */, FACB36F11C5C2BF800C6DF4E /* ThemeWebNavigationDelegate.swift in Sources */, E1F47D4D1FE0290C00C1D44E /* PluginListCell.swift in Sources */, + 0CD9FB7E2AF9C4DB009D9C7A /* UIBarButtonItem+Extensions.swift in Sources */, 3F5C865D25C9EBEF00BABE64 /* HomeWidgetAllTimeData.swift in Sources */, D829C33B21B12EFE00B09F12 /* UIView+Borders.swift in Sources */, F1A75B9B2732EF3700784A70 /* AboutScreenTracker.swift in Sources */, @@ -22331,8 +22454,6 @@ 5D42A3E2175E7452005CFF05 /* ReaderPost.m in Sources */, FEA7948D26DD136700CEC520 /* CommentHeaderTableViewCell.swift in Sources */, C3C39B0726F50D3900B1238D /* WordPressSupportSourceTag+Editor.swift in Sources */, - 80A2154329D1177A002FE8EB /* RemoteConfigDebugViewController.swift in Sources */, - D80BC79C207464D200614A59 /* MediaLibraryMediaPickingCoordinator.swift in Sources */, 46183CF4251BD658004F9AFD /* PageTemplateLayout+CoreDataClass.swift in Sources */, 982A4C3520227D6700B5518E /* NoResultsViewController.swift in Sources */, 57C3D393235DFD8E00FE9CE6 /* ActionDispatcherFacade.swift in Sources */, @@ -22348,7 +22469,6 @@ 010459E629153FFF000C7778 /* JetpackNotificationMigrationService.swift in Sources */, FAC1B82729B1F1EE00E0C542 /* BlazePostPreviewView.swift in Sources */, E61084C21B9B47BA008050C5 /* ReaderTagTopic.swift in Sources */, - C995C22429D30A9900ACEF43 /* URL+WidgetSource.swift in Sources */, 0189AF052ACAD89700F63393 /* ShoppingCartService.swift in Sources */, F110239B2318479000C4E84A /* Media.swift in Sources */, 176CE91627FB44C100F1E32B /* StatsBaseCell.swift in Sources */, @@ -22390,6 +22510,8 @@ 98BC522427F6245700D6E8C2 /* BloggingPromptsFeatureIntroduction.swift in Sources */, 8BF1C81A27BC00AF00F1C203 /* BlogDashboardCardFrameView.swift in Sources */, E1209FA41BB4978B00D69778 /* PeopleService.swift in Sources */, + 0CD9FB8B2AFADAFE009D9C7A /* SiteMediaPageViewController.swift in Sources */, + 0CD9CC9F2AD73A560044A33C /* PostSearchViewController.swift in Sources */, 984B139421F66B2D0004B6A2 /* StatsPeriodStore.swift in Sources */, DC772AF1282009BA00664C02 /* InsightsLineChart.swift in Sources */, E13A8C9B1C3E6EF2005BB1C1 /* ImmuTable+WordPress.swift in Sources */, @@ -22399,25 +22521,24 @@ 9A38DC69218899FB006A409B /* Revision.swift in Sources */, 403269922027719C00608441 /* PluginDirectoryAccessoryItem.swift in Sources */, 931215EC267F5F45008C3B69 /* ReferrerDetailsRow.swift in Sources */, - FFB1FAA01BF0EC4E0090C761 /* PHAsset+Exporters.swift in Sources */, 465B097A24C877E500336B6C /* GutenbergLightNavigationController.swift in Sources */, 8298F38F1EEF2B15008EB7F0 /* AppFeedbackPromptView.swift in Sources */, 0CA1C8C12A940EE300F691EE /* AvatarMenuController.swift in Sources */, 931215E8267F52A6008C3B69 /* ReferrerDetailsHeaderRow.swift in Sources */, F17A2A1E23BFBD72001E96AC /* UIView+ExistingConstraints.swift in Sources */, + 0C700B862AE1E1300085C2EE /* PageListCell.swift in Sources */, F1450CF32437DA3E00A28BFE /* MediaRequestAuthenticator.swift in Sources */, 0C391E5E2A2FE5350040EA91 /* DashboardBlazeCampaignView.swift in Sources */, 9881296E219CF1310075FF33 /* StatsCellHeader.swift in Sources */, E1823E6C1E42231C00C19F53 /* UIEdgeInsets.swift in Sources */, - 5D18FE9F1AFBB17400EFEED0 /* RestorePageTableViewCell.m in Sources */, F42A1D9729928B360059CC70 /* BlockedAuthor.swift in Sources */, 069A4AA62664448F00413FA9 /* GutenbergFeaturedImageHelper.swift in Sources */, B54C02241F38F50100574572 /* String+RegEx.swift in Sources */, FFB1FA9E1BF0EB840090C761 /* UIImage+Exporters.swift in Sources */, + F4F7B2552AFA60DA00207282 /* DomainDetailsWebViewController.swift in Sources */, F48D44BD2989AA8C0051EAA6 /* ReaderSiteService.m in Sources */, 98FCFC232231DF43006ECDD4 /* PostStatsTitleCell.swift in Sources */, E1556CF2193F6FE900FC52EA /* CommentService.m in Sources */, - D80BC7A22074739400614A59 /* MediaLibraryStrings.swift in Sources */, 9A09F915230C3E9700F42AB7 /* StoreFetchingStatus.swift in Sources */, F582060223A85495005159A9 /* SiteDateFormatters.swift in Sources */, 83E1E5592A58B5C2000B576F /* JetpackSocialError.swift in Sources */, @@ -22425,12 +22546,11 @@ 837B49D7283C2AE80061A657 /* BloggingPromptSettings+CoreDataClass.swift in Sources */, 98BC522A27F6259700D6E8C2 /* BloggingPromptsFeatureDescriptionView.swift in Sources */, FAD9458E25B5678700F011B5 /* JetpackRestoreWarningCoordinator.swift in Sources */, - 5DB3BA0518D0E7B600F3F3E9 /* WPPickerView.m in Sources */, 8386C6A32AC4E3C700568183 /* ReaderPostCardCell.swift in Sources */, - 0880BADC29ED6FF3002D3AB0 /* UIColor+DesignSystem.swift in Sources */, 805CC0C1296CF3B3002941DC /* JetpackNewUsersOverlaySecondaryView.swift in Sources */, B5E94D151FE04815000E7C20 /* UIImageView+SiteIcon.swift in Sources */, 17CE77ED20C6C2F3001DEA5A /* ReaderSiteSearchService.swift in Sources */, + 0CE538CA2B0D6E0000834BA2 /* ExternalMediaDataSource.swift in Sources */, 98FF6A3E23A30A250025FD72 /* QuickStartNavigationSettings.swift in Sources */, 5D42A405175E76A7005CFF05 /* WPImageViewController.m in Sources */, 0C896DE02A3A763400D7D4E7 /* SettingsCell.swift in Sources */, @@ -22441,6 +22561,7 @@ FEA1123C29944896008097B0 /* SelfHostedJetpackRemoteInstallViewModel.swift in Sources */, FAD7625B29ED780B00C09583 /* JSONDecoderExtension.swift in Sources */, C81CCD83243BF7A600A83E27 /* TenorDataLoader.swift in Sources */, + F4D140222AFE794300961797 /* RegisterDomainTransferFooterView.swift in Sources */, 98797DBC222F434500128C21 /* OverviewCell.swift in Sources */, F484D4ED2A32C4520050BE15 /* CATransaction+Extension.swift in Sources */, C3BC86F629528997005D1A01 /* PeopleViewController+JetpackBannerViewController.swift in Sources */, @@ -22448,7 +22569,6 @@ 5DA3EE161925090A00294E0B /* MediaService.m in Sources */, 8BF9E03327B1A8A800915B27 /* DashboardCard.swift in Sources */, 7E4123C520F4097B00DF8486 /* FormattableContentAction.swift in Sources */, - D8A468E521828D940094B82F /* SiteVerticalsService.swift in Sources */, 3F8D988926153484003619E5 /* UnifiedPrologueBackgroundView.swift in Sources */, E65219FB1B8D10DA000B1217 /* ReaderBlockedSiteCell.swift in Sources */, FAF13C5325A57ABD003EE470 /* JetpackRestoreWarningViewController.swift in Sources */, @@ -22464,6 +22584,7 @@ 80C740FB2989FC4600199027 /* PostStatsTableViewController+JetpackBannerViewController.swift in Sources */, B5EEDB971C91F10400676B2B /* Blog+Interface.swift in Sources */, 80D9D04A29FC0D9000FE3400 /* NSMutableArray+NullableObjects.m in Sources */, + 0C0AD1062B0C483F00EC06E6 /* ExternalMediaSelectionTitleView.swift in Sources */, 019D69A02A5EBF47003B676D /* WordPressAuthenticatorProtocol.swift in Sources */, 982DDF96263238A6002B3904 /* LikeUserPreferredBlog+CoreDataProperties.swift in Sources */, 5D839AA8187F0D6B00811F4A /* PostFeaturedImageCell.m in Sources */, @@ -22476,9 +22597,9 @@ D82253DC2199411F0014D0E2 /* SiteAddressService.swift in Sources */, 83B1D037282C62620061D911 /* BloggingPromptsAttribution.swift in Sources */, B5B56D3319AFB68800B4E29B /* WPStyleGuide+Notifications.swift in Sources */, + 01B5C3C72AE7FC61007055BB /* UITestConfigurator.swift in Sources */, FF8DDCDF1B5DB1C10098826F /* SettingTableViewCell.m in Sources */, 9A3BDA0E22944F3500FBF510 /* CountriesMapView.swift in Sources */, - 17BB26AE1E6D8321008CD031 /* MediaLibraryViewController.swift in Sources */, 98E58A2F2360D23400E5534B /* TodayWidgetStats.swift in Sources */, F5E1577F25DE04E200EEEDFB /* GutenbergMediaFilesUploadProcessor.swift in Sources */, 80EF672227F160720063B138 /* DashboardCustomAnnouncementCell.swift in Sources */, @@ -22510,7 +22631,6 @@ B5416CF51C171D7100006DD8 /* PushNotificationsManager.swift in Sources */, 98AE3DF5219A1789003C0E24 /* StatsInsightsStore.swift in Sources */, 08216FCD1CDBF96000304BA7 /* MenuItemPagesViewController.m in Sources */, - 57D6C840229498C5003DDC7E /* InteractivePostView.swift in Sources */, 32F2566025012D3F006B8BC4 /* LinearGradientView.swift in Sources */, D865721221869C590023A99C /* WizardStep.swift in Sources */, 43D54D131DCAA070007F575F /* PostPostViewController.swift in Sources */, @@ -22530,7 +22650,6 @@ D86572172186C3600023A99C /* WizardDelegate.swift in Sources */, 73713583208EA4B900CCDFC8 /* Blog+Files.swift in Sources */, 439F4F3C219B78B500F8D0C7 /* RevisionDiffsBrowserViewController.swift in Sources */, - 08799C252A334645005317F7 /* Spacing.swift in Sources */, 46F583AD2624CE790010A723 /* BlockEditorSettingElement+CoreDataClass.swift in Sources */, FAD256932611B01700EDAF88 /* UIColor+WordPressColors.swift in Sources */, E174F6E6172A73960004F23A /* WPAccount.m in Sources */, @@ -22538,15 +22657,15 @@ 98467A3F221CD48500DF51AE /* SiteStatsDetailTableViewController.swift in Sources */, 83BFAE482A6EBF1F00C7B683 /* DashboardJetpackSocialCardCell.swift in Sources */, E11450DF1C4E47E600A6BD0F /* MessageAnimator.swift in Sources */, + FE6AFE432B18EDF200F76520 /* BloganuaryTracker.swift in Sources */, C77FC90F2800CAC100726F00 /* OnboardingEnableNotificationsViewController.swift in Sources */, FA4F65A72594337300EAA9F5 /* JetpackRestoreOptionsViewController.swift in Sources */, - 7E14635720B3BEAB00B95F41 /* WPStyleGuide+Loader.swift in Sources */, - 087EBFA81F02313E001F7ACE /* MediaThumbnailService.swift in Sources */, F4DD58322A095210009A772D /* DataMigrationError.swift in Sources */, 08F8CD2A1EBD22EF0049D0C0 /* MediaExporter.swift in Sources */, FAC086D725EDFB1E00B94F2A /* ReaderRelatedPostsCell.swift in Sources */, 324780E1247F2E2A00987525 /* NoResultsViewController+FollowedSites.swift in Sources */, E100C6BB1741473000AE48D8 /* WordPress-11-12.xcmappingmodel in Sources */, + 80DB57942AF8D04600C728FF /* DomainPurchaseChoicesView.swift in Sources */, B07F133E2A16C69800AF7FCF /* PlanSelectionViewController.swift in Sources */, 837A53D72AD8C3A400B941A2 /* ReaderFollowButton.swift in Sources */, 9822A8412624CFB900FD8A03 /* UserProfileSiteCell.swift in Sources */, @@ -22572,7 +22691,6 @@ F163541626DE2ECE008B625B /* NotificationEventTracker.swift in Sources */, 3F8CBE0D24EED2CB00F71234 /* FindOutMoreCell.swift in Sources */, FF286C761DE70A4500383A62 /* NSURL+Exporters.swift in Sources */, - 3F3DD0AF26FCDA3100F5F121 /* PresentationButton.swift in Sources */, E1A03EE217422DCF0085D192 /* BlogToAccount.m in Sources */, C77FC90928009C7000726F00 /* OnboardingQuestionsPromptViewController.swift in Sources */, 8BE6F92C27EE27DB0008BDC7 /* BlogDashboardPostCardGhostCell.swift in Sources */, @@ -22585,9 +22703,7 @@ FA8E1F7725EEFA7300063673 /* ReaderPostService+RelatedPosts.swift in Sources */, B518E1651CCAA19200ADFE75 /* Blog+Capabilities.swift in Sources */, FADC40AE2A8D2E8D00C19997 /* ImageDownloader+Gravatar.swift in Sources */, - 575E126F229779E70041B3EB /* RestorePostTableViewCell.swift in Sources */, 08216FC91CDBF96000304BA7 /* MenuItemCategoriesViewController.m in Sources */, - 171CC15824FCEBF7008B7180 /* UINavigationBar+Appearance.swift in Sources */, 43C9908E21067E22009EFFEB /* QuickStartChecklistViewController.swift in Sources */, 80EF929028105CFA0064A971 /* QuickStartFactory.swift in Sources */, 082635BB1CEA69280088030C /* MenuItemsViewController.m in Sources */, @@ -22600,6 +22716,7 @@ FEC26033283FC902003D886A /* RootViewCoordinator+BloggingPrompt.swift in Sources */, 93E63369272AC532009DACF8 /* LoginEpilogueChooseSiteTableViewCell.swift in Sources */, 9A51DA1522E9E8C7005CC335 /* ChangeUsernameViewController.swift in Sources */, + 0CF0C4232AE98C13006FFAB4 /* AbstractPostHelper.swift in Sources */, 5D6C4B121B604190005E3C43 /* RichTextView.swift in Sources */, 3F43602F23F31D48001DEE70 /* ScenePresenter.swift in Sources */, 9A4E271D22EF33F5001F6A6B /* AccountSettingsStore.swift in Sources */, @@ -22608,9 +22725,11 @@ 73FF7032221F469100541798 /* Charts+Support.swift in Sources */, F16601C423E9E783007950AE /* SharingAuthorizationWebViewController.swift in Sources */, 171963401D378D5100898E8B /* SearchWrapperView.swift in Sources */, + 0830538C2B2732E400B889FE /* DynamicDashboardCard.swift in Sources */, 241E60B325CA0D2900912CEB /* UserSettings.swift in Sources */, 0815CF461E96F22600069916 /* MediaImportService.swift in Sources */, B56F25881FBDE502005C33E4 /* NSAttributedStringKey+Conversion.swift in Sources */, + 0CA10F732ADB014C00CE75AC /* StringRankedSearch.swift in Sources */, 08AA64052A84FFF40076E38D /* DashboardGoogleDomainsViewModel.swift in Sources */, 98E54FF2265C972900B4BE9A /* ReaderDetailLikesView.swift in Sources */, FFABD80821370496003C65B6 /* SelectPostViewController.swift in Sources */, @@ -22644,16 +22763,19 @@ C81CCD6F243AF7D700A83E27 /* TenorReponseParser.swift in Sources */, 4020B2BD2007AC850002C963 /* WPStyleGuide+Gridicon.swift in Sources */, 982D261F2788DDF200A41286 /* ReaderCommentsFollowPresenter.swift in Sources */, + F413F7882B2B253A00A64A94 /* DashboardCard+Personalization.swift in Sources */, F5E032D6240889EB003AF350 /* CreateButtonCoordinator.swift in Sources */, 74729CA32056FA0900D1394D /* SearchManager.swift in Sources */, 7E8980CA22E8C7A600C567B0 /* BlogToBlogMigration87to88.swift in Sources */, FA1ACAA21BC6E45D00DDDCE2 /* WPStyleGuide+Themes.swift in Sources */, 5D1181E71B4D6DEB003F3084 /* WPStyleGuide+Reader.swift in Sources */, 3250490724F988220036B47F /* Interpolation.swift in Sources */, + 0CB424F62AE0416D0080B807 /* SolidColorActivityIndicator.swift in Sources */, 0C8FC9A12A8BC8630059DCE4 /* PHPickerController+Extensions.swift in Sources */, E17FEAD8221490F7006E1D2D /* PostEditorAnalyticsSession.swift in Sources */, 738B9A5221B85CF20005062B /* SiteCreationWizardLauncher.swift in Sources */, 46F583AF2624CE790010A723 /* BlockEditorSettingElement+CoreDataProperties.swift in Sources */, + F41D98E42B39CAA5004EC050 /* BlogDashboardDynamicCardCoordinator.swift in Sources */, 3234B8E7252FA0930068DA40 /* ReaderSitesCardCell.swift in Sources */, C7234A4E2832C47D0045C63F /* QRLoginVerifyAuthorizationViewController.swift in Sources */, 08CBC77929AE6CC4000026E7 /* SiteCreationEmptySiteTemplate.swift in Sources */, @@ -22668,6 +22790,7 @@ 93CDC72126CD342900C8A3A8 /* DestructiveAlertHelper.swift in Sources */, 982D99FE26F922C100AA794C /* InlineEditableMultiLineCell.swift in Sources */, B5FF3BE71CAD881100C1D597 /* ImageCropOverlayView.swift in Sources */, + FACF66CA2ADD4703008C3E13 /* PostListCell.swift in Sources */, F5E032DB24088F44003AF350 /* UIView+SpringAnimations.swift in Sources */, E6A338501BB0A70F00371587 /* ReaderGapMarkerCell.swift in Sources */, 98BDFF6B20D0732900C72C58 /* SupportTableViewController+Activity.swift in Sources */, @@ -22677,7 +22800,6 @@ B5CABB171C0E382C0050AB9F /* PickerTableViewCell.swift in Sources */, F5AE43E425DD02C1003675F4 /* StoryEditor.swift in Sources */, F4C1FC662A44836300AD7CB0 /* PrivacySettingsAnalytics.swift in Sources */, - 08A4E129289D202F001D9EC7 /* UserPersistentStore.swift in Sources */, E1D95EB817A28F5E00A3E9F3 /* WPActivityDefaults.m in Sources */, C31852A129670F8100A78BE9 /* JetpackScanViewController+JetpackBannerViewController.swift in Sources */, 436D56302117410C00CEAA33 /* RegisterDomainDetailsViewModel+CellIndex.swift in Sources */, @@ -22706,6 +22828,7 @@ 591AA5021CEF9BF20074934F /* Post+CoreDataProperties.swift in Sources */, E19B17AE1E5C6944007517C6 /* BasePost.swift in Sources */, C71AF533281064DE00F9E99E /* OnboardingQuestionsCoordinator.swift in Sources */, + 01B759062B3ECA7300179AE6 /* DomainsStateViewModel.swift in Sources */, 0CD223DF2AA8ADFD002BD761 /* DashboardQuickActionsViewModel.swift in Sources */, 7E7947A9210BAC1D005BB851 /* NotificationContentRange.swift in Sources */, 8B6EA62323FDE50B004BA312 /* PostServiceUploadingList.swift in Sources */, @@ -22734,6 +22857,8 @@ 836498CE281735CC00A2C170 /* BloggingPromptsHeaderView.swift in Sources */, 3F43703F2893201400475B6E /* JetpackOverlayViewController.swift in Sources */, 319D6E8519E44F7F0013871C /* SuggestionsTableViewCell.m in Sources */, + 01B7590B2B3ED63B00179AE6 /* DomainDetailsWebViewControllerWrapper.swift in Sources */, + 0CB424F12ADEE52A0080B807 /* PostSearchToken.swift in Sources */, 98AA6D1126B8CE7200920C8B /* Comment+CoreDataClass.swift in Sources */, 7E4A773720F802A8001C706D /* ActivityRangesFactory.swift in Sources */, E1AC282D18282423004D394C /* SFHFKeychainUtils.m in Sources */, @@ -22744,12 +22869,11 @@ 0C0453282AC73343003079C8 /* SiteMediaVideoDurationView.swift in Sources */, 837B49D9283C2AE80061A657 /* BloggingPromptSettings+CoreDataProperties.swift in Sources */, 436D56212117312700CEAA33 /* RegisterDomainDetailsViewModel+SectionDefinitions.swift in Sources */, - F53FF3A823EA723D001AD596 /* ActionRow.swift in Sources */, FFA162311CB7031A00E2E110 /* AppSettingsViewController.swift in Sources */, F111B87826580FCE00057942 /* BloggingRemindersStore.swift in Sources */, 8B16CE9A25251C89007BE5A9 /* ReaderPostStreamService.swift in Sources */, - 57AA848F228715DA00D3C2A2 /* PostCardCell.swift in Sources */, FE43DAAF26DFAD1C00CFF595 /* CommentContentTableViewCell.swift in Sources */, + 0C308FFE2B1234E70071C551 /* SiteMediaFilterButtonView.swift in Sources */, 803BB986295A873800B3F6D6 /* RootViewPresenter+MeNavigation.swift in Sources */, F4CBE3D6292597E3004FFBB6 /* SupportTableViewControllerConfiguration.swift in Sources */, 8F22804451E5812433733348 /* TimeZoneSearchHeaderView.swift in Sources */, @@ -22766,8 +22890,9 @@ 3F762E9726784BED0088CD45 /* FancyAlertComponent.swift in Sources */, 3F2F854226FAEA50000FCDA5 /* JetpackScanScreen.swift in Sources */, 3F2F855A26FAF227000FCDA5 /* EditorNoticeComponent.swift in Sources */, - 01281E9A2A0456CB00464F8F /* DomainsSuggestionsScreen.swift in Sources */, + 01281E9A2A0456CB00464F8F /* DomainsSelectionScreen.swift in Sources */, 3F2F855D26FAF227000FCDA5 /* LoginCheckMagicLinkScreen.swift in Sources */, + 3F03F2BD2B45041E00A9CE99 /* XCUIElement+TapUntil.swift in Sources */, EA78189427596B2F00554DFA /* ContactUsScreen.swift in Sources */, D82E087829EEB7AF0098F500 /* DomainsScreen.swift in Sources */, 3F6A8CE02A246357009DBC2B /* XCUIApplication+SavePassword.swift in Sources */, @@ -22796,6 +22921,7 @@ EAD08D0E29D45E23001A72F9 /* CommentsScreen.swift in Sources */, 3F762E9526784B540088CD45 /* WireMock.swift in Sources */, C7B7CC7328134347007B9807 /* OnboardingQuestionsPromptScreen.swift in Sources */, + 1D0402762B10FB9E00888C30 /* AppSettingsScreen.swift in Sources */, 3FE39A3F26F8384E006E2B3A /* StatsScreen.swift in Sources */, 3FE39A3126F836A5006E2B3A /* LoginSiteAddressScreen.swift in Sources */, 3F2F855126FAF227000FCDA5 /* WelcomeScreenLoginComponent.swift in Sources */, @@ -23281,7 +23407,6 @@ 73178C2821BEE09300E37C9A /* SiteCreationDataCoordinatorTests.swift in Sources */, 4A266B8F282B05210089CF3D /* JSONObjectTests.swift in Sources */, D81C2F6020F891C4002AE1F1 /* TrashCommentActionTests.swift in Sources */, - 570BFD8D22823DE5007859A8 /* PostActionSheetTests.swift in Sources */, FA4ADADA1C509FE400F858D7 /* SiteManagementServiceTests.swift in Sources */, 3F1B66A323A2F54B0075F09E /* ReaderReblogActionTests.swift in Sources */, FEE48EFF2A4C9855008A48E0 /* Blog+PublicizeTests.swift in Sources */, @@ -23292,7 +23417,7 @@ F9941D1822A805F600788F33 /* UIImage+XCAssetTests.swift in Sources */, 3F28CEAF2A4ACEBE00B79686 /* PrivacySettingsViewControllerTests.swift in Sources */, 08A2AD7B1CCED8E500E84454 /* PostCategoryServiceTests.m in Sources */, - D88A64A8208D9733008AE9BC /* ThumbnailCollectionTests.swift in Sources */, + D88A64A8208D9733008AE9BC /* StockPhotosThumbnailCollectionTests.swift in Sources */, 572FB401223A806000933C76 /* NoticeStoreTests.swift in Sources */, F5CFB8F524216DFC00E58B69 /* CalendarHeaderViewTests.swift in Sources */, 748437EE1F1D4A7300E8DDAF /* RichContentFormatterTests.swift in Sources */, @@ -23329,7 +23454,6 @@ D8BA274D20FDEA2E007A5C77 /* NotificationTextContentTests.swift in Sources */, 2481B1D5260D4E8B00AE59DB /* AccountBuilder.swift in Sources */, E66969C81B9E0A6800EC9C00 /* ReaderTopicServiceTest.swift in Sources */, - 570265172298960B00F2214C /* PostListTableViewHandlerTests.swift in Sources */, E1EBC3731C118ED200F638E0 /* ImmuTableTest.swift in Sources */, F5C00EAE242179780047846F /* WeekdaysHeaderViewTests.swift in Sources */, 24C69AC22612467C00312D9A /* UserSettingsTestsObjc.m in Sources */, @@ -23340,6 +23464,7 @@ B0A6DEBF2626335F00B5B8EF /* AztecPostViewController+MenuTests.swift in Sources */, 93B853231B4416A30064FE72 /* WPAnalyticsTrackerAutomatticTracksTests.m in Sources */, C738CB0B28623CED001BE107 /* QRLoginCoordinatorTests.swift in Sources */, + F41D98E12B39C5CE004EC050 /* BlogDashboardDynamicCardCoordinatorTests.swift in Sources */, FE2E3729281C839C00A1E82A /* BloggingPromptsServiceTests.swift in Sources */, D848CC0720FF2BE200A9038F /* NotificationContentRangeFactoryTests.swift in Sources */, 732A473F21878EB10015DA74 /* WPRichContentViewTests.swift in Sources */, @@ -23358,13 +23483,11 @@ 80EF92932810FA5A0064A971 /* QuickStartFactoryTests.swift in Sources */, 80C523AB29AE6C2200B1C14B /* BlazeCreateCampaignWebViewModelTests.swift in Sources */, D848CBFF20FF010F00A9038F /* FormattableCommentContentTests.swift in Sources */, - 9123471B221449E200BD9F97 /* GutenbergInformativeDialogTests.swift in Sources */, 8332DD2829259BEB00802F7D /* DataMigratorTests.swift in Sources */, C80512FE243FFD4B00B6B04D /* TenorDataSouceTests.swift in Sources */, 323F8F3023A22C4C000BA49C /* SiteCreationRotatingMessageViewTests.swift in Sources */, FF1FD02620912AA900186384 /* URL+LinkNormalizationTests.swift in Sources */, 4A76A4BD29D43BFD00AABF4B /* CommentService+MorderationTests.swift in Sources */, - 57AA8493228790AA00D3C2A2 /* PostCardCellTests.swift in Sources */, 7E442FCF20F6C19000DEACA5 /* ActivityLogFormattableContentTests.swift in Sources */, F151EC832665271200AEA89E /* BloggingRemindersSchedulerTests.swift in Sources */, D81C2F6620F8ACCD002AE1F1 /* FormattableContentFormatterTests.swift in Sources */, @@ -23376,6 +23499,7 @@ FF0B2567237A023C004E255F /* GutenbergVideoUploadProcessorTests.swift in Sources */, FF1B11E7238FE27A0038B93E /* GutenbergGalleryUploadProcessorTests.swift in Sources */, FE320CC5294705990046899B /* ReaderPostBackupTests.swift in Sources */, + F46546332AF54DCD0017E3D1 /* AllDomainsListItemViewModelTests.swift in Sources */, F4D9AF51288AE23500803D40 /* SuggestionTableViewTests.swift in Sources */, 8BC12F72231FEBA1004DDA72 /* PostCoordinatorTests.swift in Sources */, C3C2F84628AC8BC700937E45 /* JetpackBannerScrollVisibilityTests.swift in Sources */, @@ -23387,10 +23511,10 @@ F4D7FD6C2A57030E00642E06 /* CompliancePopoverViewControllerTests.swift in Sources */, DCFC6A29292523D20062D65B /* SiteStatsPinnedItemStoreTests.swift in Sources */, D88A64AC208D9B09008AE9BC /* StockPhotosPageableTests.swift in Sources */, + 3FFB3F222AFC72EC00A742B0 /* DeepLinkSourceTests.swift in Sources */, 0148CC2B2859C87000CF5D96 /* BlogServiceMock.swift in Sources */, 59ECF87B1CB7061D00E68F25 /* PostSharingControllerTests.swift in Sources */, E157D5E01C690A6C00F04FB9 /* ImmuTableTestUtils.swift in Sources */, - FF7C89A31E3A1029000472A8 /* MediaLibraryPickerDataSourceTests.swift in Sources */, C81CCD86243C00E000A83E27 /* TenorPageableTests.swift in Sources */, 46F58501262605930010A723 /* BlockEditorSettingsServiceTests.swift in Sources */, 8BBBEBB224B8F8C0005E358E /* ReaderCardTests.swift in Sources */, @@ -23422,6 +23546,7 @@ C738CB0D28623F07001BE107 /* QRLoginURLParserTests.swift in Sources */, D809E686203F0215001AA0DE /* OldReaderPostCardCellTests.swift in Sources */, 4AAD69082A6F68A5007FE77E /* MediaRepositoryTests.swift in Sources */, + 01B7590E2B3EEEA400179AE6 /* SiteDomainsViewModelTests.swift in Sources */, FEFC0F8C273131A6001F7F1D /* CommentService+RepliesTests.swift in Sources */, 40E4698F2017E0700030DB5F /* PluginDirectoryEntryStateTests.swift in Sources */, 8BC6020D2390412000EFE3D0 /* NullBlogPropertySanitizerTests.swift in Sources */, @@ -23429,24 +23554,27 @@ D848CC1920FF3A2400A9038F /* FormattableNotIconTests.swift in Sources */, 32110547250BFC3E0048446F /* ImageDimensionParserTests.swift in Sources */, E1AB5A3A1E0C464700574B4E /* DelayTests.swift in Sources */, - C9B477B029CC35C5008CBF49 /* WidgetDataReaderTests.swift in Sources */, 8B7F51CB24EED8A8008CF5B5 /* ReaderTrackerTests.swift in Sources */, D848CC0320FF04FA00A9038F /* FormattableUserContentTests.swift in Sources */, + F41D98E82B39E14F004EC050 /* DashboardDynamicCardAnalyticsEventTests.swift in Sources */, 5948AD111AB73D19006E8882 /* WPAppAnalyticsTests.m in Sources */, 0C8FC9AA2A8C57000059DCE4 /* ItemProviderMediaExporterTests.swift in Sources */, + 4AD862E52AFAEF1700A07557 /* PostsListAPIStub.swift in Sources */, 0A69300B28B5AA5E00E98DE1 /* FullScreenCommentReplyViewModelTests.swift in Sources */, + 0CA10FA52ADB286300CE75AC /* StringRankedSearchTests.swift in Sources */, FF8032661EE9E22200861F28 /* MediaProgressCoordinatorTests.swift in Sources */, 173D82E7238EE2A7008432DA /* FeatureFlagTests.swift in Sources */, 3F4A4C212AD39CB100DE5DF8 /* TruthTable.swift in Sources */, + FA141F322AF139A200C9A653 /* PageMenuViewModelTests.swift in Sources */, E63C897C1CB9A0D700649C8F /* UITextFieldTextHelperTests.swift in Sources */, FF2EC3C22209AC19006176E1 /* GutenbergImgUploadProcessorTests.swift in Sources */, D81C2F5A20F86E94002AE1F1 /* LikeCommentActionTests.swift in Sources */, 8B8C814D2318073300A0E620 /* BasePostTests.swift in Sources */, F1F083F6241FFE930056D3B1 /* AtomicAuthenticationServiceTests.swift in Sources */, - FF8CD625214184EE00A33A8D /* MediaAssetExporterTests.swift in Sources */, C373D6EA280452F6008F8C26 /* SiteIntentDataTests.swift in Sources */, 8B69F100255C4870006B1CEF /* ActivityStoreTests.swift in Sources */, B5C0CF3F204DB92F00DB0362 /* NotificationReplyStoreTests.swift in Sources */, + 4A5598852B05AC180083C220 /* PagesListTests.swift in Sources */, 575E126322973EBB0041B3EB /* PostCompactCellGhostableTests.swift in Sources */, 2481B20C260D8FED00AE59DB /* WPAccount+ObjCLookupTests.m in Sources */, FEFA6AC82A88D5FC004EE5E6 /* Post+JetpackSocialTests.swift in Sources */, @@ -23468,19 +23596,18 @@ 02BE5CC02281B53F00E351BA /* RegisterDomainDetailsViewModelLoadingStateTests.swift in Sources */, FEFA263E26C58427009CCB7E /* ShareAppTextActivityItemSourceTests.swift in Sources */, 0C6C4CD02A4F0A000049E762 /* BlazeCampaignsStreamTests.swift in Sources */, - 577C2AAB22936DCB00AD1F03 /* PostCardCellGhostableTests.swift in Sources */, 08B954F328535EE800B07185 /* FeatureHighlightStoreTests.swift in Sources */, AE3047AA270B66D300FE9266 /* Scanner+QuotedTextTests.swift in Sources */, 02761EC4227010BC009BAF0F /* BlogDetailsSectionIndexTests.swift in Sources */, 732A473D218787500015DA74 /* WPRichTextFormatterTests.swift in Sources */, 1ABA150822AE5F870039311A /* WordPressUIBundleTests.swift in Sources */, + F46546352AF550A20017E3D1 /* AllDomainsListItem+Helpers.swift in Sources */, FEA312842987FB0100FFD405 /* BlogJetpackTests.swift in Sources */, 0C35FFF429CBA6DA00D224EB /* BlogDashboardPersonalizationViewModelTests.swift in Sources */, 57569CF2230485680052EE14 /* PostAutoUploadInteractorTests.swift in Sources */, 7E8980B922E73F4000C567B0 /* EditorSettingsServiceTests.swift in Sources */, 1797373720EBAA4100377B4E /* RouteMatcherTests.swift in Sources */, 73178C2A21BEE09300E37C9A /* SiteSegmentsCellTests.swift in Sources */, - 175CC17527205BFB00622FB4 /* DomainExpiryDateFormatterTests.swift in Sources */, B5416CFE1C1756B900006DD8 /* PushNotificationsManagerTests.m in Sources */, 321955C124BE4EBF00E3F316 /* ReaderSelectInterestsCoordinatorTests.swift in Sources */, F4EF4BAB291D3D4700147B61 /* SiteIconTests.swift in Sources */, @@ -23489,18 +23616,16 @@ C3E42AB027F4D30E00546706 /* MenuItemsViewControllerTests.swift in Sources */, D842EA4021FABB1800210E96 /* SiteSegmentTests.swift in Sources */, C3C70C562835C5BB00DD2546 /* SiteDesignSectionLoaderTests.swift in Sources */, - 08A4E12F289D2795001D9EC7 /* UserPersistentStoreTests.swift in Sources */, 436D55F5211632B700CEAA33 /* RegisterDomainDetailsViewModelTests.swift in Sources */, E180BD4C1FB462FF00D0D781 /* CookieJarTests.swift in Sources */, F4394D1F2A3AB06F003955C6 /* WPCrashLoggingDataProviderTests.swift in Sources */, 9363113F19FA996700B0C739 /* AccountServiceTests.swift in Sources */, 17FC0032264D728E00FCBD37 /* SharingServiceTests.swift in Sources */, - D88A649C208D7D81008AE9BC /* StockPhotosDataSourceTests.swift in Sources */, F4DD58362A168229009A772D /* ReaderPostCellActionsTests.swift in Sources */, 3236F7A124B61B950088E8F3 /* ReaderInterestsDataSourceTests.swift in Sources */, + 0CB424F42ADF3CBE0080B807 /* PostSearchViewModelTests.swift in Sources */, 8BDA5A74247C5EAA00AB124C /* ReaderDetailCoordinatorTests.swift in Sources */, 74585B991F0D58F300E7E667 /* DomainsServiceTests.swift in Sources */, - 8B7623382384373E00AB3EE7 /* PageListViewControllerTests.swift in Sources */, D88A64B0208DA093008AE9BC /* StockPhotosResultsPageTests.swift in Sources */, 0879FC161E9301DD00E1EFC8 /* MediaTests.swift in Sources */, B556EFCB1DCA374200728F93 /* DictionaryHelpersTests.swift in Sources */, @@ -23512,16 +23637,15 @@ 4A2C73E42A943DEA00ACE79E /* TaggedManagedObjectIDTests.swift in Sources */, 0CF7D6C32ABB753A006D1E89 /* MediaImageServiceTests.swift in Sources */, 8BFE36FF230F1C850061EBA8 /* AbstractPost+fixLocalMediaURLsTests.swift in Sources */, - C995C22629D30AB000ACEF43 /* WidgetUrlSourceTests.swift in Sources */, 08A2AD791CCED2A800E84454 /* PostTagServiceTests.m in Sources */, F543AF5723A84E4D0022F595 /* PublishSettingsControllerTests.swift in Sources */, + 3FE6D31E2B0705D400D14923 /* JetpackBrandingVisibilityTests.swift in Sources */, 027AC5212278983F0033E56E /* DomainCreditEligibilityTests.swift in Sources */, F551E7F723FC9A5C00751212 /* Collection+RotateTests.swift in Sources */, 2481B1E8260D4EAC00AE59DB /* WPAccount+LookupTests.swift in Sources */, E10F3DA11E5C2CE0008FAADA /* PostListFilterTests.swift in Sources */, 57D66B9D234BB78B005A2D74 /* PostServiceWPComTests.swift in Sources */, DCF892D0282FA42A00BB71E1 /* SiteStatsImmuTableRowsTests.swift in Sources */, - 8B939F4323832E5D00ACCB0F /* PostListViewControllerTests.swift in Sources */, 93EF094C19ED533500C89770 /* ContextManagerTests.swift in Sources */, F17A2A2023BFBD84001E96AC /* UIView+ExistingConstraints.swift in Sources */, 9A9D34FD23607CCC00BC95A3 /* AsyncOperationTests.swift in Sources */, @@ -23545,11 +23669,14 @@ 01E78D1D296EA54F00FB6863 /* StatsPeriodHelperTests.swift in Sources */, 3FFE3C0828FE00D10021BB96 /* StatsSegmentedControlDataTests.swift in Sources */, 015BA4EB29A788A300920F4B /* StatsTotalInsightsCellTests.swift in Sources */, + 0C1DB60B2B0A9A570028F200 /* ImageDownloaderTests.swift in Sources */, D81C2F5820F86CEA002AE1F1 /* NetworkStatus.swift in Sources */, + 4AA7EE0F2ADF7367007D261D /* PostRepositoryPostsListTests.swift in Sources */, E1C545801C6C79BB001CEB0E /* MediaSettingsTests.swift in Sources */, 806BA11C2A492B0F00052422 /* BlazeCampaignDetailsWebViewModelTests.swift in Sources */, 7E987F5A2108122A00CAFB88 /* NotificationUtility.swift in Sources */, FE32E7F12844971000744D80 /* ReminderScheduleCoordinatorTests.swift in Sources */, + F4F7B2532AFA585700207282 /* DomainDetailsWebViewControllerTests.swift in Sources */, 4688E6CC26AB571D00A5D894 /* RequestAuthenticatorTests.swift in Sources */, 7E442FC720F677CB00DEACA5 /* ActivityLogRangesTest.swift in Sources */, FA3FBF8E2A2777E00012FC90 /* DashboardActivityLogViewModelTests.swift in Sources */, @@ -23578,7 +23705,6 @@ E6B9B8AA1B94E1FE0001B92F /* ReaderPostTest.m in Sources */, 80EF9284280CFEB60064A971 /* DashboardPostsSyncManagerTests.swift in Sources */, 7EC9FE0B22C627DB00C5A888 /* PostEditorAnalyticsSessionTests.swift in Sources */, - D88A64A0208D8B7D008AE9BC /* StockPhotosMediaGroupTests.swift in Sources */, 4AEF2DD929A84B2C00345734 /* ReaderSiteServiceTests.swift in Sources */, 80535DC5294BF4BE00873161 /* JetpackBrandingMenuCardPresenterTests.swift in Sources */, 7EAA66EF22CB36FD00869038 /* TestAnalyticsTracker.swift in Sources */, @@ -23596,13 +23722,13 @@ 8BD36E062395CC4400EFFF1C /* MediaEditorOperation+DescriptionTests.swift in Sources */, 8B5FEC0225A750CB000CBFF7 /* UIApplication+mainWindow.swift in Sources */, 3F28CEA52A4ABB8800B79686 /* PrivacySettingsAnalyticsTrackerTests.swift in Sources */, - E6843840221F5A2200752258 /* PostListExcessiveLoadMoreTests.swift in Sources */, 325D3B3D23A8376400766DF6 /* FullScreenCommentReplyViewControllerTests.swift in Sources */, 805CC0B9296680F7002941DC /* RemoteConfigStoreMock.swift in Sources */, 0CD382862A4B6FCF00612173 /* DashboardBlazeCardCellViewModelTest.swift in Sources */, 0147D651294B6EA600AA6410 /* StatsRevampStoreTests.swift in Sources */, 57D6C83E22945A10003DDC7E /* PostCompactCellTests.swift in Sources */, B030FE0A27EBF0BC000F6F5E /* SiteCreationIntentTracksEventTests.swift in Sources */, + 3F56F55C2AEA2F67006BDCEA /* ReaderPostBuilder.swift in Sources */, D81C2F5C20F872C2002AE1F1 /* ReplyToCommentActionTests.swift in Sources */, 08C42C31281807880034720B /* ReaderSubscribeCommentsActionTests.swift in Sources */, D848CC1720FF38EA00A9038F /* FormattableCommentRangeTests.swift in Sources */, @@ -23651,6 +23777,7 @@ 0C896DE72A3A832B00D7D4E7 /* SiteVisibilityTests.swift in Sources */, E1B921BC1C0ED5A3003EA3CB /* MediaSizeSliderCellTest.swift in Sources */, C314543B262770BE005B216B /* BlogServiceAuthorTests.swift in Sources */, + FE34ACD22B174AE700108B3C /* DashboardBloganuaryCardCellTests.swift in Sources */, 3F50945F245537A700C4470B /* ReaderTabViewModelTests.swift in Sources */, 0885A3671E837AFE00619B4D /* URLIncrementalFilenameTests.swift in Sources */, D848CBF920FEF82100A9038F /* NotificationsContentFactoryTests.swift in Sources */, @@ -23689,6 +23816,7 @@ EA14533229AD874C001F3143 /* SignupTests.swift in Sources */, EA14533329AD874C001F3143 /* EditorFlow.swift in Sources */, 01281E9D2A051EEA00464F8F /* MenuNavigationTests.swift in Sources */, + 1D0402742B10FA9100888C30 /* AppSettingsTests.swift in Sources */, EA14533429AD874C001F3143 /* UIApplication+mainWindow.swift in Sources */, EA14533529AD874C001F3143 /* LoginFlow.swift in Sources */, EA14533629AD874C001F3143 /* XCTest+Extensions.swift in Sources */, @@ -23706,17 +23834,18 @@ FABB20C82602FC2C00C8785C /* ActivityPluginRange.swift in Sources */, FABB20C92602FC2C00C8785C /* ShareNoticeViewModel.swift in Sources */, FABB20CA2602FC2C00C8785C /* Routes+Reader.swift in Sources */, - FABB20CB2602FC2C00C8785C /* WPContentSearchHelper.swift in Sources */, FABB20CC2602FC2C00C8785C /* SiteStatsViewModel+AsyncBlock.swift in Sources */, FABB20CD2602FC2C00C8785C /* PostUploadOperation.swift in Sources */, FABB20CE2602FC2C00C8785C /* VerticallyStackedButton.m in Sources */, FABB20CF2602FC2C00C8785C /* PageListViewController.swift in Sources */, + 4A535E152AF3368B008B87B9 /* MenusViewController.swift in Sources */, FABB20D02602FC2C00C8785C /* CoreDataHelper.swift in Sources */, FABB20D12602FC2C00C8785C /* LocalCoreDataService.m in Sources */, FE29EFCE29A91160007CE034 /* WPAdminConvertibleRouter.swift in Sources */, FABB20D22602FC2C00C8785C /* AztecNavigationController.swift in Sources */, FABB20D32602FC2C00C8785C /* Notification+Interface.swift in Sources */, B07F133F2A16C69800AF7FCF /* PlanSelectionViewController.swift in Sources */, + FE6AFE442B18EDF200F76520 /* BloganuaryTracker.swift in Sources */, FABB20D42602FC2C00C8785C /* ReaderSaveForLater+Analytics.swift in Sources */, FABB20D62602FC2C00C8785C /* RevisionOperation.swift in Sources */, FABB20D72602FC2C00C8785C /* ZendeskUtils.swift in Sources */, @@ -23742,15 +23871,16 @@ FABB20E82602FC2C00C8785C /* AppAppearance.swift in Sources */, FABB20E92602FC2C00C8785C /* ReaderTabViewController.swift in Sources */, FE4DC5A8293A84E6008F322F /* MigrationDeepLinkRouter.swift in Sources */, + 80DB57992AF99E0900C728FF /* BlogListConfiguration.swift in Sources */, FA4B203629A786460089FE68 /* BlazeEventsTracker.swift in Sources */, FABB20EA2602FC2C00C8785C /* ActivityTypeSelectorViewController.swift in Sources */, FABB20EB2602FC2C00C8785C /* ActivityActionsParser.swift in Sources */, FABB20ED2602FC2C00C8785C /* Routes+Me.swift in Sources */, + 0C1DB6002B095DA50028F200 /* ImageView.swift in Sources */, 012041042AAAFE3A00E7C707 /* WidgetCenter+JetpackWidgets.swift in Sources */, FABB20EE2602FC2C00C8785C /* StockPhotosResultsPage.swift in Sources */, FABB20EF2602FC2C00C8785C /* QuickStartTourGuide.swift in Sources */, FABB20F02602FC2C00C8785C /* ReaderDetailToolbar.swift in Sources */, - 80A2154429D1177A002FE8EB /* RemoteConfigDebugViewController.swift in Sources */, FAD1263D2A0CF2F50004E24C /* String+NonbreakingSpace.swift in Sources */, FABB20F12602FC2C00C8785C /* RecentSitesService.swift in Sources */, FA332AD129C1F97A00182FBB /* MovedToJetpackViewController.swift in Sources */, @@ -23774,7 +23904,6 @@ FABB20FD2602FC2C00C8785C /* ShowRevisionsListManger.swift in Sources */, FABB21002602FC2C00C8785C /* WordPress-91-92.xcmappingmodel in Sources */, FABB21012602FC2C00C8785C /* ReaderCardDiscoverAttributionView.swift in Sources */, - 088D58A629E724F300E6C0F4 /* ColorGallery.swift in Sources */, FABB21022602FC2C00C8785C /* Plugin.swift in Sources */, FABB21032602FC2C00C8785C /* JetpackActivityLogViewController.swift in Sources */, 17870A712816F2A000D1C627 /* StatsLatestPostSummaryInsightsCell.swift in Sources */, @@ -23822,6 +23951,7 @@ 3F5AAC242877791900AEF5DD /* JetpackButton.swift in Sources */, 83E1E55A2A58B5C2000B576F /* JetpackSocialError.swift in Sources */, FABB211C2602FC2C00C8785C /* EncryptedLogTableViewController.swift in Sources */, + 01B7590C2B3ED63B00179AE6 /* DomainDetailsWebViewControllerWrapper.swift in Sources */, FABB211D2602FC2C00C8785C /* ActivityDateFormatting.swift in Sources */, FABB211E2602FC2C00C8785C /* UIView+Subviews.m in Sources */, FABB211F2602FC2C00C8785C /* WordPress-20-21.xcmappingmodel in Sources */, @@ -23846,6 +23976,7 @@ FE50965A2A17A69F00DDD071 /* TwitterDeprecationTableFooterView.swift in Sources */, FABB212C2602FC2C00C8785C /* NotificationsViewController+AppRatings.swift in Sources */, FABB212D2602FC2C00C8785C /* BlogService.m in Sources */, + F4F7B2512AF8EF2C00207282 /* DomainDetailsWebViewController.swift in Sources */, DC9AF76A285DF8A300EA2A0D /* StatsFollowersChartViewModel.swift in Sources */, FABB212E2602FC2C00C8785C /* JetpackSettingsViewController.swift in Sources */, 982DDF95263238A6002B3904 /* LikeUserPreferredBlog+CoreDataClass.swift in Sources */, @@ -23859,6 +23990,8 @@ FABB21332602FC2C00C8785C /* TwoColumnCell.swift in Sources */, C79C308326EA9A2300E88514 /* ReferrerDetailsCell.swift in Sources */, FABB21342602FC2C00C8785C /* PostSettingsViewController.m in Sources */, + FAAEFAE12B1E29F0004AE802 /* SitePickerViewController+SiteActions.swift in Sources */, + FACF66CB2ADD4703008C3E13 /* PostListCell.swift in Sources */, FABB21352602FC2C00C8785C /* MenuItem.m in Sources */, FABB21362602FC2C00C8785C /* WPRichTextFormatter.swift in Sources */, 175F99B62625FDE100F2687E /* FancyAlertViewController+AppIcons.swift in Sources */, @@ -23893,6 +24026,7 @@ 93E63370272C1074009DACF8 /* LoginEpilogueCreateNewSiteCell.swift in Sources */, FABB21522602FC2C00C8785C /* BuildConfiguration.swift in Sources */, FABB21532602FC2C00C8785C /* PaddedLabel.swift in Sources */, + 0CE538CB2B0D6E0000834BA2 /* ExternalMediaDataSource.swift in Sources */, FABB21542602FC2C00C8785C /* PostService.m in Sources */, FABB21552602FC2C00C8785C /* WPTextAttachmentManager.swift in Sources */, FABB21562602FC2C00C8785C /* ReaderSiteInfoSubscriptions.swift in Sources */, @@ -23904,15 +24038,16 @@ FABB215B2602FC2C00C8785C /* WhatIsNewViewController.swift in Sources */, FABB215C2602FC2C00C8785C /* FormattableCommentContent.swift in Sources */, C3E77F89293A4EA10034AE5A /* MigrationLoadWordPressViewModel.swift in Sources */, - 4A2172FF28F688890006F4F1 /* Blog+Media.swift in Sources */, FABB215D2602FC2C00C8785C /* ValueTransformers.swift in Sources */, FABB215E2602FC2C00C8785C /* ABTest.swift in Sources */, FABB215F2602FC2C00C8785C /* ContextManager+ErrorHandling.swift in Sources */, 0C7762242AAFD39700E07A88 /* SiteMediaAddMediaMenuController.swift in Sources */, C383555A288B02B00062E402 /* JetpackBannerWrapperViewController.swift in Sources */, + 0CD9CCA42AD831590044A33C /* PostSearchViewModel.swift in Sources */, FABB21602602FC2C00C8785C /* EmptyActionView.swift in Sources */, FABB21612602FC2C00C8785C /* ReaderShowAttributionAction.swift in Sources */, FABB21622602FC2C00C8785C /* LinkSettingsViewController.swift in Sources */, + F46546292AED89790017E3D1 /* AllDomainsListEmptyView.swift in Sources */, 9856A39E261FC21E008D6354 /* UserProfileUserInfoCell.swift in Sources */, FABB21632602FC2C00C8785C /* FeatureItemCell.swift in Sources */, C3FBF4E928AFEDF8003797DF /* JetpackBrandingAnalyticsHelper.swift in Sources */, @@ -23983,7 +24118,8 @@ FABB21912602FC2C00C8785C /* ReaderTopicToReaderListTopic37to38.swift in Sources */, FABB21922602FC2C00C8785C /* BlogService+JetpackConvenience.swift in Sources */, 8B55F9EE2614D977007D618E /* UnifiedPrologueStatsContentView.swift in Sources */, - FABB21932602FC2C00C8785C /* GutenbergTenorMediaPicker.swift in Sources */, + FABB21932602FC2C00C8785C /* GutenbergExternalMeidaPicker.swift in Sources */, + 0CD9FB7F2AF9C4DB009D9C7A /* UIBarButtonItem+Extensions.swift in Sources */, 3F8B45A029283D6C00730FA4 /* DashboardMigrationSuccessCell.swift in Sources */, 013A8CB72AB83B40004FF5D0 /* DashboardDomainsCardSearchView.swift in Sources */, FA98B61A29A3BF050071AAE8 /* DashboardBlazePromoCardView.swift in Sources */, @@ -24014,6 +24150,7 @@ FABB21A82602FC2C00C8785C /* WPStyleGuide+WebView.m in Sources */, FABB21A92602FC2C00C8785C /* HomeWidgetData.swift in Sources */, FABB21AA2602FC2C00C8785C /* WPStyleGuide+Activity.swift in Sources */, + F4141EE42AE7152F000D2AAE /* AllDomainsListViewController+Strings.swift in Sources */, FABB21AB2602FC2C00C8785C /* AllTimeWidgetStats.swift in Sources */, F1A75B9C2732EF3700784A70 /* AboutScreenTracker.swift in Sources */, FABB21AC2602FC2C00C8785C /* GutenbergFilesAppMediaSource.swift in Sources */, @@ -24034,7 +24171,7 @@ 08A250FD28D9F0E200F50420 /* CommentDetailInfoViewModel.swift in Sources */, FABB21B82602FC2C00C8785C /* SiteSegmentsService.swift in Sources */, FABB21B92602FC2C00C8785C /* BlogListDataSource.swift in Sources */, - FABB21BA2602FC2C00C8785C /* MediaItemTableViewCells.swift in Sources */, + FABB21BA2602FC2C00C8785C /* MediaItemHeaderView.swift in Sources */, F158541A267D3B6000A2E966 /* BloggingRemindersStore.swift in Sources */, FABB21BB2602FC2C00C8785C /* AbstractPost+fixLocalMediaURLs.swift in Sources */, FABB21BC2602FC2C00C8785C /* MediaNoticeNavigationCoordinator.swift in Sources */, @@ -24068,8 +24205,8 @@ FABB21D02602FC2C00C8785C /* SiteStatsPeriodTableViewController.swift in Sources */, FABB21D12602FC2C00C8785C /* TextBundleWrapper.m in Sources */, FABB21D22602FC2C00C8785C /* WPCategoryTree.swift in Sources */, + 0CF0C4242AE98C13006FFAB4 /* AbstractPostHelper.swift in Sources */, 019D69A12A5EBF47003B676D /* WordPressAuthenticatorProtocol.swift in Sources */, - FABB21D32602FC2C00C8785C /* PageListTableViewCell.m in Sources */, FABB21D42602FC2C00C8785C /* PageSettingsViewController.m in Sources */, FA73D7E62798765B00DF24B3 /* SitePickerViewController.swift in Sources */, FABB21D52602FC2C00C8785C /* SiteDesignContentCollectionViewController.swift in Sources */, @@ -24078,7 +24215,6 @@ FABB21D72602FC2C00C8785C /* Routes+Stats.swift in Sources */, 8BF1C81B27BC00AF00F1C203 /* BlogDashboardCardFrameView.swift in Sources */, FABB21D92602FC2C00C8785C /* SearchIdentifierGenerator.swift in Sources */, - C7124E922638905B00929318 /* StarFieldView.swift in Sources */, FABB21DA2602FC2C00C8785C /* StatsTwoColumnRow.swift in Sources */, 982DDF91263238A6002B3904 /* LikeUser+CoreDataClass.swift in Sources */, FABB21DC2602FC2C00C8785C /* ReaderSaveForLaterRemovedPosts.swift in Sources */, @@ -24091,12 +24227,10 @@ FABB21E02602FC2C00C8785C /* SupportTableViewController.swift in Sources */, FABB21E12602FC2C00C8785C /* DetailDataCell.swift in Sources */, 83B1D038282C62620061D911 /* BloggingPromptsAttribution.swift in Sources */, - C7F7AC75261CF1F300CE547F /* JetpackLoginErrorViewController.swift in Sources */, FABB21E22602FC2C00C8785C /* PluginViewModel.swift in Sources */, FABB21E32602FC2C00C8785C /* NSCalendar+Helpers.swift in Sources */, FABB21E42602FC2C00C8785C /* GutenbergCoverUploadProcessor.swift in Sources */, FABB21E52602FC2C00C8785C /* MediaQuotaCell.swift in Sources */, - FABB21E62602FC2C00C8785C /* PartScreenPresentationController.swift in Sources */, 086F2484284F52E100032F39 /* FeatureHighlightStore.swift in Sources */, FABB21E72602FC2C00C8785C /* WPRichTextImage.swift in Sources */, 17870A75281FBEC100D1C627 /* StatsTotalInsightsCell.swift in Sources */, @@ -24115,7 +24249,6 @@ FABB21EF2602FC2C00C8785C /* KeyboardDismissHelper.swift in Sources */, FABB21F02602FC2C00C8785C /* FancyAlertViewController+CreateButtonAnnouncement.swift in Sources */, FABB21F12602FC2C00C8785C /* GutenbergSettings.swift in Sources */, - FABB21F22602FC2C00C8785C /* MediaPickingContext.swift in Sources */, C7234A432832C2BA0045C63F /* QRLoginScanningViewController.swift in Sources */, 176BA53C268266E70025E4A3 /* BlogService+Reminders.swift in Sources */, FE25C236271F23000084E1DB /* ReaderCommentsNotificationSheetViewController.swift in Sources */, @@ -24124,7 +24257,6 @@ FABB21F52602FC2C00C8785C /* PostNoticeNavigationCoordinator.swift in Sources */, FABB21F62602FC2C00C8785C /* SearchableActivityConvertable.swift in Sources */, FABB21F72602FC2C00C8785C /* GridCell.swift in Sources */, - C72A4F68264088E4009CA633 /* JetpackNotFoundErrorViewModel.swift in Sources */, FE1E201B2A473E0800CE7C90 /* JetpackSocialService.swift in Sources */, FABB21F82602FC2C00C8785C /* AdaptiveNavigationController.swift in Sources */, FABB21F92602FC2C00C8785C /* RemotePostCategory+Extensions.swift in Sources */, @@ -24136,19 +24268,17 @@ 3F720C222889B65B00519938 /* JetpackBrandingVisibility.swift in Sources */, 0C35FFF729CBB5DE00D224EB /* BlogDashboardEmptyStateCell.swift in Sources */, FABB22012602FC2C00C8785C /* RevisionDiff.swift in Sources */, - FABB22022602FC2C00C8785C /* MediaThumbnailCoordinator.swift in Sources */, FABB22032602FC2C00C8785C /* RevisionDiffsPageManager.swift in Sources */, FABB22042602FC2C00C8785C /* CalendarCollectionView.swift in Sources */, 3F946C5A2684DD8E00B946F6 /* BloggingRemindersActions.swift in Sources */, F48D44BC2989AA8A0051EAA6 /* ReaderSiteService.m in Sources */, C387B7A22638D66F00BDEF86 /* PostAuthorSelectorViewController.swift in Sources */, - 171096CC270F01EA001BCDD6 /* DomainSuggestionsTableViewController.swift in Sources */, 3FFDEF7F29177FB100B625CE /* MigrationStepConfiguration.swift in Sources */, FABB22052602FC2C00C8785C /* SubjectContentGroup.swift in Sources */, FABB22062602FC2C00C8785C /* PluginListRow.swift in Sources */, FABB22072602FC2C00C8785C /* BlogSettings+DateAndTimeFormat.swift in Sources */, - FABB22082602FC2C00C8785C /* PostSearchHeader.swift in Sources */, 018635882A8109F900915532 /* SupportChatBotViewModel.swift in Sources */, + FAEC116F2AEBEEA600F9DA54 /* AbstractPostMenuViewModel.swift in Sources */, FABB22092602FC2C00C8785C /* ReaderPostCardContentLabel.swift in Sources */, FA90EFF0262E74210055AB22 /* JetpackWebViewControllerFactory.swift in Sources */, 80A2153E29C35197002FE8EB /* StaticScreensTabBarWrapper.swift in Sources */, @@ -24164,14 +24294,11 @@ FABB22142602FC2C00C8785C /* TitleBadgeDisclosureCell.swift in Sources */, FABB22152602FC2C00C8785C /* FormattableContent.swift in Sources */, 4A358DEA29B5F14C00BFCEBE /* SharingButton+Lookup.swift in Sources */, - FABB22162602FC2C00C8785C /* WebAddressWizardContent.swift in Sources */, C7AFF87D283D5CF4000E01DF /* QRLoginVerifyCoordinator.swift in Sources */, 3F8D988A26153484003619E5 /* UnifiedPrologueBackgroundView.swift in Sources */, FABB22182602FC2C00C8785C /* SiteSegmentsStep.swift in Sources */, FABB22192602FC2C00C8785C /* ReaderInterestsCollectionViewFlowLayout.swift in Sources */, - FABB221A2602FC2C00C8785C /* PHAsset+Metadata.swift in Sources */, FABB221B2602FC2C00C8785C /* Theme.m in Sources */, - FABB221C2602FC2C00C8785C /* StockPhotosMediaGroup.swift in Sources */, FABB221D2602FC2C00C8785C /* DefaultFormattableContentAction.swift in Sources */, 80D9CFF529DD314600FE3400 /* DashboardPagesListCardCell.swift in Sources */, FABB221E2602FC2C00C8785C /* PostEditorNavigationBarManager.swift in Sources */, @@ -24183,14 +24310,17 @@ FA20751527A86B73001A644D /* UIScrollView+Helpers.swift in Sources */, FABB22232602FC2C00C8785C /* PluginDirectoryCollectionViewCell.swift in Sources */, FABB22242602FC2C00C8785C /* GutenbergNetworking.swift in Sources */, + 0CA10F6E2ADAE86E00CE75AC /* PostSearchSuggestionsService.swift in Sources */, FABB22252602FC2C00C8785C /* NavigationActionHelpers.swift in Sources */, C9F1D4BB2706EEEB00BDF917 /* HomepageEditorNavigationBarManager.swift in Sources */, FABB22262602FC2C00C8785C /* String+Ranges.swift in Sources */, FABB22272602FC2C00C8785C /* ActivityCommentRange.swift in Sources */, FABB22282602FC2C00C8785C /* GutenbergAudioUploadProcessor.swift in Sources */, FABB22292602FC2C00C8785C /* AsyncOperation.swift in Sources */, + 0C0AD10B2B0CCFA400EC06E6 /* MediaPreviewController.swift in Sources */, FABB222A2602FC2C00C8785C /* JetpackScanThreatSectionGrouping.swift in Sources */, 0839F88D2993C1B600415038 /* JetpackPluginOverlayCoordinator.swift in Sources */, + F413F7892B2B253A00A64A94 /* DashboardCard+Personalization.swift in Sources */, FABB222B2602FC2C00C8785C /* MediaHelper.swift in Sources */, FABB222C2602FC2C00C8785C /* ReaderActionHelpers.swift in Sources */, FABB222D2602FC2C00C8785C /* NoteBlockUserTableViewCell.swift in Sources */, @@ -24199,6 +24329,7 @@ 8B074A5127AC3A64003A2EB8 /* BlogDashboardViewModel.swift in Sources */, FEAC916F28001FC4005026E7 /* AvatarTrainView.swift in Sources */, FA4BC0D12996A589005EB077 /* BlazeService.swift in Sources */, + 01B759092B3ECAF300179AE6 /* DomainsStateView.swift in Sources */, FABB22302602FC2C00C8785C /* Double+Stats.swift in Sources */, FABB22312602FC2C00C8785C /* CircularProgressView.swift in Sources */, FABB22322602FC2C00C8785C /* StatsNoDataRow.swift in Sources */, @@ -24213,12 +24344,13 @@ 08E6E07F2A4C405500B807B0 /* CompliancePopoverViewModel.swift in Sources */, FAA9084D27BD60710093FFA8 /* MySiteViewController+QuickStart.swift in Sources */, FABB223C2602FC2C00C8785C /* EditCommentViewController.m in Sources */, - FABB223D2602FC2C00C8785C /* ThisWeekWidgetStats.swift in Sources */, FABB223E2602FC2C00C8785C /* PostAutoUploadMessageProvider.swift in Sources */, FABB223F2602FC2C00C8785C /* GutenbergMediaPickerHelper.swift in Sources */, + 0CB424EF2ADEE3CD0080B807 /* PostSearchTokenTableCell.swift in Sources */, FABB22402602FC2C00C8785C /* FeatureFlag.swift in Sources */, 3F170E252655917400F6F670 /* UIView+SwiftUI.swift in Sources */, 80EF929128105CFA0064A971 /* QuickStartFactory.swift in Sources */, + 0C308FFF2B1234E70071C551 /* SiteMediaFilterButtonView.swift in Sources */, FABB22422602FC2C00C8785C /* BlogDetailsViewController+Activity.swift in Sources */, FABB22432602FC2C00C8785C /* NoteBlockTableViewCell.swift in Sources */, FABB22442602FC2C00C8785C /* WPTabBarController+QuickStart.swift in Sources */, @@ -24243,6 +24375,7 @@ FABB22522602FC2C00C8785C /* ActivityTableViewCell.swift in Sources */, F49D7BEC29DF329500CB93A5 /* UIPopoverPresentationController+PopoverAnchor.swift in Sources */, FABB22532602FC2C00C8785C /* BlogDetailsViewController+FancyAlerts.swift in Sources */, + F41D98E52B39CAAA004EC050 /* BlogDashboardDynamicCardCoordinator.swift in Sources */, FABB22542602FC2C00C8785C /* StoriesIntroDataSource.swift in Sources */, FABB22552602FC2C00C8785C /* StoreContainer.swift in Sources */, FABB22562602FC2C00C8785C /* AbstractPost+HashHelpers.m in Sources */, @@ -24269,6 +24402,7 @@ FABB22632602FC2C00C8785C /* UIFont+Weight.swift in Sources */, FABB22642602FC2C00C8785C /* PostChart.swift in Sources */, FE4DC5AA293A84F8008F322F /* WordPressExportRoute.swift in Sources */, + 0C1DB60E2B0BDA740028F200 /* TenorWelcomeView.swift in Sources */, FABB22652602FC2C00C8785C /* PageCoordinator.swift in Sources */, FABB22662602FC2C00C8785C /* Blog+QuickStart.swift in Sources */, FABB22672602FC2C00C8785C /* CommentsViewController+Filters.swift in Sources */, @@ -24295,6 +24429,7 @@ 0C0D3B0E2A4C79DE0050A00D /* BlazeCampaignsStream.swift in Sources */, FABB22792602FC2C00C8785C /* WPCrashLoggingProvider.swift in Sources */, FA332AD529C1FC7A00182FBB /* MovedToJetpackViewModel.swift in Sources */, + 0CA10F742ADB014C00CE75AC /* StringRankedSearch.swift in Sources */, FABB227A2602FC2C00C8785C /* StockPhotosMedia.swift in Sources */, FABB227B2602FC2C00C8785C /* FancyAlertViewController+SavedPosts.swift in Sources */, FABB227C2602FC2C00C8785C /* WPRichTextMediaAttachment.swift in Sources */, @@ -24309,17 +24444,15 @@ FABB22822602FC2C00C8785C /* WPTableViewActivityCell.m in Sources */, FABB22842602FC2C00C8785C /* PeopleViewController.swift in Sources */, FABB22852602FC2C00C8785C /* ConfettiView.swift in Sources */, - FABB22862602FC2C00C8785C /* RegisterDomainSuggestionsViewController.swift in Sources */, FABB22872602FC2C00C8785C /* AbstractPost+MarkAsFailedAndDraftIfNeeded.swift in Sources */, FABB22882602FC2C00C8785C /* UserSuggestion+CoreDataProperties.swift in Sources */, C743535727BD7144008C2644 /* AnimatedGifAttachmentViewProvider.swift in Sources */, FABB228A2602FC2C00C8785C /* ReaderHeaderAction.swift in Sources */, - C72A4F8E26408C73009CA633 /* JetpackNoSitesErrorViewModel.swift in Sources */, FABB228B2602FC2C00C8785C /* FancyAlerts+VerificationPrompt.swift in Sources */, FABB228C2602FC2C00C8785C /* ReaderSiteStreamHeader.swift in Sources */, FABB228D2602FC2C00C8785C /* VideoUploadProcessor.swift in Sources */, + FE34ACD02B1661EB00108B3C /* DashboardBloganuaryCardCell.swift in Sources */, FABB228E2602FC2C00C8785C /* PeopleCellViewModel.swift in Sources */, - FABB228F2602FC2C00C8785C /* TableViewOffsetCoordinator.swift in Sources */, FABB22902602FC2C00C8785C /* RegisterDomainDetailsViewController.swift in Sources */, 1770BD0E267A368100D5F8C0 /* BloggingRemindersPushPromptViewController.swift in Sources */, C7BB601A2863AF9700748FD9 /* QRLoginProtocols.swift in Sources */, @@ -24344,14 +24477,12 @@ FABB229D2602FC2C00C8785C /* ImgUploadProcessor.swift in Sources */, FABB229E2602FC2C00C8785C /* RegisterDomainDetailsViewModel.swift in Sources */, FABB229F2602FC2C00C8785C /* JetpackScanHistoryViewController.swift in Sources */, - FABB22A02602FC2C00C8785C /* GutenbergStockPhotos.swift in Sources */, FABB22A12602FC2C00C8785C /* MediaHost+Blog.swift in Sources */, FABB22A22602FC2C00C8785C /* PlanListRow.swift in Sources */, FABB22A32602FC2C00C8785C /* SiteCreationWizard.swift in Sources */, FA4B203029A619130089FE68 /* BlazeFlowCoordinator.swift in Sources */, C7AFF875283C0ADC000E01DF /* UIApplication+Helpers.swift in Sources */, FABB22A52602FC2C00C8785C /* SignupEpilogueViewController.swift in Sources */, - FABB22A62602FC2C00C8785C /* TenorPicker.swift in Sources */, FAB985C22697550C00B172A3 /* NoResultsViewController+StatsModule.swift in Sources */, FABB22A72602FC2C00C8785C /* BlogListViewController+Activity.swift in Sources */, 3F810A5B2616870C00ADDCC2 /* UnifiedPrologueIntroContentView.swift in Sources */, @@ -24367,7 +24498,6 @@ FEA7948E26DD136700CEC520 /* CommentHeaderTableViewCell.swift in Sources */, FABB22B22602FC2C00C8785C /* StatsDataHelper.swift in Sources */, FABB22B32602FC2C00C8785C /* PrepublishingViewController.swift in Sources */, - FABB22B42602FC2C00C8785C /* PageListTableViewHandler.swift in Sources */, FABB22B52602FC2C00C8785C /* SearchableItemConvertable.swift in Sources */, 4A9948E5297624EF006282A9 /* Blog+Creation.swift in Sources */, 8BBBCE712717651200B277AC /* JetpackModuleHelper.swift in Sources */, @@ -24376,7 +24506,6 @@ 80C523A82995D73C00B1C14B /* BlazeCreateCampaignWebViewModel.swift in Sources */, 982DDF97263238A6002B3904 /* LikeUserPreferredBlog+CoreDataProperties.swift in Sources */, FABB22B82602FC2C00C8785C /* QuickStartChecklistManager.swift in Sources */, - FABB22B92602FC2C00C8785C /* NoResultsTenorConfiguration.swift in Sources */, C3234F4F27EB96AB004ADB29 /* IntentCell.swift in Sources */, 98AA6D1226B8CE7200920C8B /* Comment+CoreDataClass.swift in Sources */, FABB22BA2602FC2C00C8785C /* JetpackRemoteInstallViewController.swift in Sources */, @@ -24391,6 +24520,7 @@ 0CDEC40D2A2FAF0500BB3A91 /* DashboardBlazeCampaignsCardView.swift in Sources */, 4A1E77CD2989F2F7006281CC /* WPAccount+DeduplicateBlogs.swift in Sources */, FA98B61D29A3DB840071AAE8 /* BlazeHelper.swift in Sources */, + 0830538D2B2732E400B889FE /* DynamicDashboardCard.swift in Sources */, FABB22C32602FC2C00C8785C /* ReaderTagsFooter.swift in Sources */, 4A2C73F52A95856000ACE79E /* PostRepository.swift in Sources */, 98DCF4A6275945E00008630F /* ReaderDetailNoCommentCell.swift in Sources */, @@ -24408,6 +24538,7 @@ FABB22CB2602FC2C00C8785C /* FilterBarView.swift in Sources */, 98B88453261E4E09007ED7F8 /* LikeUserTableViewCell.swift in Sources */, FABB22CC2602FC2C00C8785C /* BlogToBlogMigration_61_62.swift in Sources */, + 01ABF1712AD578B3004331BD /* WidgetAnalytics.swift in Sources */, FABB22CD2602FC2C00C8785C /* WebKitViewController.swift in Sources */, FABB22CE2602FC2C00C8785C /* AnnouncementsDataSource.swift in Sources */, FABB22CF2602FC2C00C8785C /* AssembledSiteView.swift in Sources */, @@ -24435,12 +24566,12 @@ E6D6A1312683ABE6004C24A7 /* ReaderSubscribeCommentsAction.swift in Sources */, F49B9A06293A21BF000CEFCE /* MigrationAnalyticsTracker.swift in Sources */, E62CE58F26B1D14200C9D147 /* AccountService+Cookies.swift in Sources */, - FABB22DE2602FC2C00C8785C /* KeyboardInfo.swift in Sources */, 0C391E622A3002950040EA91 /* BlazeCampaignStatusView.swift in Sources */, FABB22DF2602FC2C00C8785C /* PlanDetailViewModel.swift in Sources */, FABB22E02602FC2C00C8785C /* DebugMenuViewController.swift in Sources */, 9801E683274EEBC4002FDDB6 /* ReaderDetailCommentsHeader.swift in Sources */, FABB22E12602FC2C00C8785C /* MenuItemCheckButtonView.m in Sources */, + 80348F312AF87FEA0045CCD3 /* AllDomainsListViewController.swift in Sources */, FABB22E22602FC2C00C8785C /* MenuItemLinkViewController.m in Sources */, FABB22E32602FC2C00C8785C /* PreviewWebKitViewController.swift in Sources */, FA85F5F62A9DFDCC001D8425 /* NoSitesViewModel.swift in Sources */, @@ -24456,8 +24587,8 @@ F49B99FF2937C9B4000CEFCE /* MigrationEmailService.swift in Sources */, FABB22E92602FC2C00C8785C /* GutenbergVideoUploadProcessor.swift in Sources */, FABB22EA2602FC2C00C8785C /* PostCategory.m in Sources */, + F4141EE82AE72DC4000D2AAE /* AllDomainsListTableViewCell.swift in Sources */, 3F685B6A26D431FA001C6808 /* DomainSuggestionViewControllerWrapper.swift in Sources */, - FABB22EB2602FC2C00C8785C /* MediaAssetExporter.swift in Sources */, FABB22EC2602FC2C00C8785C /* SharingButtonsViewController.swift in Sources */, F4F9D5F2290993D400502576 /* MigrationWelcomeViewModel.swift in Sources */, FABB22ED2602FC2C00C8785C /* EditorFactory.swift in Sources */, @@ -24470,9 +24601,8 @@ FABB22F22602FC2C00C8785C /* Route.swift in Sources */, FABB22F32602FC2C00C8785C /* RegisterDomainDetailsViewController+Cells.swift in Sources */, 4A526BE0296BE9A50007B5BA /* CoreDataService.m in Sources */, - 3F46EEC728BC4935004F02B2 /* JetpackPrompt.swift in Sources */, - 175CC1712720548700622FB4 /* DomainExpiryDateFormatter.swift in Sources */, FABB22F42602FC2C00C8785C /* ReaderStreamViewController+Sharing.swift in Sources */, + B0DE91B62AF9778200D51A02 /* DomainSetupNoticeView.swift in Sources */, FABB22F52602FC2C00C8785C /* FilterTabBar.swift in Sources */, FABB22F72602FC2C00C8785C /* EditComment.swift in Sources */, FABB22F82602FC2C00C8785C /* UnknownEditorViewController.swift in Sources */, @@ -24520,8 +24650,9 @@ FABB23162602FC2C00C8785C /* WPWebViewController.m in Sources */, FABB23172602FC2C00C8785C /* ShadowView.swift in Sources */, FABB23182602FC2C00C8785C /* Wizard.swift in Sources */, + 0CD9FB8C2AFADAFE009D9C7A /* SiteMediaPageViewController.swift in Sources */, 98E14A3D27C9712D007B0896 /* NotificationCommentDetailViewController.swift in Sources */, - FABB23192602FC2C00C8785C /* Media+WPMediaAsset.m in Sources */, + FABB23192602FC2C00C8785C /* Media+Extensions.m in Sources */, FABB231A2602FC2C00C8785C /* GutenbergImgUploadProcessor.swift in Sources */, FABB231B2602FC2C00C8785C /* PluginListViewModel.swift in Sources */, FABB231C2602FC2C00C8785C /* WPStyleGuide+People.swift in Sources */, @@ -24538,6 +24669,7 @@ FABB23232602FC2C00C8785C /* Delay.swift in Sources */, FABB23242602FC2C00C8785C /* Pageable.swift in Sources */, FABB23252602FC2C00C8785C /* AppIconViewController.swift in Sources */, + F413F77B2B2A183E00A64A94 /* BlogDashboardDynamicCardCell.swift in Sources */, FABB23262602FC2C00C8785C /* ThemeBrowserCell.swift in Sources */, FA8E2FE627C6AE4500DA0982 /* QuickStartChecklistView.swift in Sources */, FABB23272602FC2C00C8785C /* ImageCropViewController.swift in Sources */, @@ -24545,7 +24677,6 @@ FABB23292602FC2C00C8785C /* SiteStatsTableViewCells.swift in Sources */, FABB232A2602FC2C00C8785C /* SharingButton.swift in Sources */, 0CED95612A460F4B0020F420 /* DebugFeatureFlagsView.swift in Sources */, - FABB232B2602FC2C00C8785C /* PostActionSheet.swift in Sources */, 4A2C73E22A943D9000ACE79E /* TaggedManagedObjectID.swift in Sources */, FABB232C2602FC2C00C8785C /* PublicizeConnection.swift in Sources */, FABB232D2602FC2C00C8785C /* TenorPageable.swift in Sources */, @@ -24554,6 +24685,7 @@ FABB232F2602FC2C00C8785C /* URL+Helpers.swift in Sources */, FABB23302602FC2C00C8785C /* WPStyleGuide+Aztec.swift in Sources */, FABB23312602FC2C00C8785C /* SettingsCommon.swift in Sources */, + F4141EEA2AE74ADA000D2AAE /* AllDomainsListActivityIndicatorTableViewCell.swift in Sources */, 17C1D6922670E4A2006C8970 /* UIFont+Fitting.swift in Sources */, FABB23322602FC2C00C8785C /* FormatBarItemProviders.swift in Sources */, 082A645C291C2DD700668D2C /* Routes+Jetpack.swift in Sources */, @@ -24565,6 +24697,7 @@ 3FFDEF7829177D7500B625CE /* MigrationNotificationsViewModel.swift in Sources */, FABB23372602FC2C00C8785C /* WebNavigationDelegate.swift in Sources */, FAFC065227D27241002F0483 /* BlogDetailsViewController+Dashboard.swift in Sources */, + 0C700B8A2AE1E1940085C2EE /* PageListItemViewModel.swift in Sources */, 8B55FAAD2614FC87007D618E /* Text+BoldSubString.swift in Sources */, FABB23382602FC2C00C8785C /* WordPress-22-23.xcmappingmodel in Sources */, FED65D79293511E4008071BF /* SharedDataIssueSolver.swift in Sources */, @@ -24582,7 +24715,6 @@ 83BFAE492A6EBF1F00C7B683 /* DashboardJetpackSocialCardCell.swift in Sources */, FABB23412602FC2C00C8785C /* SeparatorsView.swift in Sources */, FABB23422602FC2C00C8785C /* JetpackScanCoordinator.swift in Sources */, - FABB23432602FC2C00C8785C /* WPStyleGuide+ReadableMargins.m in Sources */, FABB23442602FC2C00C8785C /* FormattableContentStyles.swift in Sources */, FABB23452602FC2C00C8785C /* StatsBarChartConfiguration.swift in Sources */, FABB23462602FC2C00C8785C /* LoadingStatusView.swift in Sources */, @@ -24601,9 +24733,9 @@ FABB234E2602FC2C00C8785C /* ReaderMenuAction.swift in Sources */, FABB234F2602FC2C00C8785C /* PostSharingController.swift in Sources */, FABB23502602FC2C00C8785C /* LongPressGestureLabel.swift in Sources */, - FABB23512602FC2C00C8785C /* NoResultsStockPhotosConfiguration.swift in Sources */, FABB23532602FC2C00C8785C /* UIImage+Export.swift in Sources */, FABB23542602FC2C00C8785C /* NotificationName+Names.swift in Sources */, + FAD3DE822AE2965A00A3B031 /* AbstractPostMenuHelper.swift in Sources */, FABB23552602FC2C00C8785C /* NSFetchedResultsController+Helpers.swift in Sources */, FABB23562602FC2C00C8785C /* BlogSettings+Discussion.swift in Sources */, 0C8FC9A22A8BC8630059DCE4 /* PHPickerController+Extensions.swift in Sources */, @@ -24620,7 +24752,6 @@ FABB23612602FC2C00C8785C /* ReaderTabItemsStore.swift in Sources */, FABB23622602FC2C00C8785C /* NoResultsViewController+Model.swift in Sources */, 3F435220289B2B2B00CE19ED /* JetpackBrandingCoordinator.swift in Sources */, - FABB23632602FC2C00C8785C /* PostListTableViewHandler.swift in Sources */, FABB23642602FC2C00C8785C /* UIAlertController+Helpers.swift in Sources */, 3F4D035128A56F9B00F0A4FD /* CircularImageButton.swift in Sources */, FABB23652602FC2C00C8785C /* RevisionDiff+CoreData.swift in Sources */, @@ -24630,6 +24761,7 @@ FABB23672602FC2C00C8785C /* NotificationAction.swift in Sources */, 3F39C93627A09927001EC300 /* WordPressLibraryLogger.swift in Sources */, FABB23682602FC2C00C8785C /* NSManagedObject.swift in Sources */, + 0CB54F582AEC320700582080 /* WordPressAppDelegate+PostCoordinatorDelegate.swift in Sources */, FABB23692602FC2C00C8785C /* ExportableAsset.swift in Sources */, FABB236A2602FC2C00C8785C /* PlanListViewModel.swift in Sources */, FABB236B2602FC2C00C8785C /* InsightsManagementViewController.swift in Sources */, @@ -24637,7 +24769,7 @@ FABB236D2602FC2C00C8785C /* ReaderDetailCoordinator.swift in Sources */, 9887560D2810BA7A00AD7589 /* BloggingPromptsIntroductionPresenter.swift in Sources */, FABB236E2602FC2C00C8785C /* AccountService.m in Sources */, - FABB236F2602FC2C00C8785C /* MediaLibraryPickerDataSource.m in Sources */, + 0C3090232B12A5C90071C551 /* UIButton+Extensions.swift in Sources */, FABB23702602FC2C00C8785C /* PostStatsViewModel.swift in Sources */, FABB23712602FC2C00C8785C /* StatsStore+Cache.swift in Sources */, FABB23722602FC2C00C8785C /* WP3DTouchShortcutHandler.swift in Sources */, @@ -24648,7 +24780,6 @@ FABB23762602FC2C00C8785C /* ActivityContentGroup.swift in Sources */, C9F1D4B82706ED7C00BDF917 /* EditHomepageViewController.swift in Sources */, FABB23772602FC2C00C8785C /* MediaImageExporter.swift in Sources */, - FABB23782602FC2C00C8785C /* GutenbergViewController+InformativeDialog.swift in Sources */, 3FFDEF8A2918597700B625CE /* MigrationDoneViewController.swift in Sources */, 8B55F9DC2614D902007D618E /* CircledIcon.swift in Sources */, FABB23792602FC2C00C8785C /* ChangePasswordViewController.swift in Sources */, @@ -24668,11 +24799,11 @@ F4D829622930E9F300038726 /* MigrationDeleteWordPressViewController.swift in Sources */, 4AA33F05299A1F93005B6E23 /* ReaderTagTopic+Lookup.swift in Sources */, FABB23822602FC2C00C8785C /* FooterContentGroup.swift in Sources */, - FABB23832602FC2C00C8785C /* BasePageListCell.m in Sources */, F163541726DE2ECE008B625B /* NotificationEventTracker.swift in Sources */, 0A9610FA28B2E56300076EBA /* UserSuggestion+Comparable.swift in Sources */, 80C740FC2989FC4600199027 /* PostStatsTableViewController+JetpackBannerViewController.swift in Sources */, 83914BD52A2EA03A0017A588 /* PostSettingsViewController+JetpackSocial.swift in Sources */, + F46546312AF2F8D30017E3D1 /* DomainsStateViewModel.swift in Sources */, FABB23852602FC2C00C8785C /* WPError.m in Sources */, FABB23862602FC2C00C8785C /* ContentRouter.swift in Sources */, FABB23872602FC2C00C8785C /* BlogToBlog32to33.swift in Sources */, @@ -24688,7 +24819,6 @@ 8091019429078CFE00FCB4EA /* JetpackFullscreenOverlayViewController.swift in Sources */, FA347AEE26EB6E300096604B /* GrowAudienceCell.swift in Sources */, FABB23922602FC2C00C8785C /* UIImage+Assets.swift in Sources */, - FABB23932602FC2C00C8785C /* LoadMoreCounter.swift in Sources */, FABB23942602FC2C00C8785C /* LoginEpilogueUserInfo.swift in Sources */, 086F2483284F52DF00032F39 /* Tooltip.swift in Sources */, 8313B9FB2995A03C000AF26E /* JetpackRemoteInstallCardView.swift in Sources */, @@ -24705,6 +24835,7 @@ FABB23A02602FC2C00C8785C /* PluginDirectoryViewController.swift in Sources */, FABB23A12602FC2C00C8785C /* RoleService.swift in Sources */, FABB23A22602FC2C00C8785C /* AccountHelper.swift in Sources */, + 0C1531FF2AE17140003CDE13 /* PostSearchViewModel+Highlighter.swift in Sources */, FABB23A32602FC2C00C8785C /* Sites.intentdefinition in Sources */, FABB23A42602FC2C00C8785C /* MenuItemSourceTextBar.m in Sources */, FABB23A52602FC2C00C8785C /* PluginDirectoryViewModel.swift in Sources */, @@ -24729,11 +24860,11 @@ FABB23B22602FC2C00C8785C /* StatsChildRowsView.swift in Sources */, FABB23B32602FC2C00C8785C /* Menu.m in Sources */, FABB23B42602FC2C00C8785C /* BlogListViewController.m in Sources */, + 01B5C3C82AE7FC61007055BB /* UITestConfigurator.swift in Sources */, 837A53D82AD8C3A400B941A2 /* ReaderFollowButton.swift in Sources */, FABB23B52602FC2C00C8785C /* JetpackScanStatusCell.swift in Sources */, FABB23B62602FC2C00C8785C /* NoteBlockCommentTableViewCell.swift in Sources */, 801D951B291AC0B00051993E /* OverlayFrequencyTracker.swift in Sources */, - C7F7ACBE261E4F0600CE547F /* JetpackErrorViewModel.swift in Sources */, FABB23B72602FC2C00C8785C /* Notifiable.swift in Sources */, 80EF672627F3D63B0063B138 /* DashboardStatsStackView.swift in Sources */, FABB23B82602FC2C00C8785C /* RegisterDomainDetailsViewModel+CountryDialCodes.swift in Sources */, @@ -24771,6 +24902,7 @@ FABB23D22602FC2C00C8785C /* PostServiceOptions.m in Sources */, FABB23D32602FC2C00C8785C /* DiffTitleValue.swift in Sources */, FABB23D42602FC2C00C8785C /* ReaderCommentsViewController.swift in Sources */, + 0CB424F22ADEE52A0080B807 /* PostSearchToken.swift in Sources */, FABB23D52602FC2C00C8785C /* SiteCreationRequest+Validation.swift in Sources */, FABB23D62602FC2C00C8785C /* FormattableContentGroup.swift in Sources */, FABB23D72602FC2C00C8785C /* MenuItemSourceViewController.m in Sources */, @@ -24785,8 +24917,8 @@ FABB23DF2602FC2C00C8785C /* BlogSelectorViewController.m in Sources */, FABB23E02602FC2C00C8785C /* HomepageSettingsViewController.swift in Sources */, 93E6336D272AF504009DACF8 /* LoginEpilogueDividerView.swift in Sources */, - FABB23E12602FC2C00C8785C /* TenorMediaGroup.swift in Sources */, FABB23E22602FC2C00C8785C /* SiteSuggestionService.swift in Sources */, + 80348F342AF89BD00045CCD3 /* AllDomainsAddDomainCoordinator.swift in Sources */, FABB23E32602FC2C00C8785C /* NotificationsViewController+PushPrimer.swift in Sources */, FABB23E42602FC2C00C8785C /* ReaderPostService.m in Sources */, FABB23E52602FC2C00C8785C /* EditorMediaUtility.swift in Sources */, @@ -24805,17 +24937,16 @@ FABB23F22602FC2C00C8785C /* Animator.swift in Sources */, 03216ECD27995F3500D444CA /* SchedulingViewControllerPresenter.swift in Sources */, FABB23F32602FC2C00C8785C /* SiteStatsDashboardViewController.swift in Sources */, + F4141EEC2AE945C7000D2AAE /* AllDomainsListItemViewModel.swift in Sources */, 9895401226C1F39300EDEB5A /* EditCommentTableViewController.swift in Sources */, FABB23F42602FC2C00C8785C /* MediaSettings.swift in Sources */, FABB23F52602FC2C00C8785C /* DomainCreditRedemptionSuccessViewController.swift in Sources */, FABB23F62602FC2C00C8785C /* ActivityLogDetailViewController.m in Sources */, FABB23F72602FC2C00C8785C /* NSObject+Helpers.m in Sources */, FABB23F82602FC2C00C8785C /* ImmuTableViewController.swift in Sources */, - FABB23F92602FC2C00C8785C /* NullStockPhotosService.swift in Sources */, FABB23FA2602FC2C00C8785C /* RevisionPreviewViewController.swift in Sources */, 80EF928B280D28140064A971 /* Atomic.swift in Sources */, 3FFDEF9129187F2100B625CE /* MigrationActionsConfiguration.swift in Sources */, - FABB23FC2602FC2C00C8785C /* SitePromptView.swift in Sources */, 0C896DE32A3A7BD700D7D4E7 /* SettingsPicker.swift in Sources */, 8B4EDADE27DF9D5E004073B6 /* Blog+MySite.swift in Sources */, FABB23FD2602FC2C00C8785C /* BaseRestoreCompleteViewController.swift in Sources */, @@ -24827,9 +24958,10 @@ FABB24032602FC2C00C8785C /* ReaderCSS.swift in Sources */, FABB24042602FC2C00C8785C /* SafariActivity.m in Sources */, FABB24052602FC2C00C8785C /* WordPress-37-38.xcmappingmodel in Sources */, - 0C01A6EB2AB37F0F009F7145 /* SiteMediaCollectionCellBadgeView.swift in Sources */, + 0C01A6EB2AB37F0F009F7145 /* SiteMediaCollectionCellSelectionOverlayView.swift in Sources */, FABB24062602FC2C00C8785C /* UploadOperation.swift in Sources */, FABB24072602FC2C00C8785C /* LayoutPickerAnalyticsEvent.swift in Sources */, + F465462D2AEF22070017E3D1 /* AllDomainsListViewModel+Strings.swift in Sources */, FABB24082602FC2C00C8785C /* ActivityRange.swift in Sources */, FABB24092602FC2C00C8785C /* DiffAbstractValue+Attributes.swift in Sources */, FABB240A2602FC2C00C8785C /* Environment.swift in Sources */, @@ -24846,6 +24978,7 @@ FABB24122602FC2C00C8785C /* NoteBlockButtonTableViewCell.swift in Sources */, 084FC3BD299155CA00A17BCF /* JetpackOverlayCoordinator.swift in Sources */, C7F7ABD6261CED7A00CE547F /* JetpackAuthenticationManager.swift in Sources */, + 0C1DB6092B0A419B0028F200 /* ImageDecoder.swift in Sources */, FABB24142602FC2C00C8785C /* ThemeBrowserSectionHeaderView.swift in Sources */, 0C0453292AC73343003079C8 /* SiteMediaVideoDurationView.swift in Sources */, FABB24152602FC2C00C8785C /* SiteIconPickerPresenter.swift in Sources */, @@ -24878,19 +25011,21 @@ FABB242B2602FC2C00C8785C /* ShareExtensionService.swift in Sources */, F4F9D5F42909B7C100502576 /* MigrationWelcomeBlogTableViewCell.swift in Sources */, FABB242C2602FC2C00C8785C /* KanvasCameraCustomUI.swift in Sources */, - FABB242D2602FC2C00C8785C /* MediaPreviewHelper.swift in Sources */, F1C197A72670DDB100DE1FF7 /* BloggingRemindersTracker.swift in Sources */, FABB242E2602FC2C00C8785C /* OffsetTableViewHandler.swift in Sources */, FABB242F2602FC2C00C8785C /* UINavigationController+SplitViewFullscreen.swift in Sources */, 803BB99029667BAF00B3F6D6 /* JetpackBrandingTextProvider.swift in Sources */, + F48EBF8B2B2F94DD004CD561 /* BlogDashboardAnalyticPropertiesProviding.swift in Sources */, + 0CE7833E2B08F3C300B114EB /* ExternalMediaPickerViewController.swift in Sources */, FABB24302602FC2C00C8785C /* ReaderReblogPresenter.swift in Sources */, + 0CE538D12B0E317000834BA2 /* StockPhotosWelcomeView.swift in Sources */, FABB24312602FC2C00C8785C /* BodyContentGroup.swift in Sources */, FABB24322602FC2C00C8785C /* WPStyleGuide+Search.swift in Sources */, FABB24332602FC2C00C8785C /* WindowManager.swift in Sources */, - 08799C262A334645005317F7 /* Spacing.swift in Sources */, FABB24342602FC2C00C8785C /* NSFileManager+FolderSize.swift in Sources */, 0840513F2A4DDE3400A596E6 /* CompliancePopoverCoordinator.swift in Sources */, FABB24352602FC2C00C8785C /* ErrorStateViewController.swift in Sources */, + 0C0AD1072B0C483F00EC06E6 /* ExternalMediaSelectionTitleView.swift in Sources */, FABB24362602FC2C00C8785C /* WP3DTouchShortcutCreator.swift in Sources */, FABB24372602FC2C00C8785C /* GIFPlaybackStrategy.swift in Sources */, FE003F5F282D61BA006F8D1D /* BloggingPrompt+CoreDataProperties.swift in Sources */, @@ -24906,13 +25041,11 @@ F4D829642930EA4C00038726 /* MigrationDeleteWordPressViewModel.swift in Sources */, FABB243C2602FC2C00C8785C /* StockPhotosService.swift in Sources */, 086F2481284F52DB00032F39 /* TooltipPresenter.swift in Sources */, - 08EA036A29C9C39A00B72A87 /* Color+DesignSystem.swift in Sources */, FABB243D2602FC2C00C8785C /* Blog+Analytics.swift in Sources */, 08A250F928D9E87600F50420 /* CommentDetailInfoViewController.swift in Sources */, FABB243E2602FC2C00C8785C /* ReaderStreamViewController+Helper.swift in Sources */, FABB243F2602FC2C00C8785C /* ReaderAbstractTopic.swift in Sources */, FABB24402602FC2C00C8785C /* PostService+UnattachedMedia.swift in Sources */, - 173B215627875E0600D4DD6B /* DeviceMediaPermissionsHeader.swift in Sources */, FABB24412602FC2C00C8785C /* ReaderDefaultTopic.swift in Sources */, FABB24422602FC2C00C8785C /* ReaderCard+CoreDataClass.swift in Sources */, FABB24432602FC2C00C8785C /* ReplyToComment.swift in Sources */, @@ -24944,6 +25077,7 @@ FABB24552602FC2C00C8785C /* WPStyleGuide+Suggestions.m in Sources */, FABB24562602FC2C00C8785C /* SiteStatsInsightsViewModel.swift in Sources */, 0C71959C2A3CA582002EA18C /* SiteSettingsRelatedPostsView.swift in Sources */, + FACF66CE2ADD645C008C3E13 /* PostListHeaderView.swift in Sources */, FABB24572602FC2C00C8785C /* CheckmarkTableViewCell.swift in Sources */, FABB24582602FC2C00C8785C /* ReaderManageScenePresenter.swift in Sources */, FABB24592602FC2C00C8785C /* MediaService.swift in Sources */, @@ -24952,7 +25086,6 @@ FABB245D2602FC2C00C8785C /* ResultsPage.swift in Sources */, FABB245E2602FC2C00C8785C /* FormattableRangesFactory.swift in Sources */, FABB245F2602FC2C00C8785C /* Menu+ViewDesign.m in Sources */, - FABB24602602FC2C00C8785C /* BlogDetailsViewController+DomainCredit.swift in Sources */, 17C1D67D2670E3DC006C8970 /* SiteIconPickerView.swift in Sources */, 8BAC9D9F27BAB97E008EA44C /* BlogDashboardRemoteEntity.swift in Sources */, FA4EBCC22A98F00200BA3DFB /* NoSitesView.swift in Sources */, @@ -25016,6 +25149,7 @@ FABB24872602FC2C00C8785C /* PageLayoutService.swift in Sources */, FABB24882602FC2C00C8785C /* Route+Page.swift in Sources */, FABB24892602FC2C00C8785C /* PlayIconView.swift in Sources */, + 0CE783422B08FB2E00B114EB /* ExternalMediaPickerCollectionCell.swift in Sources */, FABB248A2602FC2C00C8785C /* WPTabBarController+Swift.swift in Sources */, 805CC0C2296CF3B3002941DC /* JetpackNewUsersOverlaySecondaryView.swift in Sources */, FABB248B2602FC2C00C8785C /* LanguageSelectorViewController.swift in Sources */, @@ -25063,10 +25197,10 @@ 3FF15A56291B4EEA00E1B4E5 /* MigrationCenterView.swift in Sources */, FABB24A62602FC2C00C8785C /* JetpackRestoreCompleteViewController.swift in Sources */, FABB24A72602FC2C00C8785C /* FormattableNoticonRange.swift in Sources */, - FABB24A82602FC2C00C8785C /* PromptViewController.swift in Sources */, 46F583AC2624CE790010A723 /* BlockEditorSettings+CoreDataProperties.swift in Sources */, FABB24AA2602FC2C00C8785C /* ActivityListRow.swift in Sources */, FABB24AB2602FC2C00C8785C /* UIColor+MurielColorsObjC.swift in Sources */, + FEF207F42AF2903E0025CB2C /* BloggingPromptRemoteObject.swift in Sources */, FABB24AC2602FC2C00C8785C /* InteractiveNotificationsManager.swift in Sources */, FABB24AD2602FC2C00C8785C /* PageTemplateLayout+CoreDataProperties.swift in Sources */, F195C42D26DFBE3A000EC884 /* WordPressBackgroundTaskEventHandler.swift in Sources */, @@ -25086,6 +25220,7 @@ F4D9188729D78C9100974A71 /* BlogDetailsViewController+Strings.swift in Sources */, FABB24B82602FC2C00C8785C /* DynamicHeightCollectionView.swift in Sources */, FABB24B92602FC2C00C8785C /* RegisterDomainDetailsViewModel+RowList.swift in Sources */, + 0C700B872AE1E1300085C2EE /* PageListCell.swift in Sources */, FEDA8D9D2A5AA7050081314F /* PrepublishingViewController+JetpackSocial.swift in Sources */, FABB24BA2602FC2C00C8785C /* TenorGIF.swift in Sources */, FABB24BB2602FC2C00C8785C /* ThemeBrowserViewController.swift in Sources */, @@ -25101,13 +25236,14 @@ F4CBE3DA29265BC8004FFBB6 /* LogOutActionHandler.swift in Sources */, FABB24C22602FC2C00C8785C /* WPSplitViewController.swift in Sources */, FABB24C32602FC2C00C8785C /* CollectionViewContainerRow.swift in Sources */, + 0CFE9ACA2AF52D3B00B8F659 /* PostSettingsViewController+Swift.swift in Sources */, FABB24C42602FC2C00C8785C /* JetpackConnectionWebViewController.swift in Sources */, FABB24C52602FC2C00C8785C /* SiteSuggestion+CoreDataProperties.swift in Sources */, F47E154B29E84A9300B6E426 /* SiteCreationPurchasingWebFlowController.swift in Sources */, FABB24C62602FC2C00C8785C /* UntouchableWindow.swift in Sources */, FABB24C72602FC2C00C8785C /* WPProgressTableViewCell.m in Sources */, + 0C749D7B2B0543D0004CB468 /* WPImageViewController+Swift.swift in Sources */, FEA7949126DF7F3700CEC520 /* WPStyleGuide+CommentDetail.swift in Sources */, - FABB24C82602FC2C00C8785C /* StockPhotosPicker.swift in Sources */, 011F52D42A1B84FF00B04114 /* PlansTracker.swift in Sources */, FABB24CA2602FC2C00C8785C /* UnifiedPrologueViewController.swift in Sources */, FABB24CC2602FC2C00C8785C /* CollapsableHeaderViewController.swift in Sources */, @@ -25130,8 +25266,8 @@ FA3A28192A38D36900206D74 /* BlazeCampaignTableViewCell.swift in Sources */, C373D6E828045281008F8C26 /* SiteIntentData.swift in Sources */, FABB24DA2602FC2C00C8785C /* HomeWidgetAllTimeData.swift in Sources */, - 0C8B8C102ACDBE1900CCE50F /* DisabledVideoOverlay.swift in Sources */, DC3B9B2D27739760003F7249 /* TimeZoneSelectorViewModel.swift in Sources */, + 0C5751112B011468001074E5 /* RemoteConfigDebugView.swift in Sources */, 0C8FC9A82A8BFAAE0059DCE4 /* NSItemProvider+Exportable.swift in Sources */, FABB24DB2602FC2C00C8785C /* UIView+Borders.swift in Sources */, FABB24DC2602FC2C00C8785C /* BasePost.m in Sources */, @@ -25151,7 +25287,6 @@ F4D82972293109A600038726 /* DashboardMigrationSuccessCell+Jetpack.swift in Sources */, 83A337A22A9FA525009ED60C /* ReaderSiteHeaderView.swift in Sources */, FABB24E82602FC2C00C8785C /* ReaderPost.m in Sources */, - FABB24E92602FC2C00C8785C /* MediaLibraryMediaPickingCoordinator.swift in Sources */, CECEEB562823164800A28ADE /* MediaCacheSettingsViewController.swift in Sources */, FABB24EA2602FC2C00C8785C /* PageTemplateLayout+CoreDataClass.swift in Sources */, FABB24EB2602FC2C00C8785C /* NoResultsViewController.swift in Sources */, @@ -25175,14 +25310,15 @@ FABB24FC2602FC2C00C8785C /* NoResultsViewController+MediaLibrary.swift in Sources */, FA4B203929A8C48F0089FE68 /* AbstractPost+Blaze.swift in Sources */, 083ED8CD2A4322CB007F89B3 /* ComplianceLocationService.swift in Sources */, + 0CB424F72AE0416D0080B807 /* SolidColorActivityIndicator.swift in Sources */, FABB24FD2602FC2C00C8785C /* StockPhotosDataLoader.swift in Sources */, 010459E729153FFF000C7778 /* JetpackNotificationMigrationService.swift in Sources */, FABB24FE2602FC2C00C8785C /* ReaderTopicToReaderTagTopic37to38.swift in Sources */, + F4D140212AFD9D5300961797 /* TransferDomainsWebViewController.swift in Sources */, FABB24FF2602FC2C00C8785C /* ChangeUsernameViewModel.swift in Sources */, C31852A329673BFC00A78BE9 /* MenusViewController+JetpackBannerViewController.swift in Sources */, FABB25002602FC2C00C8785C /* PlansLoadingIndicatorView.swift in Sources */, FABB25012602FC2C00C8785C /* JetpackScanThreatDetailsViewController.swift in Sources */, - C72A4F7B26408943009CA633 /* JetpackNotWPErrorViewModel.swift in Sources */, FABB25022602FC2C00C8785C /* NotificationTextContent.swift in Sources */, FABB25032602FC2C00C8785C /* PostEditor+Publish.swift in Sources */, FABB25042602FC2C00C8785C /* MediaCoordinator.swift in Sources */, @@ -25215,6 +25351,7 @@ FABB25152602FC2C00C8785C /* PeopleService.swift in Sources */, FABB25162602FC2C00C8785C /* StatsPeriodStore.swift in Sources */, FABB25172602FC2C00C8785C /* ImmuTable+WordPress.swift in Sources */, + 4A5DE7392B0D511900363171 /* PageTree.swift in Sources */, FABB25182602FC2C00C8785C /* NoticeStyle.swift in Sources */, FABB25192602FC2C00C8785C /* NotificationSettings.swift in Sources */, FABB251A2602FC2C00C8785C /* HeaderContentStyles.swift in Sources */, @@ -25223,7 +25360,6 @@ FABB251B2602FC2C00C8785C /* Revision.swift in Sources */, FABB251C2602FC2C00C8785C /* PluginDirectoryAccessoryItem.swift in Sources */, FEDA1AD9269D475D0038EC98 /* ListTableViewCell+Comments.swift in Sources */, - FABB251D2602FC2C00C8785C /* PHAsset+Exporters.swift in Sources */, FABB251E2602FC2C00C8785C /* GutenbergLightNavigationController.swift in Sources */, FABB251F2602FC2C00C8785C /* AppFeedbackPromptView.swift in Sources */, FABB25202602FC2C00C8785C /* UIView+ExistingConstraints.swift in Sources */, @@ -25232,29 +25368,25 @@ FABB25232602FC2C00C8785C /* StatsCellHeader.swift in Sources */, DCCDF75C283BEFEA00AA347E /* SiteStatsInsightsDetailsTableViewController.swift in Sources */, FABB25242602FC2C00C8785C /* UIEdgeInsets.swift in Sources */, - FABB25252602FC2C00C8785C /* RestorePageTableViewCell.m in Sources */, C7234A3B2832BA240045C63F /* QRLoginCoordinator.swift in Sources */, FABB25262602FC2C00C8785C /* String+RegEx.swift in Sources */, 175CC17A27230DC900622FB4 /* Bool+StringRepresentation.swift in Sources */, F1BC842F27035A1800C39993 /* BlogService+Domains.swift in Sources */, FE7FAABF299A998F0032A6F2 /* EventTracker.swift in Sources */, - 8B33BC9627A0C14C00DB5985 /* BlogDetailsViewController+QuickActions.swift in Sources */, FABB25272602FC2C00C8785C /* UIImage+Exporters.swift in Sources */, 0133A7BF2A8CEADD00B36E58 /* SupportCoordinator.swift in Sources */, FABB25282602FC2C00C8785C /* PostStatsTitleCell.swift in Sources */, FABB25292602FC2C00C8785C /* CommentService.m in Sources */, - 0880BADD29ED6FF3002D3AB0 /* UIColor+DesignSystem.swift in Sources */, FE50965D2A20D0F300DDD071 /* CommentTableHeaderView.swift in Sources */, - FABB252A2602FC2C00C8785C /* MediaLibraryStrings.swift in Sources */, FABB252B2602FC2C00C8785C /* StoreFetchingStatus.swift in Sources */, FABB252C2602FC2C00C8785C /* SiteDateFormatters.swift in Sources */, FABB252D2602FC2C00C8785C /* PostEditor.swift in Sources */, FABB252F2602FC2C00C8785C /* JetpackRestoreWarningCoordinator.swift in Sources */, FEF28E832ACB3DCE006C6579 /* ReaderDetailNewHeaderView.swift in Sources */, - FABB25302602FC2C00C8785C /* WPPickerView.m in Sources */, FABB25312602FC2C00C8785C /* UIImageView+SiteIcon.swift in Sources */, FABB25322602FC2C00C8785C /* ReaderSiteSearchService.swift in Sources */, FABB25332602FC2C00C8785C /* QuickStartNavigationSettings.swift in Sources */, + 0CFE9AC72AF44A9F00B8F659 /* AbstractPostHelper+Actions.swift in Sources */, FABB25342602FC2C00C8785C /* WPImageViewController.m in Sources */, FABB25352602FC2C00C8785C /* ReaderListTopic.swift in Sources */, FABB25362602FC2C00C8785C /* PublicizeService.swift in Sources */, @@ -25269,8 +25401,7 @@ C39ABBAF294BE84000F6F278 /* BackupListViewController+JetpackBannerViewController.swift in Sources */, FEA6517C281C491C002EA086 /* BloggingPromptsService.swift in Sources */, FABB253C2602FC2C00C8785C /* FormattableContentAction.swift in Sources */, - 3F3DD0B326FD176800F5F121 /* PresentationCard.swift in Sources */, - FABB253D2602FC2C00C8785C /* SiteVerticalsService.swift in Sources */, + 3F3DD0B326FD176800F5F121 /* SiteDomainsPresentationCard.swift in Sources */, FABB253E2602FC2C00C8785C /* ReaderBlockedSiteCell.swift in Sources */, FE32F007275F62620040BE67 /* WebCommentContentRenderer.swift in Sources */, FABB253F2602FC2C00C8785C /* JetpackRestoreWarningViewController.swift in Sources */, @@ -25295,7 +25426,6 @@ FABB254F2602FC2C00C8785C /* CountriesMapView.swift in Sources */, FAD257132611B04D00EDAF88 /* UIColor+JetpackColors.swift in Sources */, 4A1E77C7298897F6006281CC /* SharingSyncService.swift in Sources */, - FABB25502602FC2C00C8785C /* MediaLibraryViewController.swift in Sources */, FABB25512602FC2C00C8785C /* TodayWidgetStats.swift in Sources */, FABB25522602FC2C00C8785C /* GutenbergMediaFilesUploadProcessor.swift in Sources */, FABB25532602FC2C00C8785C /* WordPress-30-31.xcmappingmodel in Sources */, @@ -25303,6 +25433,7 @@ FABB25552602FC2C00C8785C /* Charts+AxisFormatters.swift in Sources */, 80D9CFFE29E711E200FE3400 /* DashboardPageCell.swift in Sources */, FABB25572602FC2C00C8785C /* EpilogueSectionHeaderFooter.swift in Sources */, + 016231512B3B3CAD0010E377 /* PrimaryDomainView.swift in Sources */, FABB25582602FC2C00C8785C /* PostCategoriesViewController.swift in Sources */, FABB25592602FC2C00C8785C /* NSManagedObject+Lookup.swift in Sources */, FABB255A2602FC2C00C8785C /* StatsPeriodHelper.swift in Sources */, @@ -25320,12 +25451,14 @@ FABB25652602FC2C00C8785C /* MediaProgressCoordinator.swift in Sources */, FABB25662602FC2C00C8785C /* NSMutableAttributedString+Helpers.swift in Sources */, FABB25672602FC2C00C8785C /* PushNotificationsManager.swift in Sources */, + 0CD9CCA02AD73A560044A33C /* PostSearchViewController.swift in Sources */, FABB25682602FC2C00C8785C /* StatsInsightsStore.swift in Sources */, FABB25692602FC2C00C8785C /* MenuItemPagesViewController.m in Sources */, 4A9314E52979FA4700360232 /* PostCategory+Creation.swift in Sources */, + F4B0F4842ADED9B5003ABC61 /* DomainsService+AllDomains.swift in Sources */, FA6402D229C325C1007A235C /* MovedToJetpackEventsTracker.swift in Sources */, - FABB256B2602FC2C00C8785C /* InteractivePostView.swift in Sources */, FABB256C2602FC2C00C8785C /* LinearGradientView.swift in Sources */, + FA141F282AEC1D9E00C9A653 /* PageMenuViewModel.swift in Sources */, FABB256D2602FC2C00C8785C /* WizardStep.swift in Sources */, 0C35FFF229CB81F700D224EB /* BlogDashboardHelpers.swift in Sources */, C395FB242821FE4B00AE7C11 /* SiteDesignSection.swift in Sources */, @@ -25333,8 +25466,8 @@ FABB256E2602FC2C00C8785C /* PostPostViewController.swift in Sources */, FABB256F2602FC2C00C8785C /* QuickStartSpotlightView.swift in Sources */, FABB25702602FC2C00C8785C /* ReaderReblogAction.swift in Sources */, + F479995F2AFD241E0023F4FB /* RegisterDomainTransferFooterView.swift in Sources */, FABB25712602FC2C00C8785C /* SiteAssemblyWizardContent.swift in Sources */, - 08A4E12A289D202F001D9EC7 /* UserPersistentStore.swift in Sources */, FEDDD47026A03DE900F8942B /* ListTableViewCell+Notifications.swift in Sources */, FABB25722602FC2C00C8785C /* UITableView+Header.swift in Sources */, FABB25732602FC2C00C8785C /* RichTextContentStyles.swift in Sources */, @@ -25358,8 +25491,6 @@ FABB257E2602FC2C00C8785C /* MessageAnimator.swift in Sources */, C3AB4879292F114A001F7AF8 /* UIApplication+AppAvailability.swift in Sources */, FABB257F2602FC2C00C8785C /* JetpackRestoreOptionsViewController.swift in Sources */, - FABB25802602FC2C00C8785C /* WPStyleGuide+Loader.swift in Sources */, - FABB25812602FC2C00C8785C /* MediaThumbnailService.swift in Sources */, FABB25822602FC2C00C8785C /* MediaExporter.swift in Sources */, 1756DBE028328B76006E6DB9 /* DonutChartView.swift in Sources */, 8B15CDAC27EB89AD00A75749 /* BlogDashboardPostsParser.swift in Sources */, @@ -25378,11 +25509,13 @@ 982D26202788DDF200A41286 /* ReaderCommentsFollowPresenter.swift in Sources */, FABB258A2602FC2C00C8785C /* BlogAuthor.swift in Sources */, FABB258B2602FC2C00C8785C /* Blog+BlogAuthors.swift in Sources */, + FACF66D12ADD6CD8008C3E13 /* PostListItemViewModel.swift in Sources */, FABB258C2602FC2C00C8785C /* ActivityContentStyles.swift in Sources */, FABB258D2602FC2C00C8785C /* CategorySectionTableViewCell.swift in Sources */, FAE4CA692732C094003BFDFE /* QuickStartPromptViewController.swift in Sources */, 175721172754D31F00DE38BC /* AppIcon.swift in Sources */, FABB258E2602FC2C00C8785C /* StatsForegroundObservable.swift in Sources */, + 80348F332AF880820045CCD3 /* DomainPurchaseChoicesView.swift in Sources */, FABB258F2602FC2C00C8785C /* AbstractPost+TitleForVisibility.swift in Sources */, FABB25902602FC2C00C8785C /* SharingViewController.m in Sources */, 0C75E26F2A9F63CB00B784E5 /* MediaImageService.swift in Sources */, @@ -25403,16 +25536,15 @@ FABB259A2602FC2C00C8785C /* ReaderBlockSiteAction.swift in Sources */, FABB259C2602FC2C00C8785C /* ReaderPostService+RelatedPosts.swift in Sources */, FABB259D2602FC2C00C8785C /* Blog+Capabilities.swift in Sources */, - FABB259E2602FC2C00C8785C /* RestorePostTableViewCell.swift in Sources */, 1756F1E02822BB6F00CD0915 /* SparklineView.swift in Sources */, FABB259F2602FC2C00C8785C /* MenuItemCategoriesViewController.m in Sources */, - FABB25A02602FC2C00C8785C /* UINavigationBar+Appearance.swift in Sources */, FABB25A12602FC2C00C8785C /* QuickStartChecklistViewController.swift in Sources */, FABB25A22602FC2C00C8785C /* MenuItemsViewController.m in Sources */, FABB25A32602FC2C00C8785C /* ReachabilityUtils+OnlineActions.swift in Sources */, FABB25A42602FC2C00C8785C /* TableViewKeyboardObserver.swift in Sources */, FABB25A52602FC2C00C8785C /* BlogToJetpackAccount.m in Sources */, FABB25A62602FC2C00C8785C /* NotificationSettingsViewController.swift in Sources */, + 0CA10FA92ADB7C5300CE75AC /* PostSearchService.swift in Sources */, FABB25A72602FC2C00C8785C /* ChangeUsernameViewController.swift in Sources */, B0B89DC12A1E882F003D5295 /* DomainResultView.swift in Sources */, 173DF292274522A1007C64B5 /* AppAboutScreenConfiguration.swift in Sources */, @@ -25422,6 +25554,7 @@ FABB25AB2602FC2C00C8785C /* FancyAlertViewController+NotificationPrimer.swift in Sources */, FABB25AC2602FC2C00C8785C /* Charts+Support.swift in Sources */, FABB25AD2602FC2C00C8785C /* SharingAuthorizationWebViewController.swift in Sources */, + 80DB57932AF8B59B00C728FF /* RegisterDomainCoordinator.swift in Sources */, 3FB1929326C6C57A000F5AA3 /* TimeSelectionView.swift in Sources */, FABB25AE2602FC2C00C8785C /* SearchWrapperView.swift in Sources */, 98830A932747043B0061A87C /* BorderedButtonTableViewCell.swift in Sources */, @@ -25443,7 +25576,7 @@ 3FFDEF812917882800B625CE /* MigrationNavigationController.swift in Sources */, FABB25BC2602FC2C00C8785C /* SharingDetailViewController.m in Sources */, 837B49DE283C2AE80061A657 /* BloggingPromptSettingsReminderDays+CoreDataProperties.swift in Sources */, - 3FAF9CC326D02FC500268EA2 /* DomainsDashboardView.swift in Sources */, + 3FAF9CC326D02FC500268EA2 /* SiteDomainsView.swift in Sources */, FA70024D29DC3B5500E874FD /* DashboardActivityLogCardCell.swift in Sources */, F48D44BB2989A9070051EAA6 /* ReaderSiteService.swift in Sources */, FABB25BE2602FC2C00C8785C /* SettingsPickerViewController.swift in Sources */, @@ -25474,12 +25607,14 @@ FABB25D02602FC2C00C8785C /* SiteCreationWizardLauncher.swift in Sources */, FABB25D12602FC2C00C8785C /* ReaderSitesCardCell.swift in Sources */, 46F583B02624CE790010A723 /* BlockEditorSettingElement+CoreDataProperties.swift in Sources */, + 833441C82B1AA9DF00B1FD44 /* SOTWCardView.swift in Sources */, FABB25D22602FC2C00C8785C /* SiteSegmentsWizardContent.swift in Sources */, FABB25D32602FC2C00C8785C /* ReaderTopicToReaderDefaultTopic37to38.swift in Sources */, FEC26034283FC902003D886A /* RootViewCoordinator+BloggingPrompt.swift in Sources */, FABB25D42602FC2C00C8785C /* UploadsManager.swift in Sources */, 9856A3E5261FD27A008D6354 /* UserProfileSectionHeader.swift in Sources */, FABB25D62602FC2C00C8785C /* ActivityListSectionHeaderView.swift in Sources */, + FE34ACDB2B17AA9300108B3C /* BloganuaryOverlayViewController.swift in Sources */, FABB25D72602FC2C00C8785C /* WPAccount+RestApi.swift in Sources */, FABB25D82602FC2C00C8785C /* ImageCropOverlayView.swift in Sources */, FABB25D92602FC2C00C8785C /* UIView+SpringAnimations.swift in Sources */, @@ -25513,12 +25648,13 @@ FABB25EF2602FC2C00C8785C /* ReaderWebView.swift in Sources */, FABB25F02602FC2C00C8785C /* PostVisibilitySelectorViewController.swift in Sources */, FABB25F12602FC2C00C8785C /* UIApplication+Helpers.m in Sources */, - 3F3DD0B026FCDA3100F5F121 /* PresentationButton.swift in Sources */, + FA141F2B2AEC23E300C9A653 /* PageListViewController+Menu.swift in Sources */, FABB25F22602FC2C00C8785C /* ErrorStateView.swift in Sources */, FABB25F32602FC2C00C8785C /* MenusSelectionDetailView.m in Sources */, FABB25F42602FC2C00C8785C /* Blog+Title.swift in Sources */, C34E94BA28EDF7D900D27A16 /* InfiniteScrollerView.swift in Sources */, FADC40AC2A8D093900C19997 /* BlogDetailsViewController+Me.swift in Sources */, + 017C57BC2B2B5555001E7687 /* DomainSelectionViewController.swift in Sources */, FABB25F52602FC2C00C8785C /* FilterableCategoriesViewController.swift in Sources */, FABB25F62602FC2C00C8785C /* Post+CoreDataProperties.swift in Sources */, FABB25F72602FC2C00C8785C /* BasePost.swift in Sources */, @@ -25530,16 +25666,18 @@ FABB25FD2602FC2C00C8785C /* JetpackRemoteInstallState.swift in Sources */, FABB25FE2602FC2C00C8785C /* Scheduler.swift in Sources */, 4A2172F928EAACFF0006F4F1 /* BlogQuery.swift in Sources */, + 017008462B35C25C00C80490 /* SiteDomainsViewModel.swift in Sources */, F1C740C026B1D4D2005D0809 /* StoreSandboxSecretScreen.swift in Sources */, 801D94F02919E7D70051993E /* JetpackFullscreenOverlayGeneralViewModel.swift in Sources */, FABB25FF2602FC2C00C8785C /* RegisterDomainDetailsViewModel+RowDefinitions.swift in Sources */, 0CAE8EF72A9E9EE30073EEB9 /* SiteMediaCollectionCellViewModel.swift in Sources */, 98E082A02637545C00537BF1 /* PostService+Likes.swift in Sources */, FABB26002602FC2C00C8785C /* GravatarProfile.swift in Sources */, + F41D98D92B3901F5004EC050 /* DashboardDynamicCardAnalyticsEvent.swift in Sources */, FABB26012602FC2C00C8785C /* ReaderShareAction.swift in Sources */, C7124E4F2638528F00929318 /* JetpackPrologueViewController.swift in Sources */, FE341706275FA157005D5CA7 /* RichCommentContentRenderer.swift in Sources */, - 08240C2F2AB8A2DD00E7AEA8 /* DomainListCard.swift in Sources */, + 08240C2F2AB8A2DD00E7AEA8 /* AllDomainsListCardView.swift in Sources */, FABB26022602FC2C00C8785C /* PostingActivityMonth.swift in Sources */, 0107E15D28FFE99300DE87DB /* WidgetConfiguration.swift in Sources */, FABB26042602FC2C00C8785C /* ReaderTabItem.swift in Sources */, @@ -25567,16 +25705,15 @@ FABB26152602FC2C00C8785C /* CocoaLumberjack.swift in Sources */, 3FA62FD426FE2E4B0020793A /* ShapeWithTextView.swift in Sources */, FABB26162602FC2C00C8785C /* WPUploadStatusButton.m in Sources */, + F4141EE62AE71AF0000D2AAE /* AllDomainsListViewModel.swift in Sources */, FABB26172602FC2C00C8785C /* MediaExternalExporter.swift in Sources */, FABB26182602FC2C00C8785C /* RegisterDomainDetailsViewModel+SectionDefinitions.swift in Sources */, - FABB26192602FC2C00C8785C /* ActionRow.swift in Sources */, C395FB272822148400AE7C11 /* RemoteSiteDesign+Thumbnail.swift in Sources */, 0CD382842A4B699E00612173 /* DashboardBlazeCardCellViewModel.swift in Sources */, 9815D0B426B49A0600DF7226 /* Comment+CoreDataProperties.swift in Sources */, FABB261A2602FC2C00C8785C /* AppSettingsViewController.swift in Sources */, 4A82C43228D321A300486CFF /* Blog+Post.swift in Sources */, FABB261B2602FC2C00C8785C /* ReaderPostStreamService.swift in Sources */, - FABB261E2602FC2C00C8785C /* PostCardCell.swift in Sources */, 8F228B22E190FF92D05E53DB /* TimeZoneSearchHeaderView.swift in Sources */, 8F2289EDA1886BF77687D72D /* TimeZoneSelectorViewController.swift in Sources */, ); @@ -25619,6 +25756,7 @@ CC7CB97322B1510900642EE9 /* SignupTests.swift in Sources */, CC52188C2278C622008998CE /* EditorFlow.swift in Sources */, 01281E9C2A051EEA00464F8F /* MenuNavigationTests.swift in Sources */, + 1D0402732B10FA9100888C30 /* AppSettingsTests.swift in Sources */, 8B5FEAF125A746CB000CBFF7 /* UIApplication+mainWindow.swift in Sources */, BED4D8331FF11E3800A11345 /* LoginFlow.swift in Sources */, FF2716A11CABC7D40006E2D4 /* XCTest+Extensions.swift in Sources */, @@ -30128,20 +30266,20 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 17A8858B2757B97F0071FCA3 /* XCRemoteSwiftPackageReference "AutomatticAbout-swift" */ = { + 0CD9FB852AFA71B9009D9C7A /* XCRemoteSwiftPackageReference "Charts" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/automattic/AutomatticAbout-swift"; + repositoryURL = "https://github.com/danielgindi/Charts"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.1.2; + minimumVersion = 5.0.0; }; }; - 3F2B62DA284F4E0B0008CD59 /* XCRemoteSwiftPackageReference "Charts" */ = { + 17A8858B2757B97F0071FCA3 /* XCRemoteSwiftPackageReference "AutomatticAbout-swift" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/danielgindi/Charts"; + repositoryURL = "https://github.com/automattic/AutomatticAbout-swift"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 4.0.3; + minimumVersion = 1.1.2; }; }; 3F338B6F289BD3040014ADC5 /* XCRemoteSwiftPackageReference "Nimble" */ = { @@ -30195,6 +30333,24 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 08E63FCC2B28E52B00747E21 /* DesignSystem */ = { + isa = XCSwiftPackageProductDependency; + productName = DesignSystem; + }; + 08E63FCE2B28E53400747E21 /* DesignSystem */ = { + isa = XCSwiftPackageProductDependency; + productName = DesignSystem; + }; + 0CD9FB862AFA71B9009D9C7A /* DGCharts */ = { + isa = XCSwiftPackageProductDependency; + package = 0CD9FB852AFA71B9009D9C7A /* XCRemoteSwiftPackageReference "Charts" */; + productName = DGCharts; + }; + 0CD9FB882AFA71C2009D9C7A /* DGCharts */ = { + isa = XCSwiftPackageProductDependency; + package = 0CD9FB852AFA71B9009D9C7A /* XCRemoteSwiftPackageReference "Charts" */; + productName = DGCharts; + }; 17A8858C2757B97F0071FCA3 /* AutomatticAbout */ = { isa = XCSwiftPackageProductDependency; package = 17A8858B2757B97F0071FCA3 /* XCRemoteSwiftPackageReference "AutomatticAbout-swift" */; @@ -30209,16 +30365,6 @@ isa = XCSwiftPackageProductDependency; productName = WordPressFlux; }; - 3F2B62DB284F4E0B0008CD59 /* Charts */ = { - isa = XCSwiftPackageProductDependency; - package = 3F2B62DA284F4E0B0008CD59 /* XCRemoteSwiftPackageReference "Charts" */; - productName = Charts; - }; - 3F2B62DD284F4E310008CD59 /* Charts */ = { - isa = XCSwiftPackageProductDependency; - package = 3F2B62DA284F4E0B0008CD59 /* XCRemoteSwiftPackageReference "Charts" */; - productName = Charts; - }; 3F338B70289BD3040014ADC5 /* Nimble */ = { isa = XCSwiftPackageProductDependency; package = 3F338B6F289BD3040014ADC5 /* XCRemoteSwiftPackageReference "Nimble" */; @@ -30249,6 +30395,18 @@ package = 3F411B6D28987E3F002513AE /* XCRemoteSwiftPackageReference "lottie-ios" */; productName = Lottie; }; + 3F9F23242B0AE1AC00B56061 /* JetpackStatsWidgetsCore */ = { + isa = XCSwiftPackageProductDependency; + productName = JetpackStatsWidgetsCore; + }; + 3F9F232A2B0B27DD00B56061 /* JetpackStatsWidgetsCore */ = { + isa = XCSwiftPackageProductDependency; + productName = JetpackStatsWidgetsCore; + }; + 3F9F232C2B0B281400B56061 /* JetpackStatsWidgetsCore */ = { + isa = XCSwiftPackageProductDependency; + productName = JetpackStatsWidgetsCore; + }; 3FC2C33C26C4CF0A00C6D98F /* XCUITestHelpers */ = { isa = XCSwiftPackageProductDependency; package = 3FC2C33B26C4CF0A00C6D98F /* XCRemoteSwiftPackageReference "XCUITestHelpers" */; @@ -30259,6 +30417,14 @@ package = 3FF1442E266F3C2400138163 /* XCRemoteSwiftPackageReference "ScreenObject" */; productName = ScreenObject; }; + 3FFB3F1F2AFC70B400A742B0 /* JetpackStatsWidgetsCore */ = { + isa = XCSwiftPackageProductDependency; + productName = JetpackStatsWidgetsCore; + }; + 3FFB3F232AFC730C00A742B0 /* JetpackStatsWidgetsCore */ = { + isa = XCSwiftPackageProductDependency; + productName = JetpackStatsWidgetsCore; + }; EA14532529AD874C001F3143 /* BuildkiteTestCollector */ = { isa = XCSwiftPackageProductDependency; package = EA14532629AD874C001F3143 /* XCRemoteSwiftPackageReference "test-collector-swift" */; @@ -30287,6 +30453,7 @@ E125443B12BF5A7200D87A0A /* WordPress.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + FE5F52D82AF9461200371A3A /* WordPress 153.xcdatamodel */, 0CFD6C792A73E703003DD0A0 /* WordPress 152.xcdatamodel */, FE1E200F2A45ACE900CE7C90 /* WordPress 151.xcdatamodel */, FA3A281D2A42049F00206D74 /* WordPress 150.xcdatamodel */, @@ -30440,7 +30607,7 @@ 8350E15911D28B4A00A7B073 /* WordPress.xcdatamodel */, E125443D12BF5A7200D87A0A /* WordPress 2.xcdatamodel */, ); - currentVersion = 0CFD6C792A73E703003DD0A0 /* WordPress 152.xcdatamodel */; + currentVersion = FE5F52D82AF9461200371A3A /* WordPress 153.xcdatamodel */; name = WordPress.xcdatamodeld; path = Classes/WordPress.xcdatamodeld; sourceTree = ""; diff --git a/WordPress/WordPressShareExtension/ShareExtensionEditorViewController.swift b/WordPress/WordPressShareExtension/ShareExtensionEditorViewController.swift index ac11c5e1a405..acfbd2822f3f 100644 --- a/WordPress/WordPressShareExtension/ShareExtensionEditorViewController.swift +++ b/WordPress/WordPressShareExtension/ShareExtensionEditorViewController.swift @@ -1309,7 +1309,6 @@ fileprivate extension ShareExtensionEditorViewController { static let aztecFormatBarDisabledColor = UIColor.neutral(.shade10) static let aztecFormatBarDividerColor = UIColor.divider static let aztecCursorColor = UIColor.primary - static let aztecFormatBarBackgroundColor = UIColor.basicBackground static let aztecFormatBarInactiveColor = UIColor.toolbarInactive static let aztecFormatBarActiveColor = UIColor.primary diff --git a/WordPress/WordPressShareExtension/ShareNoticeConstants.swift b/WordPress/WordPressShareExtension/ShareNoticeConstants.swift index 2c139e66f915..8919cfdb4b9e 100644 --- a/WordPress/WordPressShareExtension/ShareNoticeConstants.swift +++ b/WordPress/WordPressShareExtension/ShareNoticeConstants.swift @@ -26,9 +26,7 @@ struct ShareNoticeText { static let failureDraftTitleDefault = AppLocalizedString("Unable to upload 1 draft post", comment: "Alert displayed to the user when a single post has failed to upload.") static let failureTitleDefault = AppLocalizedString("Unable to upload 1 post", comment: "Alert displayed to the user when a single post has failed to upload.") - static let failureDraftTitleSingular = AppLocalizedString("Unable to upload 1 draft post, 1 file", comment: "Alert displayed to the user when a single post and 1 file has failed to upload.") static let failureTitleSingular = AppLocalizedString("Unable to upload 1 post, 1 file", comment: "Alert displayed to the user when a single post and 1 file has failed to upload.") - static let failureDraftTitlePlural = AppLocalizedString("Unable to upload 1 draft post, %ld files", comment: "Alert displayed to the user when a single post and multiple files have failed to upload.") static let failureTitlePlural = AppLocalizedString("Unable to upload 1 post, %ld files", comment: "Alert displayed to the user when a single post and multiple files have failed to upload.") /// Helper method to provide the formatted version of a success title based on the media item count. diff --git a/WordPress/WordPressShareExtension/ShareNoticeNavigationCoordinator.swift b/WordPress/WordPressShareExtension/ShareNoticeNavigationCoordinator.swift index 4c007f1df0cd..9006fff26629 100644 --- a/WordPress/WordPressShareExtension/ShareNoticeNavigationCoordinator.swift +++ b/WordPress/WordPressShareExtension/ShareNoticeNavigationCoordinator.swift @@ -46,7 +46,6 @@ class ShareNoticeNavigationCoordinator { onSuccess: @escaping (_ post: Post?) -> Void, onFailure: @escaping () -> Void) { let context = ContextManager.sharedInstance().mainContext - let postService = PostService(managedObjectContext: context) guard let postIDString = userInfo[ShareNoticeUserInfoKey.postID] as? String, let postID = NumberFormatter().number(from: postIDString), diff --git a/WordPress/WordPressShareExtension/String+Extensions.swift b/WordPress/WordPressShareExtension/String+Extensions.swift index 0b03e29ac93c..ce2e8f2273ef 100644 --- a/WordPress/WordPressShareExtension/String+Extensions.swift +++ b/WordPress/WordPressShareExtension/String+Extensions.swift @@ -95,9 +95,4 @@ extension String { let fullOptions = options.union([.anchored]) return range(of: prefix, options: fullOptions) != nil } - - /// Returns true if this String consists of digits - var isNumeric: Bool { - return !isEmpty && rangeOfCharacter(from: CharacterSet.decimalDigits.inverted) == nil - } } diff --git a/WordPress/WordPressTest/AbstractPostTest.swift b/WordPress/WordPressTest/AbstractPostTest.swift index 76a9390b24be..c4b77622d588 100644 --- a/WordPress/WordPressTest/AbstractPostTest.swift +++ b/WordPress/WordPressTest/AbstractPostTest.swift @@ -2,7 +2,7 @@ import XCTest import WordPressKit @testable import WordPress -class AbstractPostTest: XCTestCase { +class AbstractPostTest: CoreDataTestCase { func testTitleForStatus() { var status = PostStatusDraft @@ -31,7 +31,7 @@ class AbstractPostTest: XCTestCase { } func testFeaturedImageURLForDisplay() { - let post = PostBuilder().with(pathForDisplayImage: "https://wp.me/awesome.png").build() + let post = PostBuilder(mainContext).with(pathForDisplayImage: "https://wp.me/awesome.png").build() XCTAssertEqual(post.featuredImageURLForDisplay()?.absoluteString, "https://wp.me/awesome.png") } diff --git a/WordPress/WordPressTest/AccountBuilder.swift b/WordPress/WordPressTest/AccountBuilder.swift index 0525e6933f05..fc51b55333c5 100644 --- a/WordPress/WordPressTest/AccountBuilder.swift +++ b/WordPress/WordPressTest/AccountBuilder.swift @@ -6,14 +6,11 @@ import Foundation /// @objc class AccountBuilder: NSObject { - private let coreDataStack: CoreDataStack private var account: WPAccount - @objc - init(_ coreDataStack: CoreDataStack) { - self.coreDataStack = coreDataStack - - account = NSEntityDescription.insertNewObject(forEntityName: WPAccount.entityName(), into: coreDataStack.mainContext) as! WPAccount + @objc(initWithContext:) + init(_ context: NSManagedObjectContext) { + account = NSEntityDescription.insertNewObject(forEntityName: WPAccount.entityName(), into: context) as! WPAccount account.uuid = UUID().uuidString super.init() diff --git a/WordPress/WordPressTest/AccountServiceTests.swift b/WordPress/WordPressTest/AccountServiceTests.swift index 4aa020dd5caf..7fcb20e7cf78 100644 --- a/WordPress/WordPressTest/AccountServiceTests.swift +++ b/WordPress/WordPressTest/AccountServiceTests.swift @@ -174,7 +174,7 @@ class AccountServiceTests: CoreDataTestCase { func testMergeDuplicateAccountsKeepingNonDups() throws { let context = contextManager.mainContext - let account1 = AccountBuilder(contextManager) + let account1 = AccountBuilder(contextManager.mainContext) .with(id: 1) .with(username: "username") .with(authToken: "authToken") @@ -182,14 +182,14 @@ class AccountServiceTests: CoreDataTestCase { .build() // account2 is a duplicate of account1 - let account2 = AccountBuilder(contextManager) + let account2 = AccountBuilder(contextManager.mainContext) .with(id: 1) .with(username: "username") .with(authToken: "authToken") .with(uuid: UUID().uuidString) .build() - let account3 = AccountBuilder(contextManager) + let account3 = AccountBuilder(contextManager.mainContext) .with(id: 3) .with(username: "username3") .with(authToken: "authToken3") diff --git a/WordPress/WordPressTest/BasePostTests.swift b/WordPress/WordPressTest/BasePostTests.swift index 52322c625e80..5e38a40c7d88 100644 --- a/WordPress/WordPressTest/BasePostTests.swift +++ b/WordPress/WordPressTest/BasePostTests.swift @@ -3,7 +3,7 @@ import Nimble @testable import WordPress -class BasePostTests: XCTestCase { +class BasePostTests: CoreDataTestCase { private var localUser: String = { let splitedApplicationDirectory = FileManager.default.urls(for: .applicationDirectory, in: .allDomainsMask).first!.absoluteString.split(separator: "/") @@ -19,7 +19,7 @@ class BasePostTests: XCTestCase { } func testCorrectlyRefreshUUIDForCachedFeaturedImage() { - let post = PostBuilder() + let post = PostBuilder(mainContext) .with(pathForDisplayImage: "file:///Users/\(localUser)/Library/Developer/CoreSimulator/Devices/E690FA1D-AE36-4267-905D-8F6E71F4FA31/data/Containers/Data/Application/79D64D5C-6A83-4290-897E-794B7CC78B9F/Library/Caches/Media/thumbnail-p16-1792x1792.jpeg") .build() @@ -28,7 +28,7 @@ class BasePostTests: XCTestCase { } func testCorrectlyRefreshUUIDForFeaturedImageInDocumentsFolder() { - let post = PostBuilder() + let post = PostBuilder(mainContext) .with(pathForDisplayImage: "file:///Users/\(localUser)/Library/Developer/CoreSimulator/Devices/E690FA1D-AE36-4267-905D-8F6E71F4FA31/data/Containers/Data/Application/79D64D5C-6A83-4290-897E-794B7CC78B9F/Documents/Media/p16-1792x1792.jpeg") .build() @@ -37,7 +37,7 @@ class BasePostTests: XCTestCase { } func testDoesntChangeRemoteURLs() { - let post = PostBuilder() + let post = PostBuilder(mainContext) .with(pathForDisplayImage: "https://wordpress.com/image.gif") .build() diff --git a/WordPress/WordPressTest/Blog Dashboard/Cards/DashboardBloganuaryCardCellTests.swift b/WordPress/WordPressTest/Blog Dashboard/Cards/DashboardBloganuaryCardCellTests.swift new file mode 100644 index 000000000000..0063835fd739 --- /dev/null +++ b/WordPress/WordPressTest/Blog Dashboard/Cards/DashboardBloganuaryCardCellTests.swift @@ -0,0 +1,162 @@ +import XCTest +@testable import WordPress + +final class DashboardBloganuaryCardCellTests: CoreDataTestCase { + + private static var calendar = { + Calendar(identifier: .gregorian) + }() + private let blogID = 100 + private let featureFlags = FeatureFlagOverrideStore() + + override func setUp() { + super.setUp() + try? featureFlags.override(RemoteFeatureFlag.bloganuaryDashboardNudge, withValue: true) + } + + override func tearDown() { + super.tearDown() + try? featureFlags.override(RemoteFeatureFlag.bloganuaryDashboardNudge, + withValue: RemoteFeatureFlag.bloganuaryDashboardNudge.defaultValue) + } + + // MARK: - `shouldShowCard` tests + + func testCardIsNotShownWhenFlagIsDisabled() throws { + // Given + let blog = makeBlog() + makeBloggingPromptSettings() + try mainContext.save() + try featureFlags.override(RemoteFeatureFlag.bloganuaryDashboardNudge, withValue: false) + + // When + let result = DashboardBloganuaryCardCell.shouldShowCard(for: blog, date: sometimeInDecember) + + // Then + XCTAssertFalse(result) + } + + func testCardIsNotShownWhenSiteIsNotMarkedAsBloggingSite() throws { + // Given + let blog = makeBlog() + makeBloggingPromptSettings(markAsBloggingSite: false) + try mainContext.save() + + // When + let result = DashboardBloganuaryCardCell.shouldShowCard(for: blog, date: sometimeInDecember) + + // Then + XCTAssertFalse(result) + } + + func testCardIsNotShownForEligibleSitesOutsideEligibleMonths() throws { + // Given + let blog = makeBlog() + makeBloggingPromptSettings() + try mainContext.save() + + // When + let result = DashboardBloganuaryCardCell.shouldShowCard(for: blog, date: sometimeInFebruary) + + // Then + XCTAssertFalse(result) + } + + func testCardIsShownWhenSiteIsEligible() throws { + // Given + let blog = makeBlog() + makeBloggingPromptSettings() + try mainContext.save() + + // When + let resultForDecember = DashboardBloganuaryCardCell.shouldShowCard(for: blog, date: sometimeInDecember) + let resultForJanuary = DashboardBloganuaryCardCell.shouldShowCard(for: blog, date: sometimeInJanuary) + + // Then + XCTAssertTrue(resultForDecember) + XCTAssertTrue(resultForJanuary) + } + + func testCardIsShownForEligibleSitesThatHavePromptsDisabled() throws { + // Given + let blog = makeBlog() + makeBloggingPromptSettings(promptCardEnabled: false) + try mainContext.save() + + // When + let result = DashboardBloganuaryCardCell.shouldShowCard(for: blog, date: sometimeInDecember) + + // Then + XCTAssertTrue(result) + } +} + +// MARK: - Helpers + +private extension DashboardBloganuaryCardCellTests { + + var sometimeInDecember: Date { + let date = Date() + var components = Self.calendar.dateComponents([.year, .month, .day], from: date) + components.month = 12 + components.year = 2023 + components.day = 10 + + return Self.calendar.date(from: components) ?? date + } + + var sometimeInJanuary: Date { + let date = Date() + var components = Self.calendar.dateComponents([.year, .month, .day], from: date) + components.month = 1 + components.year = 2024 + components.day = 10 + + return Self.calendar.date(from: components) ?? date + } + + var sometimeInFebruary: Date { + let date = Date() + var components = Self.calendar.dateComponents([.year, .month, .day], from: date) + components.month = 2 + components.year = 2024 + components.day = 10 + + return Self.calendar.date(from: components) ?? date + } + + func prepareData() -> (Blog, BloggingPromptSettings) { + return (makeBlog(), makeBloggingPromptSettings()) + } + + func makeBlog() -> Blog { + let builder = BlogBuilder(mainContext) + .withAnAccount() + .with(dotComID: blogID) + + return builder.build() + } + + @discardableResult + func makeBloggingPromptSettings(markAsBloggingSite: Bool = true, promptCardEnabled: Bool = true) -> BloggingPromptSettings { + let settings = NSEntityDescription.insertNewObject(forEntityName: "BloggingPromptSettings", + into: mainContext) as! WordPress.BloggingPromptSettings + + let reminderDays = NSEntityDescription.insertNewObject(forEntityName: "BloggingPromptSettingsReminderDays", + into: mainContext) as! WordPress.BloggingPromptSettingsReminderDays + reminderDays.monday = false + reminderDays.tuesday = false + reminderDays.wednesday = false + reminderDays.thursday = false + reminderDays.friday = false + reminderDays.saturday = false + reminderDays.sunday = false + + settings.isPotentialBloggingSite = markAsBloggingSite + settings.promptCardEnabled = promptCardEnabled + settings.reminderDays = reminderDays + settings.siteID = Int32(blogID) + + return settings + } +} diff --git a/WordPress/WordPressTest/Blog Dashboard/Cards/DashboardJetpackSocialCardCellTests.swift b/WordPress/WordPressTest/Blog Dashboard/Cards/DashboardJetpackSocialCardCellTests.swift index c53c93cac627..667541e910b8 100644 --- a/WordPress/WordPressTest/Blog Dashboard/Cards/DashboardJetpackSocialCardCellTests.swift +++ b/WordPress/WordPressTest/Blog Dashboard/Cards/DashboardJetpackSocialCardCellTests.swift @@ -80,6 +80,14 @@ class DashboardJetpackSocialCardCellTests: CoreDataTestCase { XCTAssertFalse(shouldShowCard(for: blog)) } + func testCardDisplaysWhenJetpackSiteRunsOutOfShares() throws { + // Given, when + let blog = createTestBlog(hasConnections: true, publicizeInfoState: .exceedingLimit) + + // Then + XCTAssertTrue(shouldShowCard(for: blog)) + } + // MARK: - Card state tests func testInitialCardState() { @@ -115,6 +123,27 @@ class DashboardJetpackSocialCardCellTests: CoreDataTestCase { // Then XCTAssertEqual(subject.displayState, .none) } + + // MARK: Atomic Site Tests + + // In some cases, atomic sites could sometimes get limited sharing from the API. + // We'll need to ignore any sharing limit information if it's a Simple or Atomic site. + // Refs: p9F6qB-dLk-p2#comment-56603 + func testCardOutOfSharesDoesNotDisplayForAtomicSites() throws { + // Given, when + let blog = createTestBlog(isAtomic: true, hasConnections: true, publicizeInfoState: .exceedingLimit) + + // Then + XCTAssertFalse(shouldShowCard(for: blog)) + } + + func testCardNoConnectionDisplaysForAtomicSites() throws { + // Given, when + let blog = createTestBlog(isAtomic: true) + + // Then + XCTAssertTrue(shouldShowCard(for: blog)) + } } // MARK: - Helpers @@ -125,13 +154,22 @@ private extension DashboardJetpackSocialCardCellTests { return DashboardJetpackSocialCardCell.shouldShowCard(for: blog) } + enum PublicizeInfoState { + case none + case belowLimit + case exceedingLimit + } + func createTestBlog(isPublicizeSupported: Bool = true, + isAtomic: Bool = false, hasServices: Bool = true, - hasConnections: Bool = false) -> Blog { + hasConnections: Bool = false, + publicizeInfoState: PublicizeInfoState = .none) -> Blog { var builder = BlogBuilder(mainContext) .withAnAccount() .with(dotComID: 12345) .with(capabilities: [.PublishPosts]) + .with(atomic: isAtomic) if isPublicizeSupported { builder = builder.with(modules: ["publicize"]) @@ -143,9 +181,30 @@ private extension DashboardJetpackSocialCardCellTests { if hasConnections { let connection = PublicizeConnection(context: mainContext) + connection.status = "ok" builder = builder.with(connections: [connection]) } - return builder.build() + + let blog = builder.build() + + switch publicizeInfoState { + case .belowLimit: + let publicizeInfo = PublicizeInfo(context: mainContext) + publicizeInfo.shareLimit = 30 + publicizeInfo.sharesRemaining = 25 + blog.publicizeInfo = publicizeInfo + break + case .exceedingLimit: + let publicizeInfo = PublicizeInfo(context: mainContext) + publicizeInfo.shareLimit = 30 + publicizeInfo.sharesRemaining = 0 + blog.publicizeInfo = publicizeInfo + break + default: + break + } + + return blog } func createPublicizeService() -> PublicizeService { diff --git a/WordPress/WordPressTest/BlogBuilder.swift b/WordPress/WordPressTest/BlogBuilder.swift index fe03ef8fc072..9ea54aaee42f 100644 --- a/WordPress/WordPressTest/BlogBuilder.swift +++ b/WordPress/WordPressTest/BlogBuilder.swift @@ -141,11 +141,12 @@ final class BlogBuilder { return self } - func with(domainCount: Int, of type: DomainType) -> Self { + func with(domainCount: Int, of type: DomainType, domainName: String = "") -> Self { var domains: [ManagedDomain] = [] for _ in 0..\n

Tell us about a time when you felt out of place.

(courtesy of plinky.com)
\n") - XCTAssertTrue(firstPrompt.attribution.isEmpty) - - let firstDateComponents = Calendar.current.dateComponents(in: Self.utcTimeZone, from: firstPrompt.date) - XCTAssertEqual(firstDateComponents.year!, 2021) - XCTAssertEqual(firstDateComponents.month!, 9) - XCTAssertEqual(firstDateComponents.day!, 12) - - XCTAssertTrue(firstPrompt.answered) - XCTAssertEqual(firstPrompt.answerCount, 1) - XCTAssertEqual(firstPrompt.displayAvatarURLs.count, 1) - - // Verify mappings for the second prompt - let secondPrompt = prompts.last! - XCTAssertEqual(secondPrompt.promptID, 239) - XCTAssertEqual(secondPrompt.text, "Was there a toy or thing you always wanted as a child, during the holidays or on your birthday, but never received? Tell us about it.") - XCTAssertEqual(secondPrompt.title, "Prompt number 1") - XCTAssertEqual(secondPrompt.content, "\n

Was there a toy or thing you always wanted as a child, during the holidays or on your birthday, but never received? Tell us about it.

(courtesy of plinky.com)
\n") - XCTAssertEqual(secondPrompt.attribution, "dayone") - - let secondDateComponents = Calendar.current.dateComponents(in: Self.utcTimeZone, from: secondPrompt.date) - XCTAssertEqual(secondDateComponents.year!, 2022) - XCTAssertEqual(secondDateComponents.month!, 5) - XCTAssertEqual(secondDateComponents.day!, 3) - - XCTAssertFalse(secondPrompt.answered) - XCTAssertEqual(secondPrompt.answerCount, 0) - XCTAssertTrue(secondPrompt.displayAvatarURLs.isEmpty) + service.fetchPrompts(from: .init(timeIntervalSince1970: 0)) { [testPrompts] prompts in + XCTAssertEqual(prompts.count, testPrompts.count) + + prompts.forEach { prompt in + guard let expected = testPrompts.first(where: { $0.promptID == prompt.promptID }) else { + XCTFail("Prompt with ID: \(prompt.promptID) not found in the test data.") + return + } + + XCTAssertEqual(prompt.promptID, Int32(expected.promptID)) + XCTAssertEqual(prompt.text, expected.text) + XCTAssertEqual(prompt.attribution, expected.attribution) + XCTAssertEqual(prompt.date, expected.date) + XCTAssertEqual(prompt.answered, expected.answered) + XCTAssertEqual(prompt.answerCount, Int32(expected.answeredUsersCount)) + XCTAssertEqual(prompt.displayAvatarURLs.count, expected.answeredUserAvatarURLs.count) + } expectation.fulfill() @@ -96,9 +94,10 @@ final class BloggingPromptsServiceTests: CoreDataTestCase { wait(for: [expectation], timeout: timeout) } - func test_fetchPrompts_shouldExcludePromptsOutsideGivenDate() { + func test_fetchPrompts_shouldExcludePromptsOutsideGivenDate() throws { // this should exclude the second prompt dated 2021-09-12. - let dateParam = Self.dateFormatter.date(from: "2022-01-01") + // the remote may return multiple prompts, but there should be a client-side filtering for the prompt dates. + let dateParam = try XCTUnwrap(Self.dateFormatter.date(from: "2022-01-01")) // use actual remote object so the request can be intercepted by HTTPStubs. service = BloggingPromptsService(contextManager: contextManager, blog: blog) @@ -107,13 +106,10 @@ final class BloggingPromptsServiceTests: CoreDataTestCase { let expectation = expectation(description: "Fetch prompts should succeed") service.fetchPrompts(from: dateParam) { prompts in XCTAssertEqual(prompts.count, 1) + let prompt = prompts.first! - // Ensure that the date returned is more recent than the supplied date parameter. - let firstPrompt = prompts.first! - let firstDateComponents = Calendar.current.dateComponents(in: Self.utcTimeZone, from: firstPrompt.date) - XCTAssertEqual(firstDateComponents.year!, 2022) - XCTAssertEqual(firstDateComponents.month!, 5) - XCTAssertEqual(firstDateComponents.day!, 3) + // Ensure that the date returned is more recent than the date parameter. + XCTAssertTrue(dateParam.compare(prompt.date) == .orderedAscending) expectation.fulfill() @@ -127,7 +123,6 @@ final class BloggingPromptsServiceTests: CoreDataTestCase { func test_fetchPrompts_givenFailureResult_callsFailureBlock() { let expectation = expectation(description: "Fetch prompts should fail") - remote.shouldReturnSuccess = false service.fetchPrompts { _ in XCTFail("This closure shouldn't be called.") @@ -136,39 +131,233 @@ final class BloggingPromptsServiceTests: CoreDataTestCase { expectation.fulfill() } + api.failureBlockPassedIn?(NSError(code: 0, domain: "", description: ""), nil) + wait(for: [expectation], timeout: timeout) } - func test_fetchPrompts_givenNoParameters_assignsDefaultValue() { - let expectedDifferenceInHours = 10 * 24 // 10 days ago. + func test_fetchPrompts_givenNoParameters_assignsDefaultValue() throws { + let expectedDifferenceInDays = 10 let expectedNumber = 25 - remote.shouldReturnSuccess = false // call the fetch just to trigger default parameter assignment. the completion blocks can be ignored. service.fetchPrompts(success: { _ in }, failure: { _ in }) - XCTAssertNotNil(remote.passedDateParameter) - let passedDate = remote.passedDateParameter! - let differenceInHours = Calendar.current.dateComponents([.hour], from: passedDate, to: Date()).hour! - XCTAssertEqual(differenceInHours, expectedDifferenceInHours) + // calculate the difference and ensure that the passed date is 10 days ago. + let date = try passedDate() + let differenceInDays = try XCTUnwrap(Self.calendar.dateComponents([.day], from: date, to: Date()).day) + XCTAssertEqual(differenceInDays, expectedDifferenceInDays) - XCTAssertNotNil(remote.passedNumberParameter) - XCTAssertEqual(remote.passedNumberParameter!, expectedNumber) + // ensure that the passed number parameter is correct. + let numberParameter = try XCTUnwrap(passedNumber()) + XCTAssertEqual(numberParameter, expectedNumber) } - func test_fetchPrompts_givenValidParameters_passesThemToRemote() { - let expectedDate = BloggingPromptsServiceRemoteMock.dateFormatter.date(from: "2022-01-02")! + func test_fetchPrompts_passesTheDateCorrectly() throws { + // with the v3 implementation, we no longer have access to intercept at the method level. + // this means, we lose the time information of the passed date. + // In this case, we can only compare the year, month, and day components. + let expectedDate = try XCTUnwrap(BloggingPromptsServiceRemoteMock.dateFormatter.date(from: "2022-01-02")) + let expectedDateComponents = Self.calendar.dateComponents(in: Self.utcTimeZone, from: expectedDate) let expectedNumber = 10 - remote.shouldReturnSuccess = false // call the fetch just to trigger default parameter assignment. the completion blocks can be ignored. service.fetchPrompts(from: expectedDate, number: expectedNumber, success: { _ in }, failure: { _ in }) - XCTAssertNotNil(remote.passedDateParameter) - XCTAssertEqual(remote.passedDateParameter!, expectedDate) + // ensure that we compare the date components in UTC timezone to prevent possible day differences. + // e.g. edge cases such as `2023-01-02 22:00 -0500` or `2023-01-02 05:00 +0700`. + let date = try passedDate() + let dateComponents = Self.calendar.dateComponents(in: Self.utcTimeZone, from: date) + let year = try passedParameter("force_year") as? Int + XCTAssertEqual(year, expectedDateComponents.year) + + // FIXME: This needs to be addressed at the root but that requires more work than we have time for considering we want to support Xcode 15.1 ASAP. + // Tracked in https://github.com/wordpress-mobile/WordPress-iOS/issues/22323 + XCTExpectFailure("The date conversion may fail at times, likely due to an underlying time zone inconsistency") { + XCTAssertEqual(dateComponents.month, expectedDateComponents.month) + XCTAssertEqual(dateComponents.day, expectedDateComponents.day) + } + + let numberParameter = try XCTUnwrap(passedNumber()) + XCTAssertEqual(numberParameter, expectedNumber) + } + + // MARK: - Upsert Tests + + // new prompts should overwrite any existing prompts. + func test_fetchPrompt_shouldOverwritePromptsWithExistingDates() throws { + // use actual remote object so the request can be intercepted by HTTPStubs. + service = BloggingPromptsService(contextManager: contextManager, blog: blog) + stubFetchPromptsResponse() + + // the expected prompt IDs locally stored in the app after fetching the prompts. + // these IDs are from blogging-prompts-fetch-success.json. + let expectedPromptIDs = Set(testPrompts.map(\.promptID)) + + // insert existing prompts. + let date = try XCTUnwrap(Self.dateFormatter.date(from: "2022-05-03")) + makeBloggingPrompt(siteID: siteID, promptID: 1000, date: date) + contextManager.save(contextManager.mainContext) + + let expectation = expectation(description: "Fetch prompts should succeed") + service.fetchPrompts(from: .distantPast) { prompts in + // the existing prompt should have been overwritten. + XCTAssertEqual(prompts.count, expectedPromptIDs.count) + + let promptIDs = Set(prompts.map { Int($0.promptID) }) + XCTAssertTrue(promptIDs == expectedPromptIDs) + + expectation.fulfill() + + } failure: { error in + XCTFail("This closure shouldn't be called.") + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + } + + // there should only be one prompt per date. + func test_fetchPrompt_shouldDeleteExcessPromptsWithTheSameDates() throws { + // use actual remote object so the request can be intercepted by HTTPStubs. + service = BloggingPromptsService(contextManager: contextManager, blog: blog) + stubFetchPromptsResponse() + + // the expected prompt IDs locally stored in the app after fetching the prompts. + let expectedPromptIDs = Set(testPrompts.map(\.promptID)) + + // add 5 existing prompts having the same dates before calling `fetchPrompts`. + let date = try XCTUnwrap(Self.dateFormatter.date(from: "2022-05-03")) + for existingID in 1...5 { + makeBloggingPrompt(siteID: siteID, promptID: existingID, date: date) + } + contextManager.save(contextManager.mainContext) + + let expectation = expectation(description: "Fetch prompts should succeed") + service.fetchPrompts(from: .distantPast) { prompts in + // the existing prompts should be overwritten by the fetched prompts. + // additionally, any "excess" prompts should be deleted, leaving only one prompt per date. + XCTAssertEqual(prompts.count, expectedPromptIDs.count) + + let promptIDs = Set(prompts.map { Int($0.promptID) }) + XCTAssertTrue(promptIDs == expectedPromptIDs) + + expectation.fulfill() + + } failure: { error in + XCTFail("This closure shouldn't be called.") + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + } + + func test_fetchPrompt_shouldNotOverwritePromptsFromOtherSites() throws { + // use actual remote object so the request can be intercepted by HTTPStubs. + service = BloggingPromptsService(contextManager: contextManager, blog: blog) + stubFetchPromptsResponse() + + let otherPromptID = 1000 + let otherSiteID = 2 - XCTAssertNotNil(remote.passedNumberParameter) - XCTAssertEqual(remote.passedNumberParameter!, expectedNumber) + // the expected prompt IDs locally stored in the app after fetching the prompts. + // the first two IDs are from blogging-prompts-fetch-success.json. + let expectedPromptIDs = Set(testPrompts.map(\.promptID) + [otherPromptID]) + + // insert existing prompts. + let date = try XCTUnwrap(Self.dateFormatter.date(from: "2022-05-03")) + makeBloggingPrompt(siteID: otherSiteID, promptID: otherPromptID, date: date) + contextManager.save(contextManager.mainContext) + + let expectation = expectation(description: "Fetch prompts should succeed") + service.fetchPrompts(from: .distantPast) { _ in + // the prompt dated 2022-05-03 in siteID=2 shouldn't be overwritten. + let prompts = self.contextManager.mainContext.allObjects(ofType: BloggingPrompt.self) + XCTAssertEqual(prompts.count, expectedPromptIDs.count) + + let promptIDs = Set(prompts.map { Int($0.promptID) }) + XCTAssertTrue(promptIDs == expectedPromptIDs) + + expectation.fulfill() + + } failure: { error in + XCTFail("This closure shouldn't be called.") + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + } + + // with the force_year parameter, it's possible for the same month and day to share the same `promptID`. + // however, the `promptID` property is not unique or marked as primary key so duplicate IDs should be OK. + func test_fetchPrompt_shouldNotOverwriteExistingPromptFromLastYear() throws { + // use actual remote object so the request can be intercepted by HTTPStubs. + service = BloggingPromptsService(contextManager: contextManager, blog: blog) + stubFetchPromptsResponse() + + let date = try XCTUnwrap(Self.dateFormatter.date(from: "2021-05-03")) // one year before 2022-05-03. + let promptID = try XCTUnwrap(testPrompts.first).promptID // same promptID as 2022-05-03 from the test data. + + // insert existing prompts. + makeBloggingPrompt(siteID: siteID, promptID: promptID, date: date) + contextManager.save(contextManager.mainContext) + + let expectation = expectation(description: "Fetch prompts should succeed") + service.fetchPrompts(from: .distantPast) { prompts in + XCTAssertEqual(prompts.count, 3) + expectation.fulfill() + } failure: { error in + XCTFail("This closure shouldn't be called.") + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + } + + // MARK: Bloganuary Tests + + func test_fetchPrompt_shouldParseBloganuaryPromptsCorrectly() { + // use actual remote object so the request can be intercepted by HTTPStubs. + service = BloggingPromptsService(contextManager: contextManager, blog: blog) + testPrompts = loadTestPrompts(from: bloganuaryPromptsResponseFileName) + stubFetchPromptsResponse(with: bloganuaryPromptsResponseFileName) + + let expectation = expectation(description: "Fetch prompts should succeed") + service.fetchPrompts(from: .distantPast) { [testPrompts] prompts in + XCTAssertEqual(prompts.count, testPrompts.count) + + prompts.forEach { prompt in + guard let expected = testPrompts.first(where: { $0.promptID == prompt.promptID }) else { + XCTFail("Prompt with ID: \(prompt.promptID) not found in the test data.") + return + } + + // check for Bloganuary prompts. + if let bloganuaryId = expected.bloganuaryId, !bloganuaryId.isEmpty { + // the attribution should be added client-side. + XCTAssertEqual(prompt.attribution, "bloganuary") + XCTAssertNotNil(prompt.additionalPostTags) + + let tags = prompt.additionalPostTags! + XCTAssertTrue(tags.contains("bloganuary")) + XCTAssertTrue(tags.contains(bloganuaryId)) + + } else { + // otherwise, normal cards shouldn't have the bloganuary attributions. + // no additional tags should be added here. + XCTAssertNotEqual(prompt.attribution, "bloganuary") + XCTAssertTrue(prompt.additionalPostTags!.isEmpty) + } + } + + expectation.fulfill() + + } failure: { error in + XCTFail("This closure shouldn't be called.") + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) } } @@ -187,38 +376,79 @@ private extension BloggingPromptsServiceTests { } func makeBlog() -> Blog { - return BlogBuilder(mainContext).isHostedAtWPcom().build() + return BlogBuilder(mainContext).isHostedAtWPcom().with(blogID: siteID).build() } - func stubFetchPromptsResponse() { + func stubFetchPromptsResponse(with fileName: String? = nil) { stub(condition: isMethodGET()) { _ in - let stubPath = OHPathForFile("blogging-prompts-fetch-success.json", type(of: self)) + let stubPath = OHPathForFile("\(fileName ?? self.fetchPromptsResponseFileName).json", type(of: self)) return fixture(filePath: stubPath!, headers: ["Content-Type": "application/json"]) } } + + @discardableResult + func makeBloggingPrompt(siteID: Int, promptID: Int, date: Date) -> BloggingPrompt { + let prompt = BloggingPrompt.newObject(in: contextManager.mainContext)! + prompt.siteID = Int32(siteID) + prompt.promptID = Int32(promptID) + prompt.date = date + + return prompt + } + + // MARK: Mock WordPressComRestAPI Helper + + func passedParameter(_ key: String) throws -> AnyHashable { + let passedParameters = try XCTUnwrap(api.parametersPassedIn as? [String: AnyHashable]) + return try XCTUnwrap(passedParameters[key], "Param not found for \(key)") + } + + func passedNumber() throws -> Int? { + return try passedParameter("per_page") as? Int + } + + func passedDate() throws -> Date { + // assumes that `ignoresYear` parameter is enabled. + // get the month and day components. + let dateString = try XCTUnwrap(passedParameter("after") as? String) + let components = dateString.split(separator: "-").compactMap { $0 } + XCTAssertEqual(components.count, 2) + + // build a date based on the passed values. + let forcedYear = try passedParameter("force_year") as? Int + let month = try XCTUnwrap(Int(components.first ?? "")) + let day = try XCTUnwrap(Int(components.last ?? "")) + + var dateComponents = DateComponents() + dateComponents.year = forcedYear + dateComponents.month = month + dateComponents.day = day + dateComponents.hour = 0 + dateComponents.minute = 0 + dateComponents.second = 0 + + return try XCTUnwrap(Self.calendar.date(from: dateComponents)) + } + + // MARK: Test Prompts + + private func loadTestPrompts(from fileName: String) -> [BloggingPromptRemoteObject] { + let bundle = Bundle(for: BloggingPromptsServiceTests.self) + guard let url = bundle.url(forResource: fileName, withExtension: "json"), + let data = try? Data(contentsOf: url), + let prompts = try? Self.jsonDecoder.decode([BloggingPromptRemoteObject].self, from: data) else { + return [] + } + return prompts + } } +// Let's keep this mock class in case we want to add additional tests for Blogging Prompt Settings. class BloggingPromptsServiceRemoteMock: BloggingPromptsServiceRemote { var passedSiteID: NSNumber? = nil var passedNumberParameter: Int? = nil var passedDateParameter: Date? = nil var shouldReturnSuccess: Bool = true - var promptsToReturn = [RemoteBloggingPrompt]() - - override func fetchPrompts(for siteID: NSNumber, - number: Int? = nil, - fromDate: Date? = nil, - completion: @escaping (Result<[RemoteBloggingPrompt], Error>) -> Void) { - passedSiteID = siteID - passedNumberParameter = number - passedDateParameter = fromDate - - if shouldReturnSuccess { - completion(.success(promptsToReturn)) - } else { - completion(.failure(Errors.failed)) - } - } enum Errors: Error { case failed @@ -228,6 +458,7 @@ class BloggingPromptsServiceRemoteMock: BloggingPromptsServiceRemote { let formatter = DateFormatter() formatter.locale = .init(identifier: "en_US_POSIX") formatter.dateFormat = "yyyy-MM-dd" + formatter.timeZone = TimeZone(abbreviation: "UTC")! return formatter }() diff --git a/WordPress/WordPressTest/Classes/Stores/UserPersistentStoreTests.swift b/WordPress/WordPressTest/Classes/Stores/UserPersistentStoreTests.swift deleted file mode 100644 index 11edef353d19..000000000000 --- a/WordPress/WordPressTest/Classes/Stores/UserPersistentStoreTests.swift +++ /dev/null @@ -1,184 +0,0 @@ -import XCTest -@testable import WordPress - -final class UserPersistentStoreTests: XCTestCase { - private static let mockSuiteName = "user.persistent.store.tests" - private let sut = UserPersistentStore(defaultsSuiteName: mockSuiteName) - - // From the `UserDefaults(suiteName:)` docs: - // "The globalDomain is also an invalid suite name, because it isn't writeable by apps." - // Therefore it should return nil. - func testInitFailsWhenGlobalDomainIsUsed() { - XCTAssertNil(UserPersistentStore(defaultsSuiteName: UserDefaults.globalDomain)) - } - - func testBoolReturnsStandardValueWhenTrueAndSuiteNotSet() { - guard let sut = sut else { - XCTFail("No `sut` found") - return - } - - let key = #function - - UserDefaults.standard.set(true, forKey: key) - XCTAssert(sut.bool(forKey: key)) - } - - func testBoolReturnsFalseWhenStandardValueIsTrueAndSuiteSetToFalse() { - guard let sut = sut else { - XCTFail("No `sut` found") - return - } - - let key = #function - - UserDefaults.standard.set(true, forKey: key) - sut.set(false, forKey: key) - XCTAssertFalse(sut.bool(forKey: key)) - } - - func testBoolReturnsTrueWhenStandardValueIsFalseAndSuiteSetToTrue() { - guard let sut = sut else { - XCTFail("No `sut` found") - return - } - - let key = #function - - UserDefaults.standard.set(false, forKey: key) - sut.set(true, forKey: key) - XCTAssert(sut.bool(forKey: key)) - } - - func testSetIntUpdatesDefaults() { - let key = #function - sut?.set(1987, forKey: key) - - guard let userDefaults = UserDefaults(suiteName: Self.mockSuiteName) else { - XCTFail("Initialization with \(Self.mockSuiteName) failed.") - return - } - - XCTAssertEqual(userDefaults.integer(forKey: key), 1987) - userDefaults.removeObject(forKey: key) - sut?.removeObject(forKey: key) - } - - func testSetIntInvalidatesStandardValue() { - guard let sut = sut else { - XCTFail("No `sut` found") - return - } - - let key = #function - let testValue: Int = 991 - UserDefaults.standard.set(testValue, forKey: key) - sut.set(testValue, forKey: key) - - XCTAssertEqual(sut.integer(forKey: key), testValue) - XCTAssertEqual(UserDefaults.standard.double(forKey: key), 0) - sut.removeObject(forKey: key) - } - - func testSetFloatUpdatesDefaults() { - let key = #function - let testValue: Float = 7.2 - sut?.set(testValue, forKey: key) - - guard let userDefaults = UserDefaults(suiteName: Self.mockSuiteName) else { - XCTFail("Initialization with \(Self.mockSuiteName) failed.") - return - } - - XCTAssertEqual(userDefaults.float(forKey: key), testValue) - sut?.removeObject(forKey: key) - } - - func testSetFloatInvalidatesStandardValue() { - guard let sut = sut else { - XCTFail("No `sut` found") - return - } - - let key = #function - let testValue: Float = 15.23 - UserDefaults.standard.set(testValue, forKey: key) - sut.set(testValue, forKey: key) - - XCTAssertEqual(sut.float(forKey: key), testValue) - XCTAssertEqual(UserDefaults.standard.double(forKey: key), 0) - sut.removeObject(forKey: key) - } - - func testSetDoubleUpdatesDefaults() { - let key = #function - let testValue: Double = 18.82732 - sut?.set(testValue, forKey: key) - - guard let userDefaults = UserDefaults(suiteName: Self.mockSuiteName) else { - XCTFail("Initialization with \(Self.mockSuiteName) failed.") - return - } - - XCTAssertEqual(userDefaults.double(forKey: key), testValue) - sut?.removeObject(forKey: key) - } - - func testSetDoubleInvalidatesStandardValue() { - guard let sut = sut else { - XCTFail("No `sut` found") - return - } - - let key = #function - let testValue: Double = 18.82732 - UserDefaults.standard.set(testValue, forKey: key) - sut.set(testValue, forKey: key) - - XCTAssertEqual(sut.double(forKey: key), testValue) - XCTAssertEqual(UserDefaults.standard.double(forKey: key), 0) - sut.removeObject(forKey: key) - } - - func testSetBoolUpdatesDefaults() { - let key = #function - sut?.set(true, forKey: key) - - guard let userDefaults = UserDefaults(suiteName: Self.mockSuiteName) else { - XCTFail("Initialization with \(Self.mockSuiteName) failed.") - return - } - - XCTAssert(userDefaults.bool(forKey: key)) - sut?.removeObject(forKey: key) - } - - func testSetBoolInvalidatesStandardValue() { - guard let sut = sut else { - XCTFail("No `sut` found") - return - } - - let key = #function - UserDefaults.standard.set(true, forKey: key) - - sut.set(true, forKey: key) - - XCTAssert(sut.bool(forKey: key)) - sut.removeObject(forKey: key) - } - - func testSetURLUpdatesDefaults() { - let key = #function - let url = URL(string: "https://wordpress.com")! - sut?.set(url, forKey: key) - - guard let userDefaults = UserDefaults(suiteName: Self.mockSuiteName) else { - XCTFail("Initialization with \(Self.mockSuiteName) failed.") - return - } - - XCTAssertEqual(userDefaults.url(forKey: key), url) - sut?.removeObject(forKey: key) - } -} diff --git a/WordPress/WordPressTest/Dashboard/BlogDashboardServiceTests.swift b/WordPress/WordPressTest/Dashboard/BlogDashboardServiceTests.swift index 947de92be926..fa82e6b4eea2 100644 --- a/WordPress/WordPressTest/Dashboard/BlogDashboardServiceTests.swift +++ b/WordPress/WordPressTest/Dashboard/BlogDashboardServiceTests.swift @@ -11,6 +11,7 @@ class BlogDashboardServiceTests: CoreDataTestCase { private var persistenceMock: BlogDashboardPersistenceMock! private var repositoryMock: InMemoryUserDefaults! private var postsParserMock: BlogDashboardPostsParserMock! + private var remoteFeatureFlagStore: RemoteFeatureFlagStoreMock! private let featureFlags = FeatureFlagOverrideStore() private let wpComID = 123456 @@ -18,24 +19,55 @@ class BlogDashboardServiceTests: CoreDataTestCase { override func setUp() { super.setUp() + // Simulate the app is signed in with a WP.com account + contextManager.useAsSharedInstance(untilTestFinished: self) + let accountService = AccountService(coreDataStack: contextManager) + _ = accountService.createOrUpdateAccount(withUsername: "username", authToken: "token") + remoteServiceMock = DashboardServiceRemoteMock() persistenceMock = BlogDashboardPersistenceMock() repositoryMock = InMemoryUserDefaults() postsParserMock = BlogDashboardPostsParserMock(managedObjectContext: mainContext) - service = BlogDashboardService(managedObjectContext: mainContext, remoteService: remoteServiceMock, persistence: persistenceMock, repository: repositoryMock, postsParser: postsParserMock) - - try? featureFlags.override(FeatureFlag.personalizeHomeTab, withValue: true) + remoteFeatureFlagStore = RemoteFeatureFlagStoreMock() + service = BlogDashboardService( + managedObjectContext: mainContext, + // Notice these three boolean make the test run as if the app was Jetpack. + // + // What would be the additional effor to test the remaining 5 configurations? + // Is there something we can do to reduce the combinatorial space? + // + // See also https://github.com/wordpress-mobile/WordPress-iOS/pull/21740 + isJetpack: true, + isDotComAvailable: true, + shouldShowJetpackFeatures: true, + remoteService: remoteServiceMock, + persistence: persistenceMock, + repository: repositoryMock, + postsParser: postsParserMock, + remoteFeatureFlagStore: remoteFeatureFlagStore + ) + + // The state of the world these tests assume relies on certain feature flags. + // + // Similarly to the isJetpack, isDotComAvailable, etc above, it would be ideal to inject these at call site to: + // 1. Make the dependency on that bit of information explicit + // 2. Allow for testing all combinations + // + // At the time of writing, the priority was getting some tests for new code to pass under the important Jetpac user path. + // As such, here are a bunch of global-state feature flags overrides. try? featureFlags.override(RemoteFeatureFlag.activityLogDashboardCard, withValue: true) try? featureFlags.override(RemoteFeatureFlag.pagesDashboardCard, withValue: true) + try? featureFlags.override(FeatureFlag.googleDomainsCard, withValue: false) + try? featureFlags.override(RemoteFeatureFlag.dynamicDashboardCards, withValue: true) } override func tearDown() { super.tearDown() context = nil - try? featureFlags.override(FeatureFlag.personalizeHomeTab, withValue: FeatureFlag.personalizeHomeTab.originalValue) try? featureFlags.override(RemoteFeatureFlag.activityLogDashboardCard, withValue: RemoteFeatureFlag.activityLogDashboardCard.originalValue) try? featureFlags.override(RemoteFeatureFlag.pagesDashboardCard, withValue: RemoteFeatureFlag.pagesDashboardCard.originalValue) + try? featureFlags.override(FeatureFlag.googleDomainsCard, withValue: FeatureFlag.googleDomainsCard.originalValue) } func testCallServiceWithCorrectIDAndCards() { @@ -45,7 +77,7 @@ class BlogDashboardServiceTests: CoreDataTestCase { service.fetch(blog: blog) { _ in XCTAssertEqual(self.remoteServiceMock.didCallWithBlogID, self.wpComID) - XCTAssertEqual(self.remoteServiceMock.didRequestCards, ["todays_stats", "posts", "pages", "activity"]) + XCTAssertEqual(self.remoteServiceMock.didRequestCards, ["todays_stats", "posts", "pages", "activity", "dynamic"]) expect.fulfill() } @@ -59,8 +91,8 @@ class BlogDashboardServiceTests: CoreDataTestCase { let blog = newTestBlog(id: wpComID, context: mainContext) service.fetch(blog: blog) { cards in - let draftPostsCardItem = cards.first(where: {$0.cardType == .draftPosts}) - let scheduledPostsCardItem = cards.first(where: {$0.cardType == .scheduledPosts}) + let draftPostsCardItem = cards.first(where: { $0.cardType == .draftPosts })?.normal() + let scheduledPostsCardItem = cards.first(where: { $0.cardType == .scheduledPosts })?.normal() // Posts section exists XCTAssertNotNil(draftPostsCardItem) @@ -96,7 +128,7 @@ class BlogDashboardServiceTests: CoreDataTestCase { let blog = newTestBlog(id: wpComID, context: mainContext) service.fetch(blog: blog) { cards in - let pagesCardItem = cards.first(where: {$0.cardType == .pages}) + let pagesCardItem = cards.first(where: { $0.cardType == .pages })?.normal() // Pages section exists XCTAssertNotNil(pagesCardItem) @@ -113,16 +145,38 @@ class BlogDashboardServiceTests: CoreDataTestCase { func testActivityLog() { let expect = expectation(description: "Parse activities") + // Will fail with logged in user. + // + // It happens because for some reason the logic that should add activity as one of the type of cards to fetch doesn't do that. let blog = newTestBlog(id: wpComID, context: mainContext) service.fetch(blog: blog) { cards in - let activityCardItem = cards.first(where: {$0.cardType == .activityLog}) + guard let activityCardItem = cards.first(where: { $0.cardType == .activityLog })?.normal() else { + return XCTFail("Unexpectedly found nil Optional") + } + + guard let apiResponse = activityCardItem.apiResponse else { + return XCTFail("Unexpectedly found nil Optional") + } + + guard let activity = apiResponse.activity else { + return XCTFail("Unexpectedly found nil Optional") + } - // Activity section exists - XCTAssertNotNil(activityCardItem) + guard let value = activity.value else { + return XCTFail("Unexpectedly found nil Optional") + } + + guard let current = value.current else { + return XCTFail("Unexpectedly found nil Optional") + } + + guard let orderedItems = current.orderedItems else { + return XCTFail("Unexpectedly found nil Optional") + } // 2 activity items - XCTAssertEqual(activityCardItem!.apiResponse!.activity!.value!.current!.orderedItems!.count, 2) + XCTAssertEqual(orderedItems.count, 2) expect.fulfill() } @@ -137,7 +191,7 @@ class BlogDashboardServiceTests: CoreDataTestCase { let blog = newTestBlog(id: wpComID, context: mainContext) service.fetch(blog: blog) { cards in - let todaysStatsItem = cards.first(where: {$0.cardType == .todaysStats}) + let todaysStatsItem = cards.first(where: { $0.cardType == .todaysStats })?.normal() // Todays stats section exists XCTAssertNotNil(todaysStatsItem) @@ -357,6 +411,103 @@ class BlogDashboardServiceTests: CoreDataTestCase { waitForExpectations(timeout: 3, handler: nil) } + // MARK: - Dynamic Cards + + func testCardsPresenceWhenAllCardsFeatureFlagsAreEnabled() throws { + let expect = expectation(description: "2 dynamic cards at the top and one at the bottom should be present") + remoteServiceMock.respondWith = .withMultipleDynamicCards + remoteFeatureFlagStore.enabledFeatureFlags = ["feature_flag_12345", "feature_flag_67890", "feature_flag_13579"] + + let blog = newTestBlog(id: wpComID, context: mainContext) + + service.fetch(blog: blog) { cards in + XCTAssertEqual(cards[0].dynamic()?.payload.id, "id_12345") + XCTAssertEqual(cards[1].dynamic()?.payload.id, "id_67890") + XCTAssertEqual(cards[cards.endIndex - 2].dynamic()?.payload.id, "id_13579") + expect.fulfill() + } + + waitForExpectations(timeout: 3, handler: nil) + } + + func testCardsPresenceWhenSomeCardsFeatureFlagsAreEnabled() throws { + let expect = expectation(description: "2 dynamic cards at the top and one at the bottom should be present") + remoteServiceMock.respondWith = .withMultipleDynamicCards + remoteFeatureFlagStore.enabledFeatureFlags = ["feature_flag_12345"] + remoteFeatureFlagStore.disabledFeatureFlag = ["feature_flag_67890"] + + let blog = newTestBlog(id: wpComID, context: mainContext) + + service.fetch(blog: blog) { cards in + let numberOfDynamicCards = cards.compactMap { $0.dynamic() }.count + XCTAssertEqual(numberOfDynamicCards, 1) + XCTAssertEqual(cards[0].dynamic()?.payload.id, "id_12345") + expect.fulfill() + } + + waitForExpectations(timeout: 3, handler: nil) + } + + func testCardsAbsenceWhenRemoteFeatureFlagIsDisabled() throws { + let expect = expectation(description: "No dynamic card should be present") + remoteServiceMock.respondWith = .withMultipleDynamicCards + remoteFeatureFlagStore.enabledFeatureFlags = ["feature_flag_12345", "feature_flag_67890", "feature_flag_13579"] + try featureFlags.override(RemoteFeatureFlag.dynamicDashboardCards, withValue: false) + + let blog = newTestBlog(id: wpComID, context: mainContext) + + service.fetch(blog: blog) { cards in + let dynamicCards = cards.compactMap { $0.dynamic() } + XCTAssertTrue(dynamicCards.isEmpty) + expect.fulfill() + } + + waitForExpectations(timeout: 3, handler: nil) + } + + func testDecodingWithDynamicCards() throws { + let expect = expectation(description: "Dynamic card should be successfully decoded") + remoteServiceMock.respondWith = .withOnlyOneDynamicCard + remoteFeatureFlagStore.enabledFeatureFlags = ["feature_flag_12345"] + try featureFlags.override(RemoteFeatureFlag.dynamicDashboardCards, withValue: true) + + let blog = newTestBlog(id: wpComID, context: mainContext) + + service.fetch(blog: blog) { cards in + do { + let card = try XCTUnwrap(cards.first?.dynamic()) + let payload = card.payload + let expected = BlogDashboardRemoteEntity.BlogDashboardDynamic( + id: "id_12345", + remoteFeatureFlag: "feature_flag_12345", + title: "Title 12345", + featuredImage: "https://example.com/image12345", + url: "https://example.com/url12345", + action: "Action 12345", + order: .top, + rows: [ + .init( + title: "Row Title 1", + description: nil, + icon: "https://example.com/icon12345" + ), + .init( + title: "Row Title 2", + description: "Row Description 2", + icon: nil + ) + ] + ) + XCTAssertEqual(payload, expected) + } catch { + XCTFail(error.localizedDescription) + } + expect.fulfill() + } + + waitForExpectations(timeout: 3, handler: nil) + } + // MARK: - Local Pages // TODO: Add test to check that local pages are considered if no pages are returned from the endpoint @@ -369,6 +520,7 @@ class BlogDashboardServiceTests: CoreDataTestCase { private func newTestBlog(id: Int, context: NSManagedObjectContext, isAdmin: Bool = true) -> Blog { let blog = ModelTestHelper.insertDotComBlog(context: mainContext) + blog.account = try! WPAccount.lookupDefaultWordPressComAccount(in: context) blog.dotComID = id as NSNumber blog.isAdmin = isAdmin return blog @@ -379,6 +531,8 @@ class BlogDashboardServiceTests: CoreDataTestCase { class DashboardServiceRemoteMock: DashboardServiceRemote { enum Response: String { + case withOnlyOneDynamicCard = "dashboard-200-with-only-one-dynamic-card.json" + case withMultipleDynamicCards = "dashboard-200-with-multiple-dynamic-cards.json" case withDraftAndSchedulePosts = "dashboard-200-with-drafts-and-scheduled.json" case withDraftsOnly = "dashboard-200-with-drafts-only.json" case withoutPosts = "dashboard-200-without-posts.json" diff --git a/WordPress/WordPressTest/Dashboard/DashboardCardTests.swift b/WordPress/WordPressTest/Dashboard/DashboardCardTests.swift index df010e23e4ee..ef119e291c1c 100644 --- a/WordPress/WordPressTest/Dashboard/DashboardCardTests.swift +++ b/WordPress/WordPressTest/Dashboard/DashboardCardTests.swift @@ -248,7 +248,7 @@ class DashboardCardTests: CoreDataTestCase { let identifiers = DashboardCard.RemoteDashboardCard.allCases.map { $0.rawValue } // Then - XCTAssertEqual(identifiers, ["todays_stats", "posts", "pages", "activity"]) + XCTAssertEqual(identifiers, ["todays_stats", "posts", "pages", "activity", "dynamic"]) } // MARK: Helpers diff --git a/WordPress/WordPressTest/Dashboard/DashboardPostsSyncManagerTests.swift b/WordPress/WordPressTest/Dashboard/DashboardPostsSyncManagerTests.swift index 737bc582859e..748e922eede0 100644 --- a/WordPress/WordPressTest/Dashboard/DashboardPostsSyncManagerTests.swift +++ b/WordPress/WordPressTest/Dashboard/DashboardPostsSyncManagerTests.swift @@ -4,18 +4,28 @@ import XCTest class DashboardPostsSyncManagerTests: CoreDataTestCase { private var blog: Blog! + private var blogID: TaggedManagedObjectID! private let draftStatuses: [BasePost.Status] = [.draft, .pending] private let scheduledStatuses: [BasePost.Status] = [.scheduled] - private var postService: PostServiceMock! + private var postRepository: PostRepository! private var blogService: BlogServiceMock! - override func setUp() { - super.setUp() - contextManager.useAsSharedInstance(untilTestFinished: self) - blog = BlogBuilder(contextManager.mainContext).build() - blog.dashboardState.postsSyncingStatuses = [] - blog.dashboardState.pagesSyncingStatuses = [] - postService = PostServiceMock() + override func setUp() async throws { + try await super.setUp() + + let account = AccountService(coreDataStack: contextManager).createOrUpdateAccount(withUsername: "username", authToken: "token") + blogID = try await contextManager.performAndSave { + let blog = try BlogBuilder($0).withAccount(id: account).with(dotComID: 42).build() + blog.dashboardState.postsSyncingStatuses = [] + blog.dashboardState.pagesSyncingStatuses = [] + return TaggedManagedObjectID(blog) + } + + try await mainContext.perform { + self.blog = try self.mainContext.existingObject(with: self.blogID) + } + + postRepository = PostRepository(coreDataStack: contextManager) blogService = BlogServiceMock(coreDataStack: contextManager) } @@ -26,62 +36,57 @@ class DashboardPostsSyncManagerTests: CoreDataTestCase { func testSuccessfulPostsSync() { // Given - let postsToReturn = [PostBuilder(contextManager.mainContext).build()] - postService.syncShouldSucceed = true - postService.returnSyncedPosts = postsToReturn + stubGetPostsList(type: "post", total: 50) - let manager = DashboardPostsSyncManager(postService: postService, blogService: blogService) + let manager = DashboardPostsSyncManager(postRepository: postRepository, blogService: blogService) let listener = SyncManagerListenerMock() manager.addListener(listener) // When - manager.syncPosts(blog: blog, postType: .post, statuses: draftStatuses.strings) + manager.syncPosts(blog: blog, postType: .post, statuses: draftStatuses) + wait(for: [expectation(that: \.postsSyncedCalled, on: listener, willEqual: true)], timeout: 0.1) // Then XCTAssertTrue(listener.postsSyncedCalled) XCTAssertTrue(listener.postsSyncSuccess ?? false) XCTAssertEqual(listener.postsSyncBlog, blog) XCTAssertEqual(listener.postsSyncType, .post) - XCTAssertEqual(listener.postsSynced, postsToReturn) XCTAssertEqual(blog.dashboardState.postsSyncingStatuses, []) - XCTAssertTrue(postService.syncPostsCalled) XCTAssertFalse(blogService.syncAuthorsCalled) } func testSuccessfulPagesSync() { // Given - let postsToReturn = [PostBuilder(contextManager.mainContext).build()] - postService.syncShouldSucceed = true - postService.returnSyncedPosts = postsToReturn + stubGetPostsList(type: "page", total: 50) - let manager = DashboardPostsSyncManager(postService: postService, blogService: blogService) + let manager = DashboardPostsSyncManager(postRepository: postRepository, blogService: blogService) let listener = SyncManagerListenerMock() manager.addListener(listener) // When - manager.syncPosts(blog: blog, postType: .page, statuses: draftStatuses.strings) + manager.syncPosts(blog: blog, postType: .page, statuses: draftStatuses) + wait(for: [expectation(that: \.postsSyncedCalled, on: listener, willEqual: true)], timeout: 0.1) // Then XCTAssertTrue(listener.postsSyncedCalled) XCTAssertTrue(listener.postsSyncSuccess ?? false) XCTAssertEqual(listener.postsSyncBlog, blog) XCTAssertEqual(listener.postsSyncType, .page) - XCTAssertEqual(listener.postsSynced, postsToReturn) XCTAssertEqual(blog.dashboardState.pagesSyncingStatuses, []) - XCTAssertTrue(postService.syncPostsCalled) XCTAssertFalse(blogService.syncAuthorsCalled) } func testFailingPostsSync() { // Given - postService.syncShouldSucceed = false + stubGetPostsListWithServerError() - let manager = DashboardPostsSyncManager(postService: postService, blogService: blogService) + let manager = DashboardPostsSyncManager(postRepository: postRepository, blogService: blogService) let listener = SyncManagerListenerMock() manager.addListener(listener) // When - manager.syncPosts(blog: blog, postType: .post, statuses: draftStatuses.strings) + manager.syncPosts(blog: blog, postType: .post, statuses: draftStatuses) + wait(for: [expectation(that: \.postsSyncedCalled, on: listener, willEqual: true)], timeout: 0.1) // Then XCTAssertTrue(listener.postsSyncedCalled) @@ -89,66 +94,63 @@ class DashboardPostsSyncManagerTests: CoreDataTestCase { XCTAssertEqual(listener.postsSyncBlog, blog) XCTAssertEqual(listener.postsSyncType, .post) XCTAssertEqual(blog.dashboardState.postsSyncingStatuses, []) - XCTAssertTrue(postService.syncPostsCalled) XCTAssertFalse(blogService.syncAuthorsCalled) } func testNotSyncingIfAnotherSyncinProgress() { // Given - postService.syncShouldSucceed = false - blog.dashboardState.postsSyncingStatuses = draftStatuses.strings + blog.dashboardState.postsSyncingStatuses = draftStatuses - let manager = DashboardPostsSyncManager(postService: postService, blogService: blogService) + let manager = DashboardPostsSyncManager(postRepository: postRepository, blogService: blogService) let listener = SyncManagerListenerMock() manager.addListener(listener) // When - manager.syncPosts(blog: blog, postType: .post, statuses: draftStatuses.strings) - + manager.syncPosts(blog: blog, postType: .post, statuses: draftStatuses) // Then XCTAssertFalse(listener.postsSyncedCalled) - XCTAssertFalse(postService.syncPostsCalled) XCTAssertFalse(blogService.syncAuthorsCalled) } func testSyncingPostsIfSomeStatusesAreNotBeingSynced() { // Given - postService.syncShouldSucceed = false - blog.dashboardState.postsSyncingStatuses = draftStatuses.strings + stubGetPostsListWithServerError() + blog.dashboardState.postsSyncingStatuses = draftStatuses - let manager = DashboardPostsSyncManager(postService: postService, blogService: blogService) + let manager = DashboardPostsSyncManager(postRepository: postRepository, blogService: blogService) let listener = SyncManagerListenerMock() manager.addListener(listener) // When - let toBeSynced = draftStatuses.strings + scheduledStatuses.strings + let toBeSynced = draftStatuses + scheduledStatuses manager.syncPosts(blog: blog, postType: .post, statuses: toBeSynced) + wait(for: [expectation(that: \.postsSyncedCalled, on: listener, willEqual: true)], timeout: 0.1) // Then XCTAssertTrue(listener.postsSyncedCalled) XCTAssertFalse(listener.postsSyncSuccess ?? false) XCTAssertEqual(listener.postsSyncBlog, blog) XCTAssertEqual(listener.postsSyncType, .post) - XCTAssertEqual(listener.statusesSynced, scheduledStatuses.strings) - XCTAssertEqual(blog.dashboardState.postsSyncingStatuses, draftStatuses.strings) - XCTAssertTrue(postService.syncPostsCalled) + XCTAssertEqual(listener.statusesSynced, scheduledStatuses) + XCTAssertEqual(blog.dashboardState.postsSyncingStatuses, draftStatuses) XCTAssertFalse(blogService.syncAuthorsCalled) } func testSuccessfulSyncAfterAuthorsSync() { // Given - postService.syncShouldSucceed = true + stubGetPostsList(type: "post", total: 50) blogService.syncShouldSucceed = true blog.userID = nil blog.isAdmin = true - let manager = DashboardPostsSyncManager(postService: postService, blogService: blogService) + let manager = DashboardPostsSyncManager(postRepository: postRepository, blogService: blogService) let listener = SyncManagerListenerMock() manager.addListener(listener) // When - manager.syncPosts(blog: blog, postType: .post, statuses: draftStatuses.strings) + manager.syncPosts(blog: blog, postType: .post, statuses: draftStatuses) + wait(for: [expectation(that: \.postsSyncedCalled, on: listener, willEqual: true)], timeout: 0.1) // Then XCTAssertTrue(listener.postsSyncedCalled) @@ -157,22 +159,22 @@ class DashboardPostsSyncManagerTests: CoreDataTestCase { XCTAssertEqual(listener.postsSyncType, .post) XCTAssertEqual(blog.dashboardState.postsSyncingStatuses, []) XCTAssertTrue(blogService.syncAuthorsCalled) - XCTAssertTrue(postService.syncPostsCalled) } func testFailingAuthorsSync() { // Given - postService.syncShouldSucceed = true + stubGetPostsList(type: "post", total: 50) blogService.syncShouldSucceed = false blog.userID = nil blog.isAdmin = true - let manager = DashboardPostsSyncManager(postService: postService, blogService: blogService) + let manager = DashboardPostsSyncManager(postRepository: postRepository, blogService: blogService) let listener = SyncManagerListenerMock() manager.addListener(listener) // When - manager.syncPosts(blog: blog, postType: .post, statuses: draftStatuses.strings) + manager.syncPosts(blog: blog, postType: .post, statuses: draftStatuses) + wait(for: [expectation(that: \.postsSyncedCalled, on: listener, willEqual: true)], timeout: 0.1) // Then XCTAssertTrue(listener.postsSyncedCalled) @@ -181,30 +183,26 @@ class DashboardPostsSyncManagerTests: CoreDataTestCase { XCTAssertEqual(listener.postsSyncType, .post) XCTAssertEqual(blog.dashboardState.postsSyncingStatuses, []) XCTAssertTrue(blogService.syncAuthorsCalled) - XCTAssertFalse(postService.syncPostsCalled) } } -class SyncManagerListenerMock: DashboardPostsSyncManagerListener { +private class SyncManagerListenerMock: NSObject, DashboardPostsSyncManagerListener { - var postsSyncedCalled = false - var postsSyncSuccess: Bool? - var postsSyncBlog: Blog? - var postsSyncType: DashboardPostsSyncManager.PostType? - var postsSynced: [AbstractPost]? - var statusesSynced: [String]? + @objc dynamic private(set) var postsSyncedCalled = false + private(set) var postsSyncSuccess: Bool? + private(set) var postsSyncBlog: Blog? + private(set) var postsSyncType: DashboardPostsSyncManager.PostType? + private(set) var statusesSynced: [BasePost.Status]? func postsSynced(success: Bool, blog: Blog, postType: DashboardPostsSyncManager.PostType, - posts: [AbstractPost]?, - for statuses: [String]) { + for statuses: [BasePost.Status]) { self.postsSyncedCalled = true self.postsSyncSuccess = success self.postsSyncBlog = blog self.postsSyncType = postType - self.postsSynced = posts self.statusesSynced = statuses } } diff --git a/WordPress/WordPressTest/Dashboard/Dynamic Cards/BlogDashboardDynamicCardCoordinatorTests.swift b/WordPress/WordPressTest/Dashboard/Dynamic Cards/BlogDashboardDynamicCardCoordinatorTests.swift new file mode 100644 index 000000000000..8895ac9aadb6 --- /dev/null +++ b/WordPress/WordPressTest/Dashboard/Dynamic Cards/BlogDashboardDynamicCardCoordinatorTests.swift @@ -0,0 +1,174 @@ +import Nimble +@testable import WordPress +import XCTest + +final class BlogDashboardDynamicCardCoordinatorTests: XCTestCase { + + override func setUp() { + super.setUp() + + // Because AnalyticsEventTrackingSpy logs events in a static var, we need to reset it between tests + AnalyticsEventTrackingSpy.reset() + } + + func test_trackAnalyticEventWhenDidAppearIsCalled() { + // Given + let id = "123" + let event = DashboardDynamicCardAnalyticsEvent.cardShown(id: id) + let coordinator = makeCoordinator(id: id) + + // When + coordinator.didAppear() + coordinator.didAppear() + coordinator.didAppear() + + // Then + expect(AnalyticsEventTrackingSpy.trackedEvents).to(haveCount(1)) + expect(AnalyticsEventTrackingSpy.trackedEvents).to(containElementSatisfying({ firedEvent in + firedEvent.name == event.name && firedEvent.properties == event.properties + })) + } + + func test_trackAnalyticEventWhenDidTapCardIsCalled() { + // Given + let (id, url) = ("123", "https://wordpress.com") + let event = DashboardDynamicCardAnalyticsEvent.cardTapped(id: id, url: url) + let coordinator = makeCoordinator(id: id, url: url) + + // When + coordinator.didTapCard() + coordinator.didTapCard() + coordinator.didTapCard() + + // Then + expect(AnalyticsEventTrackingSpy.trackedEvents).to(haveCount(3)) + expect(AnalyticsEventTrackingSpy.trackedEvents).to(containElementSatisfying({ firedEvent in + firedEvent.name == event.name && firedEvent.properties == event.properties + })) + } + + func test_trackAnalyticEventWhenDidTapCardCtaIsCalled() { + // Given + let (id, url) = ("123", "https://wordpress.com") + let event = DashboardDynamicCardAnalyticsEvent.cardCtaTapped(id: id, url: url) + let coordinator = makeCoordinator(id: id, url: url) + + // When + coordinator.didTapCardCTA() + coordinator.didTapCardCTA() + coordinator.didTapCardCTA() + + // Then + expect(AnalyticsEventTrackingSpy.trackedEvents).to(haveCount(3)) + expect(AnalyticsEventTrackingSpy.trackedEvents).to(containElementSatisfying({ firedEvent in + firedEvent.name == event.name && firedEvent.properties == event.properties + })) + } + + func test_didTapCardInvokesLinkRouterWhenURLMatches() { + // Given + let (id, url) = ("123", "https://wordpress.com") + var mockRouter = MockRouter(routes: []) + let expectation = expectation(description: "LinkRouter must invoke completion") + var isLinkInvoked = false + + mockRouter.completion = { _, _ in + isLinkInvoked = true + expectation.fulfill() + } + let coordinator = makeCoordinator(id: id, url: url, linkRouter: mockRouter) + + // When + coordinator.didTapCard() + + // Then + wait(for: [expectation], timeout: 1) + XCTAssert(isLinkInvoked) + } + + func test_didTapCardDoesNotInvokeLinkRouterWhenURLInvalid() { + // Given + let (id, url) = ("123", "gibberish") + var mockRouter = MockRouter(routes: []) + mockRouter.canHandle = false + var isLinkInvoked = false + + mockRouter.completion = { _, _ in + isLinkInvoked = true + XCTFail("completion shouldn't have ran") + } + let coordinator = makeCoordinator(id: id, url: url, linkRouter: mockRouter) + + // When + coordinator.didTapCard() + + // Then + XCTAssertFalse(isLinkInvoked) + } + + func test_didTapCardCTAInvokesLinkRouterWhenURLMatches() { + // Given + let (id, url) = ("123", "https://wordpress.com") + var mockRouter = MockRouter(routes: []) + let expectation = expectation(description: "LinkRouter must invoke completion") + var isLinkInvoked = false + + mockRouter.completion = { _, _ in + isLinkInvoked = true + expectation.fulfill() + } + let coordinator = makeCoordinator(id: id, url: url, linkRouter: mockRouter) + + // When + coordinator.didTapCardCTA() + + // Then + wait(for: [expectation], timeout: 1) + XCTAssert(isLinkInvoked) + } + + func test_didTapCardCTADoesNotInvokeLinkRouterWhenURLInvalid() { + // Given + let (id, url) = ("123", "gibberish") + var mockRouter = MockRouter(routes: []) + mockRouter.canHandle = false + var isLinkInvoked = false + + mockRouter.completion = { _, _ in + isLinkInvoked = true + XCTFail("completion shouldn't have ran") + } + let coordinator = makeCoordinator(id: id, url: url, linkRouter: mockRouter) + + // When + coordinator.didTapCardCTA() + + // Then + XCTAssertFalse(isLinkInvoked) + } + + // MARK: - Helpers + + private func makeCoordinator( + id: String, + url: String? = nil, + linkRouter: LinkRouter = MockRouter(routes: []) + ) -> BlogDashboardDynamicCardCoordinator { + let payload = DashboardDynamicCardModel.Payload( + id: id, + remoteFeatureFlag: "default", + title: "Domain Management", + featuredImage: "https://wordpress.com", + url: url, + action: "Read more", + order: .top, + rows: nil + ) + return .init( + viewController: UIViewController(), + model: .init(payload: payload, dotComID: 1), + linkRouter: linkRouter, + analyticsTracker: AnalyticsEventTrackingSpy.self + ) + } +} diff --git a/WordPress/WordPressTest/Dashboard/Dynamic Cards/DashboardDynamicCardAnalyticsEventTests.swift b/WordPress/WordPressTest/Dashboard/Dynamic Cards/DashboardDynamicCardAnalyticsEventTests.swift new file mode 100644 index 000000000000..127345466136 --- /dev/null +++ b/WordPress/WordPressTest/Dashboard/Dynamic Cards/DashboardDynamicCardAnalyticsEventTests.swift @@ -0,0 +1,24 @@ +import Nimble +@testable import WordPress +import XCTest + +final class DashboardDynamicCardAnalyticsEventTests: XCTestCase { + + func testNamesAndProperties() { + // Given + let (id, url) = ("123", "https://wordpress.com") + + // When + let cardShownEvent = DashboardDynamicCardAnalyticsEvent.cardShown(id: id) + let cardTappedEvent = DashboardDynamicCardAnalyticsEvent.cardTapped(id: id, url: url) + let cardCTATappedEvent = DashboardDynamicCardAnalyticsEvent.cardCtaTapped(id: id, url: url) + + // Then + XCTAssertEqual(cardShownEvent.name, "dynamic_dashboard_card_shown") + XCTAssertEqual(cardTappedEvent.name, "dynamic_dashboard_card_tapped") + XCTAssertEqual(cardCTATappedEvent.name, "dynamic_dashboard_card_cta_tapped") + XCTAssertEqual(cardShownEvent.properties, ["id": id]) + XCTAssertEqual(cardTappedEvent.properties, ["id": id, "url": url]) + XCTAssertEqual(cardCTATappedEvent.properties, ["id": id, "url": url]) + } +} diff --git a/WordPress/WordPressTest/DeepLinkSourceTests.swift b/WordPress/WordPressTest/DeepLinkSourceTests.swift new file mode 100644 index 000000000000..6e24eb5a22bc --- /dev/null +++ b/WordPress/WordPressTest/DeepLinkSourceTests.swift @@ -0,0 +1,25 @@ +@testable import WordPress +import JetpackStatsWidgetsCore +import XCTest + +class DeepLinkSourceTests: XCTestCase { + + // MARK: – Test WidgetUrlSource compatibility + + // Notice that WidgetUrlSource is not a type we use in either the WordPress or Jetpack apps. + // It's a type used in the Jetpack stats widget. + // It's a bit of a stretch to import it here, given these are the unit tests for the WordPress and Jetpack apps. + // Still, it's useful to do so to ensure there are no compatibility breaks between the assumptions widgets and apps made. + + func testHomeScreenWidgetSourceType() { + let source = WidgetUrlSource.homeScreenWidget.rawValue + let deepLinkSource = DeepLinkSource(sourceName: source) + XCTAssertEqual(deepLinkSource, .widget) + } + + func testLockScreenWidgetSourceType() { + let source = WidgetUrlSource.lockScreenWidget.rawValue + let deepLinkSource = DeepLinkSource(sourceName: source) + XCTAssertEqual(deepLinkSource, .lockScreenWidget) + } +} diff --git a/WordPress/WordPressTest/Domains/AllDomainsListItem+Helpers.swift b/WordPress/WordPressTest/Domains/AllDomainsListItem+Helpers.swift new file mode 100644 index 000000000000..501d1890c4d7 --- /dev/null +++ b/WordPress/WordPressTest/Domains/AllDomainsListItem+Helpers.swift @@ -0,0 +1,61 @@ +import Foundation + +@testable import WordPress + +extension DomainsService.AllDomainsListItem { + + static func make( + domain: String = Defaults.domain, + blogId: Int = Defaults.blogId, + blogName: String = Defaults.blogName, + type: String = Defaults.type, + isDomainOnlySite: Bool = Defaults.isDomainOnlySite, + isWpcomStagingDomain: Bool = Defaults.isWpcomStagingDomain, + hasRegistration: Bool = Defaults.hasRegistration, + registrationDate: String? = Defaults.registrationDate, + expiryDate: String? = Defaults.expiryDate, + wpcomDomain: Bool = Defaults.wpcomDomain, + currentUserIsOwner: Bool? = Defaults.currentUserIsOwner, + siteSlug: String = Defaults.siteSlug, + status: DomainStatus = Defaults.status + ) throws -> Self { + let json: [String: Any] = [ + "domain": domain, + "blog_id": blogId, + "blog_name": blogName, + "type": type, + "is_domain_only_site": isDomainOnlySite, + "is_wpcom_staging_domain": isWpcomStagingDomain, + "has_registration": hasRegistration, + "registration_date": registrationDate as Any, + "expiry": expiryDate as Any, + "wpcom_domain": wpcomDomain, + "current_user_is_owner": currentUserIsOwner as Any, + "site_slug": siteSlug, + "domain_status": ["status": status.value, "status_type": status.type.rawValue] + ] + let data = try JSONSerialization.data(withJSONObject: json, options: .prettyPrinted) + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + return try decoder.decode(Domain.self, from: data) + } + + enum Defaults { + static let domain: String = "example1.com" + static let blogId: Int = 12345 + static let blogName: String = "Example Blog 1" + static let type: String = "mapped" + static let isDomainOnlySite: Bool = false + static let isWpcomStagingDomain: Bool = false + static let hasRegistration: Bool = true + static let registrationDate: String? = "2022-01-01T00:00:00+00:00" + static let expiryDate: String? = "2023-01-01T00:00:00+00:00" + static let wpcomDomain: Bool = false + static let currentUserIsOwner: Bool? = false + static let siteSlug: String = "exampleblog1.wordpress.com" + static let status: DomainStatus = .init(value: "Active", type: .success) + } + + typealias Domain = DomainsService.AllDomainsListItem + typealias DomainStatus = Domain.Status +} diff --git a/WordPress/WordPressTest/Domains/AllDomainsListItemViewModelTests.swift b/WordPress/WordPressTest/Domains/AllDomainsListItemViewModelTests.swift new file mode 100644 index 000000000000..aa19c48452aa --- /dev/null +++ b/WordPress/WordPressTest/Domains/AllDomainsListItemViewModelTests.swift @@ -0,0 +1,101 @@ +import XCTest + +@testable import WordPress + +fileprivate typealias Domain = DomainsService.AllDomainsListItem +fileprivate typealias DomainStatus = Domain.Status +fileprivate typealias ViewModel = AllDomainsListItemViewModel + +final class AllDomainsListItemViewModelTests: XCTestCase { + + func testMappingWithDefaultInput() throws { + self.assert( + viewModelFromDomain: try .make(), + equalTo: .make() + ) + } + + func testMappingWithDomainOnlySite() throws { + self.assert( + viewModelFromDomain: try .make(isDomainOnlySite: true), + equalTo: .make(description: nil) + ) + } + + func testMappingWithEmptyBlogNameDomain() throws { + self.assert( + viewModelFromDomain: try .make(blogName: ""), + equalTo: .make(description: Domain.Defaults.siteSlug) + ) + } + + func testMappingWithUnregisteredDomain() throws { + self.assert( + viewModelFromDomain: try .make(hasRegistration: false), + equalTo: .make(expiryDate: "Never expires") + ) + } + + func testMappingWithValidDomain() throws { + let futureDate = Date.init(timeIntervalSinceNow: 365 * 24 * 60 * 60) + let iso8601Date = ViewModel.Row.DateFormatters.iso8601.string(from: futureDate) + let humanReadableDate = ViewModel.Row.DateFormatters.humanReadable.string(from: futureDate) + self.assert( + viewModelFromDomain: try .make(expiryDate: iso8601Date), + equalTo: .make(expiryDate: "Renews \(humanReadableDate)") + ) + } + + private func assert(viewModelFromDomain domain: Domain, equalTo row: ViewModel.Row) { + let viewModel = ViewModel(domain: domain) + XCTAssertEqual(viewModel.row, row) + } +} + +// MARK: - ViewModel Helpers + +fileprivate extension AllDomainsListItemViewModel.Row { + + enum DateFormatters { + static let iso8601 = ISO8601DateFormatter() + static let humanReadable: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .medium + formatter.timeStyle = .none + return formatter + }() + } + + static func make( + name: String = "example1.com", + description: String? = "Example Blog 1", + status: DomainStatus = .init(value: "Active", type: .success), + expiryDate: String? = Self.defaultExpiryDate() + ) -> Self { + return .init( + name: name, + description: description, + status: status, + expiryDate: expiryDate + ) + } + + private static func defaultExpiryDate() -> String? { + guard let input = Domain.Defaults.expiryDate, let date = DateFormatters.iso8601.date(from: input) else { + return nil + } + let formatted = DateFormatters.humanReadable.string(from: date) + return "Expired \(formatted)" + } +} + +extension AllDomainsListItemViewModel.Row: Equatable { + + static public func ==(left: Self, right: Self) -> Bool { + return left.name == right.name + && left.description == right.description + && left.expiryDate == right.expiryDate + && left.status?.value == right.status?.value + && left.status?.type == right.status?.type + } +} diff --git a/WordPress/WordPressTest/Domains/DomainDetailsWebViewControllerTests.swift b/WordPress/WordPressTest/Domains/DomainDetailsWebViewControllerTests.swift new file mode 100644 index 000000000000..ff3ff7a0cbf6 --- /dev/null +++ b/WordPress/WordPressTest/Domains/DomainDetailsWebViewControllerTests.swift @@ -0,0 +1,61 @@ +import XCTest + +@testable import WordPress + +final class DomainDetailsWebViewControllerTests: XCTestCase { + + // MARK: - Types + + private typealias Domain = DomainsService.AllDomainsListItem + + private enum Constants { + static let domainManagementBase = "https://wordpress.com/domains/manage/all" + static let domain = Domain.Defaults.domain + static let siteSlug = Domain.Defaults.siteSlug + static let type = DomainType.mapped + static let viewSlug = "edit" + } + + // MARK: - Tests + + func testURLWithDomainOfTypeMapped() { + XCTAssertEqual(try makeURL(type: .mapped), try makeExpectedURL(viewSlug: "edit")) + } + + func testURLWithDomainOfTypeWpcom() { + XCTAssertEqual(try makeURL(type: .wpCom), try makeExpectedURL(viewSlug: "edit")) + } + + func testURLWithDomainOfTypeRegistered() { + XCTAssertEqual(try makeURL(type: .registered), try makeExpectedURL(viewSlug: "edit")) + } + + func testURLWithDomainOfTypeTransfer() { + XCTAssertEqual(try makeURL(type: .transfer), try makeExpectedURL(viewSlug: "transfer/in")) + } + + func testURLWithDomainOfTypeSiteRedirect() { + XCTAssertEqual(try makeURL(type: .siteRedirect), try makeExpectedURL(viewSlug: "redirect")) + } + + // MARK: - Helpers + + private func makeURL( + domain: String = Constants.domain, + siteSlug: String = Constants.siteSlug, + type: DomainType = Constants.type + ) throws -> String { + let controller = DomainDetailsWebViewController(domain: domain, siteSlug: siteSlug, type: type) + return try XCTUnwrap(controller.url?.absoluteString) + } + + private func makeExpectedURL( + domain: String = Constants.domain, + siteSlug: String = Constants.siteSlug, + viewSlug: String = Constants.viewSlug + ) throws -> String { + let url = "\(Constants.domainManagementBase)/\(domain)/\(viewSlug)/\(siteSlug)".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) + return try XCTUnwrap(url) + } + +} diff --git a/WordPress/WordPressTest/Domains/DomainExpiryDateFormatterTests.swift b/WordPress/WordPressTest/Domains/DomainExpiryDateFormatterTests.swift deleted file mode 100644 index 5ba260e54fab..000000000000 --- a/WordPress/WordPressTest/Domains/DomainExpiryDateFormatterTests.swift +++ /dev/null @@ -1,82 +0,0 @@ -// -// DomainExpiryDateFormatterTests.swift -// WordPressTest -// -// Created by James Frost on 20/10/2021. -// Copyright © 2021 WordPress. All rights reserved. -// - -import XCTest -@testable import WordPress - -class DomainExpiryDateFormatterTests: XCTestCase { - typealias Localized = DomainExpiryDateFormatter.Localized - - func testDomainWithNoExpiry() { - let domain = Domain(domainName: "mycooldomain.com", - isPrimaryDomain: false, - domainType: .registered, - expiryDate: nil) - - let expiryDate = DomainExpiryDateFormatter.expiryDate(for: domain) - XCTAssertEqual(expiryDate, Localized.neverExpires) - } - - func testAutoRenewingDomain() { - let domain = Domain(domainName: "mycooldomain.com", - isPrimaryDomain: false, - domainType: .registered, - autoRenewing: true, - autoRenewalDate: "5th August, 2022", - expirySoon: false, - expired: false, - expiryDate: "4th August, 2022") - - let expiryDate = DomainExpiryDateFormatter.expiryDate(for: domain) - let formattedString = String(format: Localized.renewsOn, domain.autoRenewalDate) - - XCTAssertEqual(expiryDate, formattedString) - } - - func testAutoRenewingDomainWithNoDate() { - // I think this should never happen, but it's good to cover the case anyway. - let domain = Domain(domainName: "mycooldomain.com", - isPrimaryDomain: false, - domainType: .registered, - autoRenewing: true, - autoRenewalDate: "", - expirySoon: false, - expired: false, - expiryDate: "4th August, 2022") - - let expiryDate = DomainExpiryDateFormatter.expiryDate(for: domain) - XCTAssertEqual(expiryDate, Localized.autoRenews) - } - - func testExpiredDomain() { - let domain = Domain(domainName: "mycooldomain.com", - isPrimaryDomain: false, - domainType: .registered, - autoRenewing: false, - autoRenewalDate: "", - expirySoon: false, - expired: true, - expiryDate: "4th August, 2021") - - let expiryDate = DomainExpiryDateFormatter.expiryDate(for: domain) - XCTAssertEqual(expiryDate, Localized.expired) - } - - func testExpiringDomain() { - let domain = Domain(domainName: "mycooldomain.com", - isPrimaryDomain: false, - domainType: .registered, - expired: false, - expiryDate: "4th August, 2022") - - let expiryDate = DomainExpiryDateFormatter.expiryDate(for: domain) - let formattedString = String(format: Localized.expiresOn, domain.expiryDate) - - XCTAssertEqual(expiryDate, formattedString) - } -} diff --git a/WordPress/WordPressTest/Domains/SiteDomainsViewModelTests.swift b/WordPress/WordPressTest/Domains/SiteDomainsViewModelTests.swift new file mode 100644 index 000000000000..3ecb7ac9377d --- /dev/null +++ b/WordPress/WordPressTest/Domains/SiteDomainsViewModelTests.swift @@ -0,0 +1,185 @@ +import XCTest +@testable import WordPress + +final class SiteDomainsViewModelTests: CoreDataTestCase { + private var viewModel: SiteDomainsViewModel! + private var mockDomainsService: MockDomainsService! + + override func setUp() { + super.setUp() + mockDomainsService = MockDomainsService() + viewModel = SiteDomainsViewModel(blog: BlogBuilder(mainContext).build(), domainsService: mockDomainsService) + } + + override func tearDown() { + viewModel = nil + mockDomainsService = nil + super.tearDown() + } + + func testInitialState_isLoading() { + viewModel.refresh() + XCTAssertTrue(viewModel.state == SiteDomainsViewModel.State.loading, "Initial state should be loading") + XCTAssertTrue(mockDomainsService.resolveStatus) + XCTAssertFalse(mockDomainsService.noWPCOM) + } + + func testRefresh_onlyFreeDomain() throws { + let blog = BlogBuilder(mainContext) + .with(blogID: 111) + .with(supportsDomains: true) + .build() + viewModel = SiteDomainsViewModel(blog: blog, domainsService: mockDomainsService) + + mockDomainsService.fetchResult = .success([try .make(domain: "Test", blogId: 111, wpcomDomain: true)]) + viewModel.refresh() + + if case .normal(let sections) = viewModel.state, + case .rows(let rows) = sections[0].content { + XCTAssertEqual(sections[0].title, SiteDomainsViewModel.Strings.freeDomainSectionTitle) + XCTAssertEqual(sections[1].content, .upgradePlan) + XCTAssertEqual(rows[0].viewModel.name, "Test") + } else { + XCTFail("Expected state not loaded") + } + } + + func testRefresh_stagingAndSimpleFreeDomain() throws { + let blog = BlogBuilder(mainContext) + .with(blogID: 111) + .with(supportsDomains: true) + .build() + viewModel = SiteDomainsViewModel(blog: blog, domainsService: mockDomainsService) + + mockDomainsService.fetchResult = .success([ + try .make(domain: "test.wordpress.com", blogId: 111, isWpcomStagingDomain: false, wpcomDomain: true), + try .make(domain: "test.wpcomstaging.com", blogId: 111, isWpcomStagingDomain: true, wpcomDomain: true) + ]) + viewModel.refresh() + + if case .normal(let sections) = viewModel.state, + case .rows(let rows) = sections[0].content { + XCTAssertEqual(sections[0].title, SiteDomainsViewModel.Strings.freeDomainSectionTitle) + XCTAssertEqual(sections[1].content, .upgradePlan) + XCTAssertEqual(rows[0].viewModel.name, "test.wpcomstaging.com") + } else { + XCTFail("Expected state not loaded") + } + } + + func testRefresh_paidDomain() throws { + let blog = BlogBuilder(mainContext) + .with(blogID: 123) + .with(supportsDomains: true) + .with(domainCount: 1, of: .wpCom, domainName: "Test") + .build() + viewModel = SiteDomainsViewModel(blog: blog, domainsService: mockDomainsService) + + mockDomainsService.fetchResult = .success([ + try .make(blogId: 123), + try .make(domain: "Test", blogId: 123, wpcomDomain: true) + ]) + viewModel.refresh() + + if case .normal(let sections) = viewModel.state, + case .rows(let secondSectionRows) = sections[1].content { + XCTAssertEqual(secondSectionRows[0].viewModel.name, DomainsService.AllDomainsListItem.Defaults.domain) + XCTAssertEqual(sections[2].content, .addDomain) + } else { + XCTFail("Expected state not loaded") + } + } + + func testRefresh_paidDomainForOtherBlog() throws { + let blog = BlogBuilder(mainContext) + .with(blogID: 123) + .with(supportsDomains: true) + .build() + viewModel = SiteDomainsViewModel(blog: blog, domainsService: mockDomainsService) + + mockDomainsService.fetchResult = .success([ + try .make(blogId: 1), + try .make(domain: "Test", blogId: 123, wpcomDomain: true) + ]) + viewModel.refresh() + + if case .normal(let sections) = viewModel.state { + XCTAssertEqual(sections[1].content, .upgradePlan) + } else { + XCTFail("Expected state not loaded") + } + } + + func testRefresh_error() { + let blog = BlogBuilder(mainContext) + .with(blogID: 123) + .with(supportsDomains: true) + .build() + viewModel = SiteDomainsViewModel(blog: blog, domainsService: mockDomainsService) + + mockDomainsService.fetchResult = .failure(NSError(domain: NSURLErrorDomain, code: NSURLErrorNotConnectedToInternet)) + viewModel.refresh() + + if case .message(let message) = viewModel.state { + XCTAssertEqual(message.title, DomainsStateViewModel.Strings.offlineEmptyStateTitle) + } else { + XCTFail("Expected state not loaded") + } + } +} + +// MARK: - Helpers + +private class MockDomainsService: NSObject, DomainsServiceAllDomainsFetching { + var fetchResult: Result<[DomainsService.AllDomainsListItem], Error>? + var resolveStatus: Bool = false + var noWPCOM: Bool = false + + func fetchAllDomains(resolveStatus: Bool, noWPCOM: Bool, completion: @escaping (DomainsServiceRemote.AllDomainsEndpointResult) -> Void) { + self.resolveStatus = resolveStatus + self.noWPCOM = noWPCOM + if let result = fetchResult { + completion(result) + } + } +} + +extension SiteDomainsViewModel.State: Equatable { + public static func == (lhs: SiteDomainsViewModel.State, rhs: SiteDomainsViewModel.State) -> Bool { + switch (lhs, rhs) { + case (.normal(let lhsSections), .normal(let rhsSections)): + return lhsSections == rhsSections + case (.loading, .loading): + return true + case (.message(let lhsMessage), .message(let rhsMessage)): + return lhsMessage.title == rhsMessage.title + default: + return false + } + } +} + +extension SiteDomainsViewModel.Section: Equatable { + public static func == (lhs: SiteDomainsViewModel.Section, rhs: SiteDomainsViewModel.Section) -> Bool { + return lhs.id == rhs.id && lhs.title == rhs.title + } +} + +extension SiteDomainsViewModel.Section.SectionKind: Equatable { + public static func == (lhs: SiteDomainsViewModel.Section.SectionKind, rhs: SiteDomainsViewModel.Section.SectionKind) -> Bool { + switch (lhs, rhs) { + case (.rows(let lhsRows), .rows(let rhsRows)): + return lhsRows == rhsRows + case (.addDomain, .addDomain), (.upgradePlan, .upgradePlan): + return true + default: + return false + } + } +} + +extension SiteDomainsViewModel.Section.Row: Equatable { + public static func == (lhs: SiteDomainsViewModel.Section.Row, rhs: SiteDomainsViewModel.Section.Row) -> Bool { + return lhs.id == rhs.id && lhs.viewModel == rhs.viewModel + } +} diff --git a/WordPress/WordPressTest/EUUSCompliance/CompliancePopoverViewModelTests.swift b/WordPress/WordPressTest/EUUSCompliance/CompliancePopoverViewModelTests.swift index d791573662ec..d72363a9f713 100644 --- a/WordPress/WordPressTest/EUUSCompliance/CompliancePopoverViewModelTests.swift +++ b/WordPress/WordPressTest/EUUSCompliance/CompliancePopoverViewModelTests.swift @@ -67,7 +67,7 @@ final class CompliancePopoverViewModelTests: CoreDataTestCase { } private func account() -> WPAccount { - return AccountBuilder(contextManager) + return AccountBuilder(contextManager.mainContext) .with(id: 1229) .with(username: "foobar") .with(email: "foo@automattic.com") @@ -81,7 +81,7 @@ private class MockCompliancePopoverCoordinator: CompliancePopoverCoordinatorProt private(set) var presentIfNeededCallCount = 0 private(set) var dismissCallCount = 0 - func presentIfNeeded(on viewController: UIViewController) { + func presentIfNeeded() { presentIfNeededCallCount += 1 } diff --git a/WordPress/WordPressTest/GutenbergInformativeDialogTests.swift b/WordPress/WordPressTest/GutenbergInformativeDialogTests.swift deleted file mode 100644 index 1c5f2a0cf795..000000000000 --- a/WordPress/WordPressTest/GutenbergInformativeDialogTests.swift +++ /dev/null @@ -1,41 +0,0 @@ -import XCTest -import UIKit -@testable import WordPress - -fileprivate class MockUIViewController: UIViewController, UIViewControllerTransitioningDelegate { - @objc func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { - return FancyAlertPresentationController(presentedViewController: presented, presenting: presenting) - } -} - -class GutenbergInformativeDialogTests: XCTestCase { - private var rootWindow: UIWindow! - private var viewController: MockUIViewController! - - override func setUp() { - viewController = MockUIViewController() - rootWindow = UIWindow(frame: UIScreen.main.bounds) - rootWindow.isHidden = false - rootWindow.rootViewController = viewController - } - - override func tearDown() { - rootWindow.rootViewController = nil - rootWindow.isHidden = true - rootWindow = nil - viewController = nil - } - - func testShowInformativeDialog() { - showInformativeDialog() - XCTAssertNotNil(viewController.presentedViewController as? FancyAlertViewController) - } - - private func showInformativeDialog() { - GutenbergViewController.showInformativeDialog( - on: viewController, - message: GutenbergViewController.InfoDialog.postMessage, - animated: false - ) - } -} diff --git a/WordPress/WordPressTest/ImageDownloaderTests.swift b/WordPress/WordPressTest/ImageDownloaderTests.swift new file mode 100644 index 000000000000..c141123a643b --- /dev/null +++ b/WordPress/WordPressTest/ImageDownloaderTests.swift @@ -0,0 +1,108 @@ +import XCTest +import OHHTTPStubs +@testable import WordPress + +class ImageDownloaderTests: CoreDataTestCase { + private var sut: ImageDownloader! + private let cache = MockMemoryCache() + + override func setUp() { + super.setUp() + + sut = ImageDownloader(cache: cache) + } + + // TODO: test canellation + // TODO: test coalescing + // TODO: test caching + // TODO: test resizing + + override func tearDown() { + super.tearDown() + + HTTPStubs.removeAllStubs() + } + + func testLoadResizedThumbnail() async throws { + // GIVEN + let imageURL = try XCTUnwrap(URL(string: "https://example.files.wordpress.com/2023/09/image.jpg")) + + // GIVEN remote image is mocked (1024×680 px) + try mockResponse(withResource: "test-image", fileExtension: "jpg") + + // WHEN + let options = ImageRequestOptions( + size: CGSize(width: 256, height: 256), + isMemoryCacheEnabled: false, + isDiskCacheEnabled: false + ) + let image = try await sut.image(from: imageURL, options: options) + + // THEN + XCTAssertEqual(image.size, CGSize(width: 386, height: 256)) + } + + func testCancellation() async throws { + // GIVEN + let imageURL = try XCTUnwrap(URL(string: "https://example.files.wordpress.com/2023/09/image.jpg")) + + // GIVEN remote image is mocked (1024×680 px) + try mockResponse(withResource: "test-image", fileExtension: "jpg", delay: 3) + + // WHEN + let options = ImageRequestOptions( + size: CGSize(width: 256, height: 256), + isMemoryCacheEnabled: false, + isDiskCacheEnabled: false + ) + let task = Task { + try await sut.image(from: imageURL, options: options) + } + + DispatchQueue.global().async { + task.cancel() + } + + // THEM + do { + let _ = try await task.value + XCTFail() + } catch { + XCTAssertEqual((error as? URLError)?.code, .cancelled) + } + } + + // MARK: - Helpers + + /// `Media` is hardcoded to work with a specific direcoty URL managed by `MediaFileManager` + func makeLocalURL(forResource name: String, fileExtension: String) throws -> URL { + let sourceURL = try XCTUnwrap(Bundle.test.url(forResource: name, withExtension: fileExtension)) + let mediaURL = try MediaFileManager.default.makeLocalMediaURL(withFilename: name, fileExtension: fileExtension) + try FileManager.default.copyItem(at: sourceURL, to: mediaURL) + return mediaURL + } + + func mockResponse(withResource name: String, fileExtension: String, expectedURL: URL? = nil, delay: TimeInterval = 0) throws { + let sourceURL = try XCTUnwrap(Bundle.test.url(forResource: name, withExtension: fileExtension)) + let data = try Data(contentsOf: sourceURL) + + stub(condition: { _ in + return true + }, response: { request in + guard expectedURL == nil || request.url == expectedURL else { + return HTTPStubsResponse(error: URLError(.unknown)) + } + return HTTPStubsResponse(data: data, statusCode: 200, headers: nil) + .requestTime(delay, responseTime: 0) + }) + } +} + +private final class MockMemoryCache: MemoryCacheProtocol { + var cache: [String: UIImage] = [:] + + subscript(key: String) -> UIImage? { + get { cache[key] } + set { cache[key] = newValue } + } +} diff --git a/WordPress/WordPressTest/Jetpack/JetpackBrandingVisibilityTests.swift b/WordPress/WordPressTest/Jetpack/JetpackBrandingVisibilityTests.swift new file mode 100644 index 000000000000..1c9ada9d8caa --- /dev/null +++ b/WordPress/WordPressTest/Jetpack/JetpackBrandingVisibilityTests.swift @@ -0,0 +1,28 @@ +@testable import WordPress +import XCTest + +class JetpackBrandingVisibilityTests: XCTestCase { + + func testEnabledCaseAll() { + let visibility = JetpackBrandingVisibility.all + + TruthTable.threeValues.forEach { + let isEnabled = visibility.isEnabled( + isWordPress: $0, + isDotComAvailable: $1, + shouldShowJetpackFeatures: $2 + ) + + // Only visible if: + // - the app is WordPress, + // - there is a DotCom account, + // - shouldShowJetpackFeatures is true + let expected = $0 && $1 && $2 + XCTAssertEqual( + isEnabled, + expected, + "isEnabled for WordPress \($0), DotCom \($1), and Jetpack features \($2) was not \(expected)" + ) + } + } +} diff --git a/WordPress/WordPressTest/JetpackBrandingMenuCardPresenterTests.swift b/WordPress/WordPressTest/JetpackBrandingMenuCardPresenterTests.swift index 7703ff0e6c78..13e9b3c7b83b 100644 --- a/WordPress/WordPressTest/JetpackBrandingMenuCardPresenterTests.swift +++ b/WordPress/WordPressTest/JetpackBrandingMenuCardPresenterTests.swift @@ -12,7 +12,7 @@ final class JetpackBrandingMenuCardPresenterTests: CoreDataTestCase { contextManager.useAsSharedInstance(untilTestFinished: self) mockUserDefaults = InMemoryUserDefaults() currentDateProvider = MockCurrentDateProvider() - let account = AccountBuilder(contextManager).build() + let account = AccountBuilder(contextManager.mainContext).build() UserSettings.defaultDotComUUID = account.uuid } diff --git a/WordPress/WordPressTest/JetpackBrandingTextProviderTests.swift b/WordPress/WordPressTest/JetpackBrandingTextProviderTests.swift index 935193a81bbe..ad88b3ce8feb 100644 --- a/WordPress/WordPressTest/JetpackBrandingTextProviderTests.swift +++ b/WordPress/WordPressTest/JetpackBrandingTextProviderTests.swift @@ -21,7 +21,7 @@ final class JetpackBrandingTextProviderTests: CoreDataTestCase { remoteFeatureFlagsStore = RemoteFeatureFlagStoreMock() currentDateProvider = MockCurrentDateProvider() remoteConfigStore.removalDeadline = "2022-10-10" - let account = AccountBuilder(contextManager).build() + let account = AccountBuilder(contextManager.mainContext).build() UserSettings.defaultDotComUUID = account.uuid } diff --git a/WordPress/WordPressTest/JetpackFeaturesRemovalCoordinatorTests.swift b/WordPress/WordPressTest/JetpackFeaturesRemovalCoordinatorTests.swift index 559b45ec02c9..9db30af93c62 100644 --- a/WordPress/WordPressTest/JetpackFeaturesRemovalCoordinatorTests.swift +++ b/WordPress/WordPressTest/JetpackFeaturesRemovalCoordinatorTests.swift @@ -8,7 +8,7 @@ final class JetpackFeaturesRemovalCoordinatorTests: CoreDataTestCase { override func setUp() { contextManager.useAsSharedInstance(untilTestFinished: self) mockUserDefaults = InMemoryUserDefaults() - let account = AccountBuilder(contextManager).build() + let account = AccountBuilder(contextManager.mainContext).build() UserSettings.defaultDotComUUID = account.uuid } diff --git a/WordPress/WordPressTest/MBarRouteTests.swift b/WordPress/WordPressTest/MBarRouteTests.swift index 60fd5f3bcc3d..56224f62f3b7 100644 --- a/WordPress/WordPressTest/MBarRouteTests.swift +++ b/WordPress/WordPressTest/MBarRouteTests.swift @@ -4,6 +4,7 @@ import OHHTTPStubs struct MockRouter: LinkRouter { let matcher: RouteMatcher + var canHandle: Bool = true var completion: ((URL, DeepLinkSource?) -> Void)? init(routes: [Route]) { @@ -11,7 +12,7 @@ struct MockRouter: LinkRouter { } func canHandle(url: URL) -> Bool { - return true + canHandle } func handle(url: URL, shouldTrack track: Bool, source: DeepLinkSource?) { diff --git a/WordPress/WordPressTest/MediaAssetExporterTests.swift b/WordPress/WordPressTest/MediaAssetExporterTests.swift deleted file mode 100644 index 359250e98c1e..000000000000 --- a/WordPress/WordPressTest/MediaAssetExporterTests.swift +++ /dev/null @@ -1,301 +0,0 @@ -import XCTest -@testable import WordPress -import MobileCoreServices -import UniformTypeIdentifiers -import Photos - -class MediaAssetExporterTests: XCTestCase { - - // MARK: - Image export testing - - let testDeviceImageNameWithGPS = "test-image-device-photo-gps.jpg" - let testDeviceImageNameWithGPSInPortrait = "test-image-device-photo-gps-portrait.jpg" - let testImageNameInPortrait = "test-image-portrait.jpg" - - func testThatAssetExportingWorks() { - let image = MediaImageExporterTests.imageForFileNamed(testDeviceImageNameWithGPS) - guard let asset = assetForFile(named: testDeviceImageNameWithGPS) else { - return - } - let expect = self.expectation(description: "Image PHAsset export.") - let exporter = MediaAssetExporter(asset: asset) - exporter.mediaDirectoryType = .temporary - exporter.export(onCompletion: { (imageExport) in - MediaImageExporterTests.validateImageExport(imageExport, withExpectedSize: max(image.size.width, image.size.height)) - MediaExporterTests.cleanUpExportedMedia(atURL: imageExport.url) - expect.fulfill() - }) { (error) in - XCTFail("Error: an error occurred testing an image export: \(error.toNSError())") - expect.fulfill() - } - waitForExpectations(timeout: 2.0, handler: nil) - } - - func testThatAssetExportingWithResizingWorks() { - guard let asset = assetForFile(named: testDeviceImageNameWithGPS) else { - return - } - let expect = self.expectation(description: "image export with a maximum size") - let exporter = MediaAssetExporter(asset: asset) - let maximumImageSize = CGFloat(200) - exporter.mediaDirectoryType = .temporary - var options = MediaImageExporter.Options() - options.maximumImageSize = maximumImageSize - exporter.imageOptions = options - exporter.export(onCompletion: { (imageExport) in - MediaImageExporterTests.validateImageExport(imageExport, withExpectedSize: maximumImageSize) - MediaExporterTests.cleanUpExportedMedia(atURL: imageExport.url) - expect.fulfill() - }) { (error) in - XCTFail("Error: an error occurred testing an image export by URL with a maximum size: \(error.toNSError())") - expect.fulfill() - } - waitForExpectations(timeout: 2.0, handler: nil) - } - - - // MARK: - Image export GPS testing - - func testThatImageExportingAndStrippingGPSWorks() { - guard let asset = assetForFile(named: testDeviceImageNameWithGPS) else { - return - } - let expect = self.expectation(description: "image export with stripping GPS") - let exporter = MediaAssetExporter(asset: asset) - exporter.mediaDirectoryType = .temporary - var options = MediaImageExporter.Options() - options.stripsGeoLocationIfNeeded = true - exporter.imageOptions = options - exporter.export(onCompletion: { (imageExport) in - MediaImageExporterTests.validateImageExportStrippedGPS(imageExport) - MediaExporterTests.cleanUpExportedMedia(atURL: imageExport.url) - expect.fulfill() - }) { (error) in - XCTFail("Error: an error occurred testing an image export and stripping GPS: \(error.toNSError())") - expect.fulfill() - } - waitForExpectations(timeout: 2.0, handler: nil) - } - - func testThatImageExportingAndDidNotStripGPSWorks() { - guard let asset = assetForFile(named: testDeviceImageNameWithGPS) else { - return - } - let expect = self.expectation(description: "image export with stripping GPS") - let exporter = MediaAssetExporter(asset: asset) - exporter.mediaDirectoryType = .temporary - var options = MediaImageExporter.Options() - options.stripsGeoLocationIfNeeded = false - exporter.imageOptions = options - exporter.export(onCompletion: { (imageExport) in - MediaImageExporterTests.validateImageExportDidNotStripGPS(imageExport) - MediaExporterTests.cleanUpExportedMedia(atURL: imageExport.url) - expect.fulfill() - }) { (error) in - XCTFail("Error: an error occurred testing an image export and not stripping GPS: \(error.toNSError())") - expect.fulfill() - } - waitForExpectations(timeout: 2.0, handler: nil) - } - - func testThatImageExportingWithResizingAndStrippingGPSWorks() { - guard let asset = assetForFile(named: testDeviceImageNameWithGPS) else { - return - } - let expect = self.expectation(description: "image export with resizing and stripping GPS") - let exporter = MediaAssetExporter(asset: asset) - let maximumImageSize = CGFloat(200) - exporter.mediaDirectoryType = .temporary - var options = MediaImageExporter.Options() - options.stripsGeoLocationIfNeeded = true - options.maximumImageSize = maximumImageSize - exporter.imageOptions = options - exporter.export(onCompletion: { (imageExport) in - MediaImageExporterTests.validateImageExportStrippedGPS(imageExport) - MediaImageExporterTests.validateImageExport(imageExport, withExpectedSize: maximumImageSize) - MediaExporterTests.cleanUpExportedMedia(atURL: imageExport.url) - expect.fulfill() - }) { (error) in - XCTFail("Error: an error occurred testing an image export with resizing and stripping GPS: " + - "\(error.toNSError())") - expect.fulfill() - } - waitForExpectations(timeout: 2.0, handler: nil) - } - - func testThatImageExportingWithResizingAndNotStrippingGPSWorks() { - guard let asset = assetForFile(named: testDeviceImageNameWithGPS) else { - return - } - let expect = self.expectation(description: "image export with resizing and stripping GPS") - let exporter = MediaAssetExporter(asset: asset) - let maximumImageSize = CGFloat(200) - exporter.mediaDirectoryType = .temporary - var options = MediaImageExporter.Options() - options.stripsGeoLocationIfNeeded = false - options.maximumImageSize = maximumImageSize - exporter.imageOptions = options - exporter.export(onCompletion: { (imageExport) in - MediaImageExporterTests.validateImageExportDidNotStripGPS(imageExport) - MediaImageExporterTests.validateImageExport(imageExport, withExpectedSize: maximumImageSize) - MediaExporterTests.cleanUpExportedMedia(atURL: imageExport.url) - expect.fulfill() - }) { (error) in - XCTFail("Error: an error occurred testing an image export with resizing and not stripping GPS: \(error.toNSError())") - expect.fulfill() - } - waitForExpectations(timeout: 2.0, handler: nil) - } - - // MARK: - Image export orientation testing - - func testExportingAPortraitImageWithoutResizeKeepsTheOrientationWorks() { - let image = MediaImageExporterTests.imageForFileNamed(testImageNameInPortrait) - if image.imageOrientation != .leftMirrored { - XCTFail("Error: the test portrait image was not in the expected orientation, expected: " - + " \(UIImage.Orientation.leftMirrored.rawValue) " + - " but read: \(image.imageOrientation.rawValue)") - return - } - guard let asset = assetForFile(named: testImageNameInPortrait) else { - return - } - let exporter = MediaAssetExporter(asset: asset) - let expect = self.expectation(description: "image export by UIImage and keeping the orientation") - exporter.mediaDirectoryType = .temporary - exporter.export(onCompletion: { (imageExport) in - // If not resising the image the orientation stays the same has the original - MediaImageExporterTests.validateImageExportedWithExpectedOrientation(export: imageExport, expected: .up) - MediaExporterTests.cleanUpExportedMedia(atURL: imageExport.url) - expect.fulfill() - }) { (error) in - XCTFail("Error: an error occurred testing an image export: \(error.toNSError())") - expect.fulfill() - } - waitForExpectations(timeout: 2.0, handler: nil) - } - - func testExportingAPortraitImageAndCorrectingTheOrientationWhileResizingWorks() { - let image = MediaImageExporterTests.imageForFileNamed(testImageNameInPortrait) - if image.imageOrientation != .leftMirrored { - XCTFail("Error: the test portrait image was not in the expected orientation, expected: \(UIImage.Orientation.leftMirrored.rawValue) but read: \(image.imageOrientation.rawValue)") - return - } - guard let asset = assetForFile(named: testImageNameInPortrait) else { - return - } - let exporter = MediaAssetExporter(asset: asset) - let maximumImageSize = CGFloat(200) - exporter.mediaDirectoryType = .temporary - var options = MediaImageExporter.Options() - options.maximumImageSize = maximumImageSize - exporter.imageOptions = options - let expect = self.expectation(description: "image export by UIImage and correcting the orientation with resizing") - exporter.export(onCompletion: { (imageExport) in - MediaImageExporterTests.validateImageExportedWithExpectedOrientation(export: imageExport, expected: .up) - MediaImageExporterTests.validateImageExport(imageExport, withExpectedSize: maximumImageSize) - MediaExporterTests.cleanUpExportedMedia(atURL: imageExport.url) - expect.fulfill() - }) { (error) in - XCTFail("Error: an error occurred testing an image export: \(error.toNSError())") - expect.fulfill() - } - waitForExpectations(timeout: 2.0, handler: nil) - } - - func testExportingAPortraitImageAndCorrectingTheOrientationWhileResizingAndStrippingGPSWorks() { - guard let asset = assetForFile(named: testDeviceImageNameWithGPSInPortrait) else { - return - } - - let exporter = MediaAssetExporter(asset: asset) - let maximumImageSize = CGFloat(200) - exporter.mediaDirectoryType = .temporary - var options = MediaImageExporter.Options() - options.maximumImageSize = maximumImageSize - options.stripsGeoLocationIfNeeded = true - exporter.imageOptions = options - let expect = self.expectation(description: "image export by UIImage and correcting the orientation with resizing and stripping GPS") - exporter.export(onCompletion: { (imageExport) in - MediaImageExporterTests.validateImageExportedWithExpectedOrientation(export: imageExport, expected: .up) - MediaImageExporterTests.validateImageExportStrippedGPS(imageExport) - MediaImageExporterTests.validateImageExport(imageExport, withExpectedSize: maximumImageSize) - MediaExporterTests.cleanUpExportedMedia(atURL: imageExport.url) - expect.fulfill() - }) { (error) in - XCTFail("Error: an error occurred testing an image export: \(error.toNSError())") - expect.fulfill() - } - waitForExpectations(timeout: 2.0, handler: nil) - } - - func testThatImageExportingByImageAndChangingFormatsWorks() { - let image = MediaImageExporterTests.imageForFileNamed(testDeviceImageNameWithGPS) - guard let asset = assetForFile(named: testDeviceImageNameWithGPS) else { - return - } - let exporter = MediaAssetExporter(asset: asset) - exporter.mediaDirectoryType = .temporary - var options = MediaImageExporter.Options() - options.exportImageType = UTType.png.identifier - exporter.imageOptions = options - let expect = self.expectation(description: "image export by UIImage") - exporter.export(onCompletion: { (imageExport) in - XCTAssertEqual(UTType.png.identifier, imageExport.url.typeIdentifier, "Unexpected image format when trying to target a PNG format from a JPEG.") - MediaImageExporterTests.validateImageExport(imageExport, withExpectedSize: max(image.size.width, image.size.height)) - MediaExporterTests.cleanUpExportedMedia(atURL: imageExport.url) - expect.fulfill() - }) { (error) in - XCTFail("Error: an error occurred testing an image export: \(error.toNSError())") - expect.fulfill() - } - waitForExpectations(timeout: 2.0, handler: nil) - } - - // MARK: - Helper methods - - private func assetForFile(named: String) -> PHAsset? { - if PHPhotoLibrary.authorizationStatus() != .authorized { - return nil - } - let path = MediaImageExporterTests.filePathForTestImageNamed(named) - let url = URL(fileURLWithPath: path) - let expect = self.expectation(description: "Create asset from image") - var assetIdentifier: String? = nil - PHPhotoLibrary.shared().performChanges({ - // Request creating an asset from the image. - let creationRequest = PHAssetChangeRequest.creationRequestForAssetFromImage(atFileURL: url) - if let placeholder = creationRequest?.placeholderForCreatedAsset { - assetIdentifier = placeholder.localIdentifier - } - - }, completionHandler: { success, error in - expect.fulfill() - if !success || assetIdentifier == nil { - XCTFail("Error: an error occurred loading an asset to test export: \(String(describing: error))") - } - }) - waitForExpectations(timeout: 2.0, handler: nil) - guard let identifier = assetIdentifier else { - XCTFail("Error: an error occurred loading an asset to test export.") - preconditionFailure() - } - let result = PHAsset.fetchAssets(withLocalIdentifiers: [identifier], options: nil) - guard let asset = result.firstObject else { - XCTFail("Error: an error occurred loading an asset to test export.") - preconditionFailure() - } - return asset - } - - private func deleteAsset(_ asset: PHAsset) { - let expect = self.expectation(description: "Create asset from image") - PHPhotoLibrary.shared().performChanges({ - // Request creating an asset from the image. - PHAssetChangeRequest.deleteAssets([asset] as NSArray) - }, completionHandler: { success, error in - expect.fulfill() - }) - waitForExpectations(timeout: 2.0, handler: nil) - } -} diff --git a/WordPress/WordPressTest/MediaImageServiceTests.swift b/WordPress/WordPressTest/MediaImageServiceTests.swift index 7d9ed0632b6d..7be4512c973d 100644 --- a/WordPress/WordPressTest/MediaImageServiceTests.swift +++ b/WordPress/WordPressTest/MediaImageServiceTests.swift @@ -26,6 +26,29 @@ class MediaImageServiceTests: CoreDataTestCase { } } + // MARK: - Original Image + + func testLoadOriginalImage() async throws { + // GIVEN + let media = Media(context: mainContext) + media.blog = makeEmptyBlog() + media.mediaType = .image + media.width = 1024 + media.height = 680 + let remoteURL = try XCTUnwrap(URL(string: "https://example.files.wordpress.com/2023/09/image.jpg")) + media.remoteURL = remoteURL.absoluteString + try mainContext.save() + + // GIVEN remote image is mocked + try mockResponse(withResource: "test-image", fileExtension: "jpg") + + // WHEN + let image = try await sut.image(for: media, size: .original) + + // THEN + XCTAssertEqual(image.size, CGSize(width: 1024, height: 680)) + } + // MARK: - Local Resources func testSmallThumbnailForLocalImage() async throws { @@ -40,20 +63,44 @@ class MediaImageServiceTests: CoreDataTestCase { try mainContext.save() // WHEN - let thumbnail = try await sut.thumbnail(for: media) + let thumbnail = try await sut.image(for: media, size: .small) // THEN a small thumbnail is created - XCTAssertEqual(thumbnail.size, MediaImageService.getThumbnailSize(for: media, size: .small)) + let expectedSize = await MediaImageService.getThumbnailSize(for: media.pixelSize(), size: .small) + XCTAssertEqual(thumbnail.size, expectedSize) // GIVEN local asset is deleted try FileManager.default.removeItem(at: localURL) - sut.flush() // WHEN - let cachedThumbnail = try await sut.thumbnail(for: media) + let cachedThumbnail = try await sut.image(for: media, size: .small) // THEN cached thumbnail is still available - XCTAssertEqual(cachedThumbnail.size, MediaImageService.getThumbnailSize(for: media, size: .small)) + XCTAssertEqual(cachedThumbnail.size, expectedSize) + } + + func testThatThumbnailsGeneratedForGIFAreAnimatable() async throws { + // GIVEN + let media = Media(context: mainContext) + media.blog = makeEmptyBlog() + media.mediaType = .image + media.width = 360 + media.height = 360 + + let localURL = try makeLocalURL(forResource: "test-gif", fileExtension: "gif") + media.absoluteLocalURL = localURL + try mainContext.save() + + // WHEN + let thumbnail = try await sut.image(for: media, size: .small) + + // THEN + let expectedSize = await MediaImageService.getThumbnailSize(for: media.pixelSize(), size: .small) + XCTAssertEqual(thumbnail.size, expectedSize) + let gif = try XCTUnwrap(thumbnail as? AnimatedImage) + let data = await gif.gifData ?? Data() + let source = try XCTUnwrap(CGImageSourceCreateWithData(data as CFData, nil)) + XCTAssertEqual(CGImageSourceGetCount(source), 20) } // MARK: - Remote Resources (Images) @@ -73,19 +120,19 @@ class MediaImageServiceTests: CoreDataTestCase { try mockResizableImage(withResource: "test-image", fileExtension: "jpg") // WHEN - let thumbnail = try await sut.thumbnail(for: media) + let thumbnail = try await sut.image(for: media, size: .small) // THEN a small thumbnail is created - XCTAssertEqual(thumbnail.size, MediaImageService.getThumbnailSize(for: media, size: .small)) + let expectedSize = await MediaImageService.getThumbnailSize(for: media.pixelSize(), size: .small) + XCTAssertEqual(thumbnail.size, expectedSize) // GIVEN local asset is deleted - sut.flush() // WHEN - let cachedThumbnail = try await sut.thumbnail(for: media) + let cachedThumbnail = try await sut.image(for: media, size: .small) // THEN cached thumbnail is still available - XCTAssertEqual(cachedThumbnail.size, MediaImageService.getThumbnailSize(for: media, size: .small)) + XCTAssertEqual(cachedThumbnail.size, expectedSize) } // MARK: - Remote Resources (Videos) @@ -104,7 +151,7 @@ class MediaImageServiceTests: CoreDataTestCase { try mockResponse(withResource: "test-image", fileExtension: "jpg") // WHEN - let thumbnail = try await sut.thumbnail(for: media) + let thumbnail = try await sut.image(for: media, size: .small) // THEN a thumbnail is downloaded using the remote URL as is XCTAssertEqual(thumbnail.size, CGSize(width: 1024, height: 680)) @@ -126,19 +173,18 @@ class MediaImageServiceTests: CoreDataTestCase { try mainContext.save() // WHEN - let thumbnail = try await sut.thumbnail(for: media) + let thumbnail = try await sut.image(for: media, size: .small) - let expectedSize = MediaImageService.getThumbnailSize(for: media, size: .small) + let expectedSize = await MediaImageService.getThumbnailSize(for: media.pixelSize(), size: .small) // THEN a thumbnail is downloaded using the remote URL as is XCTAssertEqual(thumbnail.size.width, expectedSize.width, accuracy: 1.5) XCTAssertEqual(thumbnail.size.height, expectedSize.height, accuracy: 1.5) // GIVEN local asset is deleted - sut.flush() // WHEN - let cachedThumbnail = try await sut.thumbnail(for: media) + let cachedThumbnail = try await sut.image(for: media, size: .small) // THEN cached thumbnail is still available XCTAssertEqual(cachedThumbnail.size.width, expectedSize.width, accuracy: 1.5) diff --git a/WordPress/WordPressTest/MediaPicker/MediaLibraryPickerDataSourceTests.swift b/WordPress/WordPressTest/MediaPicker/MediaLibraryPickerDataSourceTests.swift deleted file mode 100644 index 81e7bb0b7992..000000000000 --- a/WordPress/WordPressTest/MediaPicker/MediaLibraryPickerDataSourceTests.swift +++ /dev/null @@ -1,136 +0,0 @@ -import XCTest -@testable import WordPress -import WPMediaPicker -import Nimble - -class MediaLibraryPickerDataSourceTests: CoreDataTestCase { - - fileprivate var dataSource: MediaLibraryPickerDataSource! - fileprivate var blog: Blog! - fileprivate var post: Post! - - override func setUp() { - super.setUp() - blog = NSEntityDescription.insertNewObject(forEntityName: "Blog", into: mainContext) as? Blog - blog.url = "http://wordpress.com" - blog.xmlrpc = "http://wordpress.com" - post = NSEntityDescription.insertNewObject(forEntityName: Post.entityName(), into: mainContext) as? Post - post.blog = blog - dataSource = MediaLibraryPickerDataSource(blog: blog) - } - - func testMediaPixelSize() { - guard let media = newImageMedia() else { - XCTFail("Media should be created without error") - return - } - let size = media.pixelSize() - XCTAssertTrue(size.width == 1024, "Width should be 1024") - XCTAssertTrue(size.height == 680, "Height should be 680") - } - - func testVideoFetchForImage() { - guard let image = newImageMedia() else { - XCTFail("Media should be created without error") - return - } - let expect = self.expectation(description: "Image should fail to return a video asset.") - // test if using a image media returns an error - image.videoAsset(completionHandler: { (asset, error) in - expect.fulfill() - guard let error = error as NSError?, asset == nil else { - XCTFail("Image should fail when asked for a video") - return - } - XCTAssertTrue(error.domain == WPMediaPickerErrorDomain, "Should return a WPMediaPickerError") - XCTAssertTrue(error.code == WPMediaPickerErrorCode.videoURLNotAvailable.rawValue, "Should return a videoURLNotAvailable") - }) - self.waitForExpectations(timeout: 5, handler: nil) - } - - func testVideoFetchForVideo() { - guard let video = newVideoMedia() else { - XCTFail("Media should be created without error") - return - } - let expect = self.expectation(description: "Video asset should be returned") - video.videoAsset(completionHandler: { (asset, error) in - expect.fulfill() - guard error == nil, let asset = asset else { - XCTFail("Image should be returned without error") - return - } - - XCTAssertTrue(asset.duration.value > 0, "Asset should have a duration") - }) - self.waitForExpectations(timeout: 5, handler: nil) - } - - func testMediaGroupUpdates() { - contextManager.useAsSharedInstance(untilTestFinished: self) - dataSource.setMediaTypeFilter(.image) - - // This variable tracks how many times the album cover (which is what - // the "group" is in this use case) has changed. - var changes = 0 - dataSource.registerGroupChangeObserverBlock { - changes += 1 - } - - // Adding a video does not change the album cover. - let video = MediaBuilder(mainContext).build() - video.remoteStatus = .sync - video.blog = self.blog - video.mediaType = .video - contextManager.saveContextAndWait(mainContext) - expect(changes).toNever(beGreaterThan(0)) - - // Adding a newly created image changes the album cover. - let newImage = MediaBuilder(mainContext).build() - newImage.remoteStatus = .sync - newImage.blog = self.blog - newImage.mediaType = .image - newImage.creationDate = Date() - contextManager.saveContextAndWait(mainContext) - expect(changes).toEventually(equal(1)) - - // Adding an old image does not change the album cover. - let oldImage = MediaBuilder(mainContext).build() - oldImage.remoteStatus = .sync - oldImage.blog = self.blog - oldImage.mediaType = .image - oldImage.creationDate = Date().advanced(by: -60) - contextManager.saveContextAndWait(mainContext) - expect(changes).toNever(beGreaterThan(1)) - } - - fileprivate func newImageMedia() -> Media? { - return newMedia(fromResource: "test-image", withExtension: "jpg") - } - - fileprivate func newVideoMedia() -> Media? { - return newMedia(fromResource: "test-video-device-gps", withExtension: "m4v") - } - - fileprivate func newMedia(fromResource resource: String, withExtension ext: String) -> Media? { - var newMedia: Media? - guard let url = Bundle(for: type(of: self)).url(forResource: resource, withExtension: ext) else { - XCTFail("Pre condition to create media service failed") - return nil - } - - let service = MediaImportService(coreDataStack: contextManager) - let expect = self.expectation(description: "Media should be create with success") - _ = service.createMedia(with: url as NSURL, blog: blog, post: post, thumbnailCallback: nil, completion: { (media, error) in - expect.fulfill() - if let _ = error { - XCTFail("Media should be created without error") - return - } - newMedia = media - }) - self.waitForExpectations(timeout: 5, handler: nil) - return newMedia - } - -} diff --git a/WordPress/WordPressTest/MediaPicker/Tenor/TenorDataSouceTests.swift b/WordPress/WordPressTest/MediaPicker/Tenor/TenorDataSouceTests.swift index 9219728df5dd..df0e0c195a11 100644 --- a/WordPress/WordPressTest/MediaPicker/Tenor/TenorDataSouceTests.swift +++ b/WordPress/WordPressTest/MediaPicker/Tenor/TenorDataSouceTests.swift @@ -8,17 +8,12 @@ final class TenorDataSourceTests: XCTestCase { private struct Constants { static let searchTerm = "cat" - static let groupCount = 1 - static let groupName = String.tenor - - static func itemCount() -> Int { - return searchTerm.count - } + static let itemCount = 3 } override func setUp() { super.setUp() - mockService = MockTenorService(resultsCount: Constants.itemCount()) + mockService = MockTenorService(resultsCount: Constants.itemCount) dataSource = TenorDataSource(service: mockService!) } @@ -34,56 +29,6 @@ final class TenorDataSourceTests: XCTestCase { // Searches are debounced for half a second wait(for: 1) - XCTAssertEqual(dataSource?.numberOfAssets(), Constants.itemCount()) - } - - func testDataSourceManagesExpectedNumberOfGroups() { - let groupCount = dataSource?.numberOfGroups() - - XCTAssertEqual(groupCount, Constants.groupCount) - } - - func testSearchCancelledClearsData() { - dataSource?.searchCancelled() - - XCTAssertEqual(dataSource?.numberOfAssets(), 0) - } - - func testClearRemovesData() { - dataSource?.clearSearch(notifyObservers: false) - - XCTAssertEqual(dataSource?.numberOfAssets(), 0) - } - - func testGroupIsNamePhotosLibrary() { - let groupAtIndexZero = dataSource?.group(at: 0) - let groupName = groupAtIndexZero?.name() - - XCTAssertEqual(groupName, Constants.groupName) - } - - func testDataSourceOnlyManagesImages() { - let mediaType = dataSource?.mediaTypeFilter() - - XCTAssertEqual(mediaType, WPMediaType.image) - } - - func testDataSourceIsSortedAscending() { - XCTAssertTrue(dataSource!.ascendingOrdering()) - } - - func testSetSelectedGroupIsIgnored() { - let groupToBeIgnored = MockMediaGroup() - dataSource?.setSelectedGroup(groupToBeIgnored) - - let selectedGroup = dataSource?.selectedGroup() - - XCTAssertEqual(selectedGroup?.name(), Constants.groupName) - } - - func testOrderingCanNotBeChanged() { - dataSource?.setAscendingOrdering(false) - - XCTAssertTrue(dataSource!.ascendingOrdering()) + XCTAssertEqual(dataSource?.assets.count, Constants.itemCount) } } diff --git a/WordPress/WordPressTest/MediaRepositoryTests.swift b/WordPress/WordPressTest/MediaRepositoryTests.swift index 6dc5d17482f7..6524fd7495bc 100644 --- a/WordPress/WordPressTest/MediaRepositoryTests.swift +++ b/WordPress/WordPressTest/MediaRepositoryTests.swift @@ -49,6 +49,76 @@ class MediaRepositoryTests: CoreDataTestCase { } } + + // MARK: - Deleting Media + + func testDeletingMediaSuccess_WhenItsSynced() async throws { + remote.deleteMediaResult = .success(()) + + // Prepare the test data + let mediaID = try await contextManager.performAndSave { context in + let media = MediaBuilder(context) + .with(remoteStatus: .sync) + .with(autoUploadFailureCount: Media.maxAutoUploadFailureCount) + .build() + media.blog = BlogBuilder(context).build() + return TaggedManagedObjectID(media) + } + + // Make sure the media exists + var mediaExists = await contextManager.performQuery { context in + (try? context.existingObject(with: mediaID)) != nil + } + XCTAssertTrue(mediaExists) + + // Call the method to delete the media object, remotely and locally. + try await repository.delete(mediaID) + + // The media object should be deleted afterwards + mediaExists = await contextManager.performQuery { context in + (try? context.existingObject(with: mediaID)) != nil + } + XCTAssertFalse(mediaExists) + } + + func testDeletingMediaFailure_WhenAPICallFails() async throws { + remote.deleteMediaResult = .failure(NSError.testInstance(code: 404)) + + // Prepare the test data + let mediaID = try await contextManager.performAndSave { context in + let media = MediaBuilder(context) + .with(remoteStatus: .sync) + .with(autoUploadFailureCount: Media.maxAutoUploadFailureCount) + .build() + media.blog = BlogBuilder(context).build() + return TaggedManagedObjectID(media) + } + + // Make sure the media exists + var mediaExists = await contextManager.performQuery { context in + (try? context.existingObject(with: mediaID)) != nil + } + XCTAssertTrue(mediaExists) + + let expectation = expectation(description: "The delete call should throw error") + do { + // Call the method to delete the media object, remotely and locally. + try await repository.delete(mediaID) + } catch let error as NSError { + expectation.fulfill() + XCTAssertEqual(error.code, 404) + } catch { + XCTFail("Unexpected error: \(error)") + } + await fulfillment(of: [expectation]) + + // The local Media object should not be deleted because the API call failed. + mediaExists = await contextManager.performQuery { context in + (try? context.existingObject(with: mediaID)) != nil + } + XCTAssertTrue(mediaExists) + } + } private class MediaServiceRemoteFactoryStub: MediaServiceRemoteFactory { @@ -58,13 +128,14 @@ private class MediaServiceRemoteFactoryStub: MediaServiceRemoteFactory { self.remote = remote } - override func remote(for blog: Blog) -> MediaServiceRemote? { + override func remote(for blog: Blog) throws -> MediaServiceRemote { remote } } private class MediaServiceRemoteStub: NSObject, MediaServiceRemote { var getMediaResult: Result = .failure(testError()) + var deleteMediaResult: Result = .failure(testError()) func getMediaWithID(_ mediaID: NSNumber!, success: ((RemoteMedia?) -> Void)!, failure: ((Error?) -> Void)!) { switch getMediaResult { @@ -82,7 +153,10 @@ private class MediaServiceRemoteStub: NSObject, MediaServiceRemote { } func delete(_ media: RemoteMedia!, success: (() -> Void)!, failure: ((Error?) -> Void)!) { - fatalError("Unimplemented") + switch deleteMediaResult { + case .success: success() + case let .failure(error): failure(error) + } } func getMediaLibrary(pageLoad: (([Any]?) -> Void)!, success: (([Any]?) -> Void)!, failure: ((Error?) -> Void)!) { diff --git a/WordPress/WordPressTest/MediaServiceTests.swift b/WordPress/WordPressTest/MediaServiceTests.swift index a1452131c621..46131f0dbd4f 100644 --- a/WordPress/WordPressTest/MediaServiceTests.swift +++ b/WordPress/WordPressTest/MediaServiceTests.swift @@ -106,29 +106,4 @@ class MediaServiceTests: CoreDataTestCase { XCTAssertEqual(failedMediaForUpload.count, 0) } - - // MARK: - Deleting Media - - func testDeletingLocalMediaThatDoesntExistInCoreData() { - let firstDeleteSucceeds = expectation(description: "The delete call succeeds even if the media object isn't saved.") - let secondDeleteSucceeds = expectation(description: "The delete call succeeds even if the media object isn't saved.") - - let media = mediaBuilder - .with(remoteStatus: .failed) - .with(autoUploadFailureCount: Media.maxAutoUploadFailureCount).build() - - mediaService.delete(media) { - firstDeleteSucceeds.fulfill() - } failure: { error in - XCTFail("Media deletion failed with error: \(error)") - } - - mediaService.delete(media) { - secondDeleteSucceeds.fulfill() - } failure: { error in - XCTFail("Media deletion failed with error: \(error)") - } - - waitForExpectations(timeout: 0.1) - } } diff --git a/WordPress/WordPressTest/MediaServiceUpdateTests.m b/WordPress/WordPressTest/MediaServiceUpdateTests.m index 5be46e768c9f..4b96c4d2dfdc 100644 --- a/WordPress/WordPressTest/MediaServiceUpdateTests.m +++ b/WordPress/WordPressTest/MediaServiceUpdateTests.m @@ -76,7 +76,7 @@ - (void)testUpdateMediaWorks return NO; } else if (![remoteMedia.mediumURL isEqual:[NSURL URLWithString:media.remoteMediumURL]]) { return NO; - } else if (![remoteMedia.date isEqualToDate:media.date]) { + } else if (![remoteMedia.date isEqualToDate:media.creationDate]) { return NO; } else if (![remoteMedia.file isEqualToString:media.filename]) { return NO; @@ -132,7 +132,7 @@ - (void)testUpdateMultpipleMediaWorks return NO; } else if (![remoteMedia.mediumURL isEqual:[NSURL URLWithString:media.remoteMediumURL]]) { return NO; - } else if (![remoteMedia.date isEqualToDate:media.date]) { + } else if (![remoteMedia.date isEqualToDate:media.creationDate]) { return NO; } else if (![remoteMedia.file isEqualToString:media.filename]) { return NO; diff --git a/WordPress/WordPressTest/MediaSettingsTests.swift b/WordPress/WordPressTest/MediaSettingsTests.swift index 91efa0bd5ba9..b2e38489bd25 100644 --- a/WordPress/WordPressTest/MediaSettingsTests.swift +++ b/WordPress/WordPressTest/MediaSettingsTests.swift @@ -4,12 +4,32 @@ import Nimble class MediaSettingsTests: XCTestCase { + // MARK: - Default values func testDefaultMaxImageSize() { let settings = MediaSettings(database: EphemeralKeyValueDatabase()) let maxImageSize = settings.maxImageSizeSetting - expect(maxImageSize).to(equal(settings.allowedImageSizeRange.max)) + expect(maxImageSize).to(equal(2000)) } + func testDefaultImageOptimization() { + let settings = MediaSettings(database: EphemeralKeyValueDatabase()) + let imageOptimization = settings.imageOptimizationEnabled + expect(imageOptimization).to(beTrue()) + } + + func testDefaultImageQuality() { + let settings = MediaSettings(database: EphemeralKeyValueDatabase()) + let imageQuality = settings.imageQualitySetting + expect(imageQuality).to(equal(.medium)) + } + + func testDefaultAdvertiseImageOptimization() { + let settings = MediaSettings(database: EphemeralKeyValueDatabase()) + let advertiseImageOptimization = settings.advertiseImageOptimization + expect(advertiseImageOptimization).to(beTrue()) + } + + // MARK: - Max Image Size values func testMaxImageSizeMigratesCGSizeToInt() { let dimension = Int(1200) let size = CGSize(width: dimension, height: dimension) @@ -40,4 +60,25 @@ class MediaSettingsTests: XCTestCase { } + // MARK: - Values based on image optimization + func testImageSizeForUploadValueBasedOnOptimization() { + let settings = MediaSettings(database: EphemeralKeyValueDatabase()) + expect(settings.imageSizeForUpload).to(equal(2000)) + settings.imageOptimizationEnabled = false + expect(settings.imageSizeForUpload).to(equal(Int.max)) + } + + func testImageQualityForUploadValueBasedOnOptimization() { + let settings = MediaSettings(database: EphemeralKeyValueDatabase()) + expect(settings.imageQualityForUpload).to(equal(.medium)) + settings.imageOptimizationEnabled = false + expect(settings.imageQualityForUpload).to(equal(.high)) + } + + func testAdvertiseImageOptimizationValueBasedOnOptimization() { + let settings = MediaSettings(database: EphemeralKeyValueDatabase()) + expect(settings.advertiseImageOptimization).to(beTrue()) + settings.imageOptimizationEnabled = false + expect(settings.advertiseImageOptimization).to(beFalse()) + } } diff --git a/WordPress/WordPressTest/MediaTests.swift b/WordPress/WordPressTest/MediaTests.swift index 5a7be8b274c8..20af2b5c5d04 100644 --- a/WordPress/WordPressTest/MediaTests.swift +++ b/WordPress/WordPressTest/MediaTests.swift @@ -89,32 +89,6 @@ class MediaTests: CoreDataTestCase { XCTAssertEqual(media.autoUploadFailureCount, 0) } - func testMediaCount() { - let blog = BlogBuilder(mainContext).build() - let addMedia: (MediaType, Int) -> Void = { type, count in - for _ in 1...count { - let media = self.newTestMedia() - media.mediaType = type - media.blog = blog - } - } - addMedia(.image, 1) - addMedia(.video, 2) - addMedia(.document, 3) - addMedia(.powerpoint, 4) - addMedia(.audio, 5) - contextManager.saveContextAndWait(mainContext) - - XCTAssertEqual(blog.mediaLibraryCount(types: [MediaType.image.rawValue]), 1) - XCTAssertEqual(blog.mediaLibraryCount(types: [MediaType.video.rawValue]), 2) - XCTAssertEqual(blog.mediaLibraryCount(types: [MediaType.document.rawValue]), 3) - XCTAssertEqual(blog.mediaLibraryCount(types: [MediaType.powerpoint.rawValue]), 4) - XCTAssertEqual(blog.mediaLibraryCount(types: [MediaType.audio.rawValue]), 5) - - XCTAssertEqual(blog.mediaLibraryCount(types: [MediaType.image.rawValue, MediaType.video.rawValue]), 3) - XCTAssertEqual(blog.mediaLibraryCount(types: [MediaType.audio.rawValue, MediaType.powerpoint.rawValue]), 9) - } - // MARK: - Media Type func testMimeType() { diff --git a/WordPress/WordPressTest/MockStockPhotosService.swift b/WordPress/WordPressTest/MockStockPhotosService.swift index 93db419c3477..6766a4000c3c 100644 --- a/WordPress/WordPressTest/MockStockPhotosService.swift +++ b/WordPress/WordPressTest/MockStockPhotosService.swift @@ -24,7 +24,7 @@ final class MockStockPhotosService: StockPhotosService { private func crateStockPhotosMedia(id: String) -> StockPhotosMedia { let url = "https://images.pexels.com/photos/710916/pexels-photo-710916.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940".toURL()! - let thumbs = ThumbnailCollection( + let thumbs = StockPhotosMedia.ThumbnailCollection( largeURL: "https://images.pexels.com/photos/710916/pexels-photo-710916.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940".toURL()!, mediumURL: "https://images.pexels.com/photos/710916/pexels-photo-710916.jpeg?auto=compress&cs=tinysrgb&h=350".toURL()!, postThumbnailURL: "https://images.pexels.com/photos/710916/pexels-photo-710916.jpeg?auto=compress&cs=tinysrgb&h=130".toURL()!, diff --git a/WordPress/WordPressTest/My Site/NoSiteViewModelTests.swift b/WordPress/WordPressTest/My Site/NoSiteViewModelTests.swift index 6abd2302462c..b055465938a6 100644 --- a/WordPress/WordPressTest/My Site/NoSiteViewModelTests.swift +++ b/WordPress/WordPressTest/My Site/NoSiteViewModelTests.swift @@ -37,7 +37,7 @@ final class NoSiteViewModelTests: CoreDataTestCase { func test_gravatarURLIsNotNil_WhenAccountIsNotNil() { // Given - let account = AccountBuilder(contextManager) + let account = AccountBuilder(contextManager.mainContext) .with(email: "account@email.com") .build() let viewModel = NoSitesViewModel(appUIType: nil, account: account) @@ -56,7 +56,7 @@ final class NoSiteViewModelTests: CoreDataTestCase { func test_displayNameIsAccountDisplayName_WhenAccountIsNotNil() { // Given - let account = AccountBuilder(contextManager) + let account = AccountBuilder(contextManager.mainContext) .with(email: "account@email.com") .with(displayName: "Test") .build() diff --git a/WordPress/WordPressTest/MySiteViewModelTests.swift b/WordPress/WordPressTest/MySiteViewModelTests.swift index 9607b562e519..902ceea3f37e 100644 --- a/WordPress/WordPressTest/MySiteViewModelTests.swift +++ b/WordPress/WordPressTest/MySiteViewModelTests.swift @@ -97,7 +97,7 @@ class MySiteViewModelTests: CoreDataTestCase { func makeBlogAccessibleThroughWPCom(file: StaticString = #file, line: UInt = #line) -> Blog { let blog = BlogBuilder(contextManager.mainContext).build() - let account = AccountBuilder(contextManager) + let account = AccountBuilder(contextManager.mainContext) // username needs to be set for the token to be registered .with(username: "username") .with(authToken: "token") diff --git a/WordPress/WordPressTest/Pages/Controllers/PageListViewControllerTests.swift b/WordPress/WordPressTest/Pages/Controllers/PageListViewControllerTests.swift deleted file mode 100644 index 894015fa6bef..000000000000 --- a/WordPress/WordPressTest/Pages/Controllers/PageListViewControllerTests.swift +++ /dev/null @@ -1,17 +0,0 @@ -import UIKit -import XCTest -import Nimble - -@testable import WordPress - -class PageListViewControllerTests: CoreDataTestCase { - func testDoesNotShowGhostableTableView() { - let blog = BlogBuilder(mainContext).build() - let pageListViewController = PageListViewController.controllerWithBlog(blog) - let _ = pageListViewController.view - - pageListViewController.startGhost() - - expect(pageListViewController.ghostableTableView.isHidden).to(beTrue()) - } -} diff --git a/WordPress/WordPressTest/PagesListTests.swift b/WordPress/WordPressTest/PagesListTests.swift new file mode 100644 index 000000000000..a3a9e124d6b3 --- /dev/null +++ b/WordPress/WordPressTest/PagesListTests.swift @@ -0,0 +1,254 @@ +import Foundation +import XCTest + +@testable import WordPress + +class PagesListTests: CoreDataTestCase { + + let randomID = UniquePool(range: 1...999999) + + func testFlatList() throws { + let total = 1000 + let pages = (1...total).map { id in + let page = PageBuilder(mainContext).build() + page.postID = NSNumber(value: id) + return page + } + try makeAssertions(pages: pages) + } + + func testOneNestedList() throws { + let pages = parentPage(childrenCount: 1, additionalLevels: 5) + try makeAssertions(pages: pages) + } + + func testManyNestedLists() throws { + let groups = [ + parentPage(childrenCount: 5), + parentPage(childrenCount: 5), + parentPage(childrenCount: 5), + parentPage(childrenCount: 5), + parentPage(childrenCount: 5) + ] + groups[0][1].parentID = NSNumber(value: randomID.next()) + groups[0][2].parentID = NSNumber(value: randomID.next()) + groups[0][4].parentID = NSNumber(value: randomID.next()) + let pages = groups.flatMap { $0 } + + try makeAssertions(pages: pages) + } + + func testHugeNestedLists() throws { + var pages = [Page]() + for _ in 1...100 { + pages.append(contentsOf: parentPage(childrenCount: 5, additionalLevels: 4)) + } + // Add orphan pages + for _ in 1...50 { + let newPages = parentPage(childrenCount: 5) + newPages[0].parentID = NSNumber(value: randomID.next()) + pages.append(contentsOf: newPages) + } + + try makeAssertions(pages: pages) + } + + // Measure performance using a page list where somewhat reflects a real-world page list, where some pages whose parent pages are in list. + func testPerformance() throws { + // Make sure the total number of pages is about the same as the one in `testWorstPerformance`. + var pages = [Page]() + // Add pages whose parents are in the list. + for _ in 1...100 { + pages.append(contentsOf: parentPage(childrenCount: 5, additionalLevels: 4)) + } + // Add pages whose parents are *not* in the list. + for _ in 1...80 { + let newPages = parentPage(childrenCount: 5, additionalLevels: 4) + newPages[0].parentID = NSNumber(value: randomID.next()) + pages.append(contentsOf: newPages) + } + // Use a shuffled list to test performance, which in theory means more iterations in trying to find a page's parent page. + pages = pages.shuffled() + NSLog("\(pages.count) pages used in \(#function)") + + measure { + let list = (try? PageTree.hierarchyList(of: pages)) ?? [] + XCTAssertEqual(list.count, pages.count) + } + } + + // Measure performance using a page list where contains non-top-level pages and none of their parent pages are in the list. + func testWorstPerformance() throws { + var pages = [Page]() + for id in 1...5000 { + let page = PageBuilder(mainContext).build() + page.postID = NSNumber(value: id) + page.parentID = NSNumber(value: randomID.next()) + pages.append(page) + } + NSLog("\(pages.count) pages used in \(#function)") + + measure { + let list = (try? PageTree.hierarchyList(of: pages)) ?? [] + XCTAssertEqual(list.count, pages.count) + } + } + + func testOrphanPagesNestedLists() throws { + var pages = [Page]() + let orphan1 = parentPage(childrenCount: 0) + pages.append(contentsOf: orphan1) + pages.append(contentsOf: parentPage(childrenCount: 2)) + let orphan2 = parentPage(childrenCount: 2) + pages.append(contentsOf: orphan2) + pages.append(contentsOf: parentPage(childrenCount: 2)) + orphan1[0].parentID = 100000 + orphan2[0].parentID = 200000 + + try makeAssertions(pages: pages) + } + + func testHierachyListRepresentationRoundtrip() throws { + let roundtrip: (String) throws -> Void = { string in + let pages = try Array(hierarchyListRepresentation: string, context: self.mainContext) + try XCTAssertEqual(PageTree.hierarchyList(of: pages).hierarchyListRepresentation(), string) + } + + try roundtrip(""" + 1 + 2 + 3 + 4 + 5 + 6 + 7 + """) + + try roundtrip(""" + 1 + 2 + 3 + 4 + 5 + 6 + 7 + """) + } + + private func parentPage(childrenCount: Int, additionalLevels: Int = 0) -> [Page] { + var pages = [Page]() + + let parent = PageBuilder(mainContext).build() + parent.postID = NSNumber(value: randomID.next()) + parent.parentID = 0 + pages.append(parent) + + let children = (0.. 1 { + let nested = parentPage(childrenCount: childrenCount, additionalLevels: additionalLevels - 1) + nested[0].parentID = parent.postID + pages.append(contentsOf: nested) + } + + return pages + } + + private func makeAssertions(pages: [Page], file: StaticString = #file, line: UInt = #line) throws { + var start: CFAbsoluteTime + + start = CFAbsoluteTimeGetCurrent() + let original = pages.hierarchySort() + NSLog("hierarchySort took \(String(format: "%.3f", (CFAbsoluteTimeGetCurrent() - start) * 1000)) millisecond to process \(pages.count) pages") + + start = CFAbsoluteTimeGetCurrent() + let new = try PageTree.hierarchyList(of: pages) + NSLog("PageTree took \(String(format: "%.3f", (CFAbsoluteTimeGetCurrent() - start) * 1000)) millisecond to process \(pages.count) pages") + + start = CFAbsoluteTimeGetCurrent() + _ = pages.sorted { ($0.postID?.int64Value ?? 0) < ($1.postID?.int64Value ?? 0) } + NSLog("Array.sort took \(String(format: "%.3f", (CFAbsoluteTimeGetCurrent() - start) * 1000)) millisecond to process \(pages.count) pages") + + let originalIDs = original.map { $0.postID! } + let newIDs = new.map { $0.postID! } + let diff = originalIDs.difference(from: newIDs).inferringMoves() + XCTAssertTrue(diff.count == 0, "Unexpected diff: \(diff)", file: file, line: line) + } +} + +class UniquePool { + private var taken: Set = [] + let range: ClosedRange + + init(range: ClosedRange) { + self.range = range + } + + func next() -> Value { + repeat { + precondition(taken.count < range.count, "None left") + + let value = Value.random(in: range) + if !taken.contains(value) { + taken.insert(value) + return value + } + } while true + } +} + +private extension Array where Element == Page { + + static let indentation = 2 + + /// A string representation of a pages list whose element has a valid `hierarchyIndex` value. + /// + /// The output looks similar to the Pages List in the app, where child page is indented based on it's hierachy level. + /// + /// For example, this output here represents four page instances. The digits in the string are page ids. + /// Page 1 and 4 are top level pages. Page 1 has two child page: 2 and 3. + /// ``` + /// 1 + /// 2 + /// 3 + /// 4 + /// ``` + /// + /// The output can be converted back to `Page` instances using the init function below. + func hierarchyListRepresentation() -> String { + map { page in + "\(String(repeating: " ", count: page.hierarchyIndex * Self.indentation))\(page.postID!)" + } + .joined(separator: "\n") + } + + /// See the doc in `hierarchyListRepresentation`. + init(hierarchyListRepresentation: String, context: NSManagedObjectContext) throws { + var pages = [Page]() + + // The non-root-level parent pages. The first element is the parent page at level 1, the second element is the parent page at level 2, and so on. + var parentStack = [Page]() + for line in hierarchyListRepresentation.split(separator: "\n") { + let firstNonWhitespaceIndex = try XCTUnwrap(line.firstIndex(where: { $0 != " " })) + let leadingSpaces = line.distance(from: line.startIndex, to: firstNonWhitespaceIndex) + // 'level' starts with 0 (the root). + let level = leadingSpaces / Self.indentation + + let page = PageBuilder(context).build() + page.postID = try NSNumber(value: XCTUnwrap(Int64(line.trimmingCharacters(in: .whitespaces)))) + page.parentID = level == 0 ? 0 : parentStack[level - 1].postID + pages.append(page) + + parentStack.removeLast(parentStack.count - level) + parentStack.append(page) + } + + self = pages + } +} diff --git a/WordPress/WordPressTest/PostActionSheetTests.swift b/WordPress/WordPressTest/PostActionSheetTests.swift index 387c7a87c0de..1df49fa0f390 100644 --- a/WordPress/WordPressTest/PostActionSheetTests.swift +++ b/WordPress/WordPressTest/PostActionSheetTests.swift @@ -22,7 +22,7 @@ class PostActionSheetTests: CoreDataTestCase { } func testPublishedPostOptions() { - let viewModel = PostCardStatusViewModel(post: PostBuilder().published().withRemote().build()) + let viewModel = PostCardStatusViewModel(post: PostBuilder(mainContext).published().withRemote().build()) postActionSheet.show(for: viewModel, from: view) @@ -31,7 +31,7 @@ class PostActionSheetTests: CoreDataTestCase { } func testLocallyPublishedPostShowsCancelAutoUploadOption() { - let viewModel = PostCardStatusViewModel(post: PostBuilder().published().with(remoteStatus: .failed).confirmedAutoUpload().build()) + let viewModel = PostCardStatusViewModel(post: PostBuilder(mainContext).published().with(remoteStatus: .failed).confirmedAutoUpload().build()) postActionSheet.show(for: viewModel, from: view, isCompactOrSearching: true) @@ -40,7 +40,7 @@ class PostActionSheetTests: CoreDataTestCase { } func testDraftedPostOptions() { - let viewModel = PostCardStatusViewModel(post: PostBuilder().drafted().build()) + let viewModel = PostCardStatusViewModel(post: PostBuilder(mainContext).drafted().build()) postActionSheet.show(for: viewModel, from: view) @@ -49,7 +49,7 @@ class PostActionSheetTests: CoreDataTestCase { } func testScheduledPostOptions() { - let viewModel = PostCardStatusViewModel(post: PostBuilder().scheduled().build()) + let viewModel = PostCardStatusViewModel(post: PostBuilder(mainContext).scheduled().build()) postActionSheet.show(for: viewModel, from: view) @@ -58,7 +58,7 @@ class PostActionSheetTests: CoreDataTestCase { } func testTrashedPostOptions() { - let viewModel = PostCardStatusViewModel(post: PostBuilder().trashed().build()) + let viewModel = PostCardStatusViewModel(post: PostBuilder(mainContext).trashed().build()) postActionSheet.show(for: viewModel, from: view) @@ -67,7 +67,7 @@ class PostActionSheetTests: CoreDataTestCase { } func testPublishedPostOptionsWithView() { - let viewModel = PostCardStatusViewModel(post: PostBuilder().published().withRemote().build()) + let viewModel = PostCardStatusViewModel(post: PostBuilder(mainContext).published().withRemote().build()) postActionSheet.show(for: viewModel, from: view, isCompactOrSearching: true) @@ -76,7 +76,7 @@ class PostActionSheetTests: CoreDataTestCase { } func testCallDelegateWhenStatsTapped() { - let viewModel = PostCardStatusViewModel(post: PostBuilder().published().withRemote().build()) + let viewModel = PostCardStatusViewModel(post: PostBuilder(mainContext).published().withRemote().build()) postActionSheet.show(for: viewModel, from: view) tap("Stats", in: viewControllerMock.viewControllerPresented) @@ -85,7 +85,7 @@ class PostActionSheetTests: CoreDataTestCase { } func testCallDelegateWhenDuplicateTapped() { - let viewModel = PostCardStatusViewModel(post: PostBuilder().published().withRemote().build()) + let viewModel = PostCardStatusViewModel(post: PostBuilder(mainContext).published().withRemote().build()) postActionSheet.show(for: viewModel, from: view) tap("Duplicate", in: viewControllerMock.viewControllerPresented) @@ -94,7 +94,7 @@ class PostActionSheetTests: CoreDataTestCase { } func testCallDelegateWhenShareTapped() { - let viewModel = PostCardStatusViewModel(post: PostBuilder().published().withRemote().build()) + let viewModel = PostCardStatusViewModel(post: PostBuilder(mainContext).published().withRemote().build()) postActionSheet.show(for: viewModel, from: view) tap("Share", in: viewControllerMock.viewControllerPresented) @@ -103,7 +103,7 @@ class PostActionSheetTests: CoreDataTestCase { } func testCallDelegateWhenMoveToDraftTapped() { - let viewModel = PostCardStatusViewModel(post: PostBuilder().published().build()) + let viewModel = PostCardStatusViewModel(post: PostBuilder(mainContext).published().build()) postActionSheet.show(for: viewModel, from: view) tap("Move to Draft", in: viewControllerMock.viewControllerPresented) @@ -112,7 +112,7 @@ class PostActionSheetTests: CoreDataTestCase { } func testCallDelegateWhenDeletePermanentlyTapped() { - let viewModel = PostCardStatusViewModel(post: PostBuilder().trashed().build()) + let viewModel = PostCardStatusViewModel(post: PostBuilder(mainContext).trashed().build()) postActionSheet.show(for: viewModel, from: view) tap("Delete Permanently", in: viewControllerMock.viewControllerPresented) @@ -121,7 +121,7 @@ class PostActionSheetTests: CoreDataTestCase { } func testCallDelegateWhenCopyLink() { - let viewModel = PostCardStatusViewModel(post: PostBuilder().published().build()) + let viewModel = PostCardStatusViewModel(post: PostBuilder(mainContext).published().build()) postActionSheet.show(for: viewModel, from: view) tap("Copy Link", in: viewControllerMock.viewControllerPresented) @@ -130,7 +130,7 @@ class PostActionSheetTests: CoreDataTestCase { } func testCallDelegateWhenMoveToTrashTapped() { - let viewModel = PostCardStatusViewModel(post: PostBuilder().published().build()) + let viewModel = PostCardStatusViewModel(post: PostBuilder(mainContext).published().build()) postActionSheet.show(for: viewModel, from: view) tap("Move to Trash", in: viewControllerMock.viewControllerPresented) @@ -139,7 +139,7 @@ class PostActionSheetTests: CoreDataTestCase { } func testCallDelegateWhenViewTapped() { - let viewModel = PostCardStatusViewModel(post: PostBuilder().published().build()) + let viewModel = PostCardStatusViewModel(post: PostBuilder(mainContext).published().build()) postActionSheet.show(for: viewModel, from: view, isCompactOrSearching: true) tap("View", in: viewControllerMock.viewControllerPresented) @@ -167,7 +167,7 @@ class PostActionSheetTests: CoreDataTestCase { } func testCallsDelegateWhenCancelAutoUploadIsTapped() { - let viewModel = PostCardStatusViewModel(post: PostBuilder().published().with(remoteStatus: .failed).confirmedAutoUpload().build()) + let viewModel = PostCardStatusViewModel(post: PostBuilder(mainContext).published().with(remoteStatus: .failed).confirmedAutoUpload().build()) postActionSheet.show(for: viewModel, from: view, isCompactOrSearching: true) tap(Titles.cancelAutoUpload, in: viewControllerMock.viewControllerPresented) @@ -176,7 +176,7 @@ class PostActionSheetTests: CoreDataTestCase { } func testCallsDelegateWhenRetryIsTapped() { - let viewModel = PostCardStatusViewModel(post: PostBuilder().with(remoteStatus: .failed).with(autoUploadAttemptsCount: 5).build()) + let viewModel = PostCardStatusViewModel(post: PostBuilder(mainContext).with(remoteStatus: .failed).with(autoUploadAttemptsCount: 5).build()) postActionSheet.show(for: viewModel, from: view, isCompactOrSearching: true) tap(Titles.retry, in: viewControllerMock.viewControllerPresented) @@ -249,10 +249,6 @@ class InteractivePostViewDelegateMock: InteractivePostViewDelegate { } - func restore(_ post: AbstractPost) { - - } - func retry(_ post: AbstractPost) { didCallRetry = true } diff --git a/WordPress/WordPressTest/PostBuilder.swift b/WordPress/WordPressTest/PostBuilder.swift index c1bab3a69ee8..652bf0d5af6d 100644 --- a/WordPress/WordPressTest/PostBuilder.swift +++ b/WordPress/WordPressTest/PostBuilder.swift @@ -8,11 +8,15 @@ import Foundation class PostBuilder { private let post: Post - init(_ context: NSManagedObjectContext = PostBuilder.setUpInMemoryManagedObjectContext(), blog: Blog? = nil) { + init(_ context: NSManagedObjectContext, blog: Blog? = nil, canBlaze: Bool = false) { post = NSEntityDescription.insertNewObject(forEntityName: Post.entityName(), into: context) as! Post // Non-null Core Data properties - post.blog = blog ?? BlogBuilder(context).build() + if let blog { + post.blog = blog + } else { + post.blog = canBlaze ? BlogBuilder(context).canBlaze().build() : BlogBuilder(context).build() + } } private static func buildPost(context: NSManagedObjectContext) -> Post { @@ -72,7 +76,6 @@ class PostBuilder { return self } - func withImage() -> PostBuilder { post.pathForDisplayImage = "https://localhost/image.png" return self @@ -207,21 +210,4 @@ class PostBuilder { // assert(post.managedObjectContext != nil) return post } - - static func setUpInMemoryManagedObjectContext() -> NSManagedObjectContext { - let managedObjectModel = NSManagedObjectModel.mergedModel(from: [Bundle.main])! - - let persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: managedObjectModel) - - do { - try persistentStoreCoordinator.addPersistentStore(ofType: NSInMemoryStoreType, configurationName: nil, at: nil, options: nil) - } catch { - print("Adding in-memory persistent store failed") - } - - let managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) - managedObjectContext.persistentStoreCoordinator = persistentStoreCoordinator - - return managedObjectContext - } } diff --git a/WordPress/WordPressTest/PostCardCellGhostableTests.swift b/WordPress/WordPressTest/PostCardCellGhostableTests.swift deleted file mode 100644 index 3c98a9a86fa1..000000000000 --- a/WordPress/WordPressTest/PostCardCellGhostableTests.swift +++ /dev/null @@ -1,109 +0,0 @@ -import UIKit -import XCTest - -@testable import WordPress - -class PostCardCellGhostableTests: CoreDataTestCase { - - var postCell: PostCardCell! - - override func setUp() { - postCell = postCellFromNib() - postCell.ghostAnimationWillStart() - super.setUp() - } - - func testHideFeaturedImage() { - XCTAssertTrue(postCell.featuredImageStackView.isHidden) - } - - func testShowGhostView() { - XCTAssertFalse(postCell.ghostStackView.isHidden) - } - - func testShowSnippetLabel() { - XCTAssertFalse(postCell.snippetLabel.isHidden) - } - - func testHideStatusView() { - XCTAssertTrue(postCell.statusView.isHidden) - } - - func testHideProgressView() { - XCTAssertTrue(postCell.progressView.isHidden) - } - - func testChangesActionBarOpacity() { - XCTAssertEqual(postCell.actionBarView.layer.opacity, 0.5) - } - - func testIsNotInteractive() { - XCTAssertFalse(postCell.isUserInteractionEnabled) - } - - func testTopPadding() { - XCTAssertEqual(postCell.topPadding.constant, 16) - } - - func testActionBarIsNotGhostable() { - XCTAssertTrue(postCell.actionBarView.isGhostableDisabled) - } - - func testUpperBorderIsNotGhostable() { - XCTAssertTrue(postCell.upperBorder.isGhostableDisabled) - } - - func testBottomBorderIsNotGhostable() { - XCTAssertTrue(postCell.bottomBorder.isGhostableDisabled) - } - - func testIsInteractiveAfterAConfigure() { - let post = PostBuilder(mainContext).build() - - postCell.configure(with: post) - - XCTAssertTrue(postCell.isUserInteractionEnabled) - } - - func testVerticalContentSpacingAfterConfigure() { - let post = PostBuilder(mainContext).build() - - postCell.configure(with: post) - - XCTAssertEqual(postCell.contentStackView.spacing, 8) - } - - func testActionBarOpacityAfterConfigure() { - let post = PostBuilder(mainContext).with(remoteStatus: .sync).build() - assert(post.managedObjectContext != nil) - postCell.configure(with: post) - - XCTAssertEqual(postCell.actionBarView.layer.opacity, 1) - } - - func testActionBarOpacityAfterConfigureWithPushingStatus() { - let post = PostBuilder(mainContext).with(remoteStatus: .pushing).build() - assert(post.managedObjectContext != nil) - postCell.configure(with: post) - - XCTAssertEqual(postCell.actionBarView.layer.opacity, 0.3) - } - - func testHideGhostView() { - let post = PostBuilder(mainContext).build() - - postCell.configure(with: post) - - XCTAssertTrue(postCell.ghostStackView.isHidden) - } - - private func postCellFromNib() -> PostCardCell { - let bundle = Bundle(for: PostCardCell.self) - guard let postCell = bundle.loadNibNamed("PostCardCell", owner: nil)?.first as? PostCardCell else { - fatalError("PostCardCell does not exist") - } - - return postCell - } - -} diff --git a/WordPress/WordPressTest/PostCardCellTests.swift b/WordPress/WordPressTest/PostCardCellTests.swift deleted file mode 100644 index 1a51aa55bf88..000000000000 --- a/WordPress/WordPressTest/PostCardCellTests.swift +++ /dev/null @@ -1,520 +0,0 @@ -import UIKit -import XCTest -import Nimble - -@testable import WordPress - -private typealias StatusMessages = PostCardStatusViewModel.StatusMessages - -class PostCardCellTests: CoreDataTestCase { - private var postCell: PostCardCell! - private var interactivePostViewDelegateMock: InteractivePostViewDelegateMock! - private var postActionSheetDelegateMock: PostActionSheetDelegateMock! - - override func setUp() { - super.setUp() - - postCell = postCellFromNib() - interactivePostViewDelegateMock = InteractivePostViewDelegateMock() - postActionSheetDelegateMock = PostActionSheetDelegateMock() - postCell.setInteractionDelegate(interactivePostViewDelegateMock) - postCell.setActionSheetDelegate(postActionSheetDelegateMock) - } - - func testIsAUITableViewCell() { - XCTAssertNotNil(postCell as UITableViewCell) - } - - func testShowImageWhenAvailable() { - let post = PostBuilder(mainContext).withImage().build() - - postCell.configure(with: post) - - XCTAssertFalse(postCell.featuredImage.isHidden) - } - - func testHideImageWhenNotAvailable() { - let post = PostBuilder(mainContext).build() - - postCell.configure(with: post) - - XCTAssertTrue(postCell.featuredImageStackView.isHidden) - } - - func testShowPostTitle() { - let post = PostBuilder(mainContext).with(title: "Foo bar").build() - - postCell.configure(with: post) - - XCTAssertEqual(postCell.titleLabel.text, "Foo bar") - } - - func testShowPostSnippet() { - let post = PostBuilder(mainContext).with(snippet: "Post content").build() - - postCell.configure(with: post) - - XCTAssertEqual(postCell.snippetLabel.text, "Post content") - XCTAssertFalse(postCell.snippetLabel.isHidden) - } - - func testHidePostSnippet() { - let post = PostBuilder(mainContext).with(snippet: "").build() - - postCell.configure(with: post) - - XCTAssertTrue(postCell.snippetLabel.isHidden) - } - - func testShowDate() { - let post = PostBuilder(mainContext).with(dateModified: Date()).drafted().build() - - postCell.configure(with: post) - - expect(self.postCell.dateLabel.text).toNot(beEmpty()) - } - - func testShowAuthor() { - let post = PostBuilder(mainContext).with(author: "John Doe").build() - - postCell.configure(with: post) - - XCTAssertEqual(postCell.authorLabel.text, "John Doe") - } - - func testShowStickyLabelWhenPostIsSticky() { - let post = PostBuilder(mainContext).is(sticked: true).with(remoteStatus: .sync).build() - - postCell.configure(with: post) - - XCTAssertEqual(postCell.statusLabel?.text, "Sticky") - } - - func testHideStickyLabelWhenPostIsntSticky() { - let post = PostBuilder(mainContext).is(sticked: false).with(remoteStatus: .sync).build() - - postCell.configure(with: post) - - XCTAssertEqual(postCell.statusLabel?.text, "") - } - - func testHideStickyLabelWhenPostIsUploading() { - let post = PostBuilder(mainContext).is(sticked: true).with(remoteStatus: .pushing).build() - - postCell.configure(with: post) - - XCTAssertEqual(postCell.statusLabel?.text, "Uploading post...") - } - - func testHideStickyLabelWhenPostIsFailed() { - let post = PostBuilder(mainContext).is(sticked: true).with(remoteStatus: .failed).build() - - postCell.configure(with: post) - - XCTAssertEqual(postCell.statusLabel?.text, StatusMessages.uploadFailed) - } - - func testShowPrivateLabelWhenPostIsPrivate() { - let post = PostBuilder(mainContext).with(remoteStatus: .sync).private().build() - - postCell.configure(with: post) - - XCTAssertEqual(postCell.statusLabel.text, "Private") - } - - func testDoNotShowTrashedLabel() { - let post = PostBuilder(mainContext).with(remoteStatus: .sync).trashed().build() - - postCell.configure(with: post) - - XCTAssertEqual(postCell.statusLabel.text, "") - } - - func testDoNotShowScheduledLabel() { - let post = PostBuilder(mainContext).with(remoteStatus: .sync).scheduled().build() - - postCell.configure(with: post) - - XCTAssertEqual(postCell.statusLabel.text, "") - } - - func testHideHideStatusView() { - let post = PostBuilder(mainContext) - .with(remoteStatus: .sync) - .published().build() - - postCell.configure(with: post) - - XCTAssertTrue(postCell.statusView.isHidden) - } - - func testShowStatusView() { - let post = PostBuilder(mainContext) - .with(remoteStatus: .failed) - .published().build() - - postCell.configure(with: post) - - XCTAssertFalse(postCell.statusView.isHidden) - } - - func testShowProgressView() { - let post = PostBuilder(mainContext) - .with(remoteStatus: .pushing) - .published().build() - - postCell.configure(with: post) - - XCTAssertFalse(postCell.progressView.isHidden) - } - - func testHideProgressView() { - let post = PostBuilder(mainContext) - .with(remoteStatus: .sync) - .published().build() - - postCell.configure(with: post) - - XCTAssertTrue(postCell.progressView.isHidden) - } - - func testIsUserInteractionEnabled() { - let post = PostBuilder(mainContext).withImage().build() - postCell.isUserInteractionEnabled = false - - postCell.configure(with: post) - - XCTAssertTrue(postCell.isUserInteractionEnabled) - } - - func testEditAction() { - let post = PostBuilder(mainContext).published().build() - postCell.configure(with: post) - - postCell.edit() - - XCTAssertTrue(interactivePostViewDelegateMock.didCallEdit) - } - - func testViewAction() { - let post = PostBuilder(mainContext).published().build() - postCell.configure(with: post) - - postCell.view() - - XCTAssertTrue(interactivePostViewDelegateMock.didCallView) - } - - func testMoreAction() { - let button = UIButton() - let post = PostBuilder(mainContext).published().build() - postCell.configure(with: post) - - postCell.more(button) - - XCTAssertEqual(postActionSheetDelegateMock.calledWithPost, post) - XCTAssertEqual(postActionSheetDelegateMock.calledWithView, button) - } - - func testRetryAction() { - let post = PostBuilder(mainContext).published().build() - postCell.configure(with: post) - - postCell.retry() - - XCTAssertTrue(interactivePostViewDelegateMock.didCallRetry) - } - - func testShowPublishButtonAndHideViewButton() { - let post = PostBuilder(mainContext).private().with(remoteStatus: .failed).build() - - postCell.configure(with: post) - - XCTAssertFalse(postCell.publishButton.isHidden) - XCTAssertTrue(postCell.viewButton.isHidden) - } - - func testHideAuthorAndSeparator() { - let post = PostBuilder(mainContext).with(author: "John Doe").build() - postCell.configure(with: post) - - postCell.shouldHideAuthor = true - - XCTAssertTrue(postCell.authorLabel.isHidden) - XCTAssertTrue(postCell.separatorLabel.isHidden) - } - - func testDoesNotHideAuthorAndSeparator() { - let post = PostBuilder(mainContext).with(author: "John Doe").build() - postCell.configure(with: post) - - postCell.shouldHideAuthor = false - - XCTAssertFalse(postCell.authorLabel.isHidden) - XCTAssertFalse(postCell.separatorLabel.isHidden) - } - - func testHidesAuthorSeparatorWhenAuthorEmpty() { - let post = PostBuilder(mainContext).with(author: "").build() - postCell.configure(with: post) - - postCell.shouldHideAuthor = false - - XCTAssertTrue(postCell.authorLabel.isHidden) - XCTAssertTrue(postCell.separatorLabel.isHidden) - } - - func testShowsPostWillBePublishedWarningForFailedPublishedPostsWithRemote() { - // Given - let post = PostBuilder(mainContext).published().withRemote().with(remoteStatus: .failed).confirmedAutoUpload().build() - - // When - postCell.configure(with: post) - - // Then - XCTAssertEqual(postCell.statusLabel.text, i18n("We'll publish the post when your device is back online.")) - XCTAssertEqual(postCell.statusLabel.textColor, UIColor.warning) - } - - func testShowsPostWillBePublishedWarningForLocallyPublishedPosts() { - // Given - let post = PostBuilder(mainContext).published().with(remoteStatus: .failed).confirmedAutoUpload().build() - - // When - postCell.configure(with: post) - - // Then - XCTAssertEqual(postCell.statusLabel.text, i18n("We'll publish the post when your device is back online.")) - XCTAssertEqual(postCell.statusLabel.textColor, UIColor.warning) - } - - func testShowsCancelButtonForUserConfirmedFailedPublishedPosts() { - // Given - let post = PostBuilder(mainContext).published().with(remoteStatus: .failed).confirmedAutoUpload().build() - - // When - postCell.configure(with: post) - - // Then - XCTAssertTrue(postCell.retryButton.isHidden) - XCTAssertFalse(postCell.cancelAutoUploadButton.isHidden) - } - - /// TODO We will be showing "Publish" buttons like on Android instead. - func testDoesNotShowRetryButtonForUnconfirmedFailedLocalDraftsAndPublishedPosts() { - // A post can be unconfirmed if: - // - // - The user pressed the Published button (confirmed) but pressed Cancel in the Post List. - // - The user edited a published post but the editor crashed. - - // Arrange - let posts = [ - PostBuilder(mainContext).published().with(remoteStatus: .failed).build(), - PostBuilder(mainContext).drafted().with(remoteStatus: .failed).build() - ] - - // Act and Assert - for post in posts { - postCell.configure(with: post) - - XCTAssertTrue(postCell.retryButton.isHidden) - XCTAssertTrue(postCell.cancelAutoUploadButton.isHidden) - } - } - - func testDoesntShowFailedForCancelledAutoUploads() { - // Given - let post = PostBuilder(mainContext) - .published() - .with(remoteStatus: .failed) - .confirmedAutoUpload() - .cancelledAutoUpload() - .build() - - // When - postCell.configure(with: post) - - // Then - XCTAssertEqual(postCell.statusLabel.text, StatusMessages.localChanges) - XCTAssertEqual(postCell.statusLabel.textColor, UIColor.warning) - } - - func testShowsDraftWillBeUploadedMessageForDraftsWithRemote() { - // Given - let post = PostBuilder(mainContext).drafted().withRemote().with(remoteStatus: .failed).confirmedAutoUpload().build() - - // When - postCell.configure(with: post) - - // Then - XCTAssertEqual(postCell.statusLabel.text, i18n("We'll save your draft when your device is back online.")) - XCTAssertEqual(postCell.statusLabel.textColor, UIColor.warning) - } - - func testShowsDraftWillBeUploadedMessageForLocalDrafts() { - // Given - let post = PostBuilder(mainContext).drafted().with(remoteStatus: .failed).build() - - // When - postCell.configure(with: post) - - // Then - XCTAssertEqual(postCell.statusLabel.text, i18n("We'll save your draft when your device is back online.")) - XCTAssertEqual(postCell.statusLabel.textColor, UIColor.warning) - } - - func testShowsFailedMessageWhenAttemptToAutoUploadADraft() { - let post = PostBuilder(mainContext).drafted().with(remoteStatus: .failed).with(autoUploadAttemptsCount: 2).build() - - postCell.configure(with: post) - - expect(self.postCell.statusLabel.text).to(equal(i18n("We couldn't complete this action, but we'll try again later."))) - expect(self.postCell.statusLabel.textColor).to(equal(UIColor.warning)) - } - - func testFailedMessageWhenMaxNumberOfAttemptsToAutoDraftIsReached() { - let post = PostBuilder(mainContext).drafted().with(remoteStatus: .failed).with(autoUploadAttemptsCount: 3).build() - - postCell.configure(with: post) - - expect(self.postCell.statusLabel.text).to(equal(i18n("We couldn't complete this action."))) - expect(self.postCell.statusLabel.textColor).to(equal(UIColor.error)) - } - - func testFailedMessageWhenMaxNumberOfAttemptsToAutoDraftIsReachedAndPostHasFailedMedia() { - let post = PostBuilder(mainContext).with(image: "", status: .failed).with(remoteStatus: .failed).with(autoUploadAttemptsCount: 3).build() - - postCell.configure(with: post) - - expect(self.postCell.statusLabel.text).to(equal(i18n("We couldn't upload this media, and didn't publish the post."))) - expect(self.postCell.statusLabel.textColor).to(equal(UIColor.error)) - } - - func testShowsFailedMessageWhenAttemptToAutoUploadAPrivatePost() { - let post = PostBuilder(mainContext).private().with(remoteStatus: .failed).with(autoUploadAttemptsCount: 2).confirmedAutoUpload().build() - - postCell.configure(with: post) - - expect(self.postCell.statusLabel.text).to(equal(i18n("We couldn't publish this private post, but we'll try again later."))) - expect(self.postCell.statusLabel.textColor).to(equal(UIColor.warning)) - } - - func testFailedMessageWhenMaxNumberOfAttemptsToUploadPrivateIsReached() { - let post = PostBuilder(mainContext).private().with(remoteStatus: .failed).with(autoUploadAttemptsCount: 3).build() - - postCell.configure(with: post) - - expect(self.postCell.statusLabel.text).to(equal(i18n("We couldn't complete this action, and didn't publish this private post."))) - expect(self.postCell.statusLabel.textColor).to(equal(UIColor.error)) - } - - func testShowsPrivatePostWillBeUploadedMessageForPrivatePosts() { - let post = PostBuilder(mainContext).private().with(remoteStatus: .failed).confirmedAutoUpload().build() - - postCell.configure(with: post) - - expect(self.postCell.statusLabel.text).to(equal(i18n("We'll publish your private post when your device is back online."))) - expect(self.postCell.statusLabel.textColor).to(equal(UIColor.warning)) - } - - func testShowsFailedMessageWhenAttemptToAutoUploadAScheduledPost() { - let post = PostBuilder(mainContext).scheduled().with(remoteStatus: .failed).with(autoUploadAttemptsCount: 2).confirmedAutoUpload().build() - - postCell.configure(with: post) - - expect(self.postCell.statusLabel.text).to(equal(i18n("We couldn't schedule this post, but we'll try again later."))) - expect(self.postCell.statusLabel.textColor).to(equal(UIColor.warning)) - } - - func testFailedMessageWhenMaxNumberOfAttemptsToUploadScheduledIsReached() { - let post = PostBuilder(mainContext).scheduled().with(remoteStatus: .failed).with(autoUploadAttemptsCount: 3).build() - - postCell.configure(with: post) - - expect(self.postCell.statusLabel.text).to(equal(i18n("We couldn't complete this action, and didn't schedule this post."))) - expect(self.postCell.statusLabel.textColor).to(equal(UIColor.error)) - } - - func testShowsScheduledPostWillBeUploadedMessageForScheduledPosts() { - let post = PostBuilder(mainContext).scheduled().with(remoteStatus: .failed).confirmedAutoUpload().build() - - postCell.configure(with: post) - - expect(self.postCell.statusLabel.text).to(equal(i18n("We'll schedule your post when your device is back online."))) - expect(self.postCell.statusLabel.textColor).to(equal(UIColor.warning)) - } - - func testShowsPostWillBeSubmittedMessageForPendingPost() { - let post = PostBuilder(mainContext).pending().with(remoteStatus: .failed).confirmedAutoUpload().build() - - postCell.configure(with: post) - - expect(self.postCell.statusLabel.text).to(equal(i18n("We'll submit your post for review when your device is back online."))) - expect(self.postCell.statusLabel.textColor).to(equal(UIColor.warning)) - } - - func testShowsFailedMessageWhenAttemptToSubmitAPendingPost() { - let post = PostBuilder(mainContext).pending().with(remoteStatus: .failed).with(autoUploadAttemptsCount: 2).confirmedAutoUpload().build() - - postCell.configure(with: post) - - expect(self.postCell.statusLabel.text).to(equal(i18n("We couldn't submit this post for review, but we'll try again later."))) - expect(self.postCell.statusLabel.textColor).to(equal(UIColor.warning)) - } - - func testFailedMessageWhenMaxNumberOfAttemptsToSubmitPendingPostIsReached() { - let post = PostBuilder(mainContext).pending().with(remoteStatus: .failed).with(autoUploadAttemptsCount: 3).build() - - postCell.configure(with: post) - - expect(self.postCell.statusLabel.text).to(equal(i18n("We couldn't complete this action, and didn't submit this post for review."))) - expect(self.postCell.statusLabel.textColor).to(equal(UIColor.error)) - } - - func testFailedMessageForCanceledPostWithFailedMedias() { - let post = PostBuilder(mainContext).drafted().with(remoteStatus: .failed).with(image: "test.png", status: .failed, autoUploadFailureCount: 3).cancelledAutoUpload().revision().build() - - postCell.configure(with: post) - - expect(self.postCell.statusLabel.text).to(equal(i18n("We couldn't upload this media."))) - expect(self.postCell.statusLabel.textColor).to(equal(UIColor.error)) - } - - func testMessageWhenPostIsArevision() { - let post = PostBuilder(mainContext).revision().with(remoteStatus: .failed).with(remoteStatus: .local).build() - - postCell.configure(with: post) - - expect(self.postCell.statusLabel.text).to(equal("Local changes")) - expect(self.postCell.statusLabel.textColor).to(equal(UIColor.warning)) - } - - func testShowsUnsavedChangesMessageWhenPostHasAutosave() { - let post = PostBuilder(mainContext).with(remoteStatus: .sync).autosaved().build() - - postCell.configure(with: post) - - expect(self.postCell.statusLabel.text).to(equal(i18n("You've made unsaved changes to this post"))) - expect(self.postCell.statusLabel.textColor).to(equal(UIColor.warning(.shade40))) - } - - private func postCellFromNib() -> PostCardCell { - let bundle = Bundle(for: PostCardCell.self) - guard let postCell = bundle.loadNibNamed("PostCardCell", owner: nil)?.first as? PostCardCell else { - fatalError("PostCardCell does not exist") - } - - return postCell - } - -} - -class PostActionSheetDelegateMock: PostActionSheetDelegate { - var calledWithPost: AbstractPost? - var calledWithView: UIView? - - func showActionSheet(_ postCardStatusViewModel: PostCardStatusViewModel, from view: UIView) { - calledWithPost = postCardStatusViewModel.post - calledWithView = view - } -} diff --git a/WordPress/WordPressTest/PostCompactCellGhostableTests.swift b/WordPress/WordPressTest/PostCompactCellGhostableTests.swift index 261a17d90248..dc8333748f8e 100644 --- a/WordPress/WordPressTest/PostCompactCellGhostableTests.swift +++ b/WordPress/WordPressTest/PostCompactCellGhostableTests.swift @@ -71,7 +71,7 @@ class PostCompactCellGhostableTests: CoreDataTestCase { } private func postCellFromNib() -> PostCompactCell { - let bundle = Bundle(for: PostCardCell.self) + let bundle = Bundle(for: PostCompactCell.self) guard let postCell = bundle.loadNibNamed("PostCompactCell", owner: nil)?.first as? PostCompactCell else { fatalError("PostCell does not exist") } diff --git a/WordPress/WordPressTest/PostCompactCellTests.swift b/WordPress/WordPressTest/PostCompactCellTests.swift index 4e9965882982..25a6db0c5961 100644 --- a/WordPress/WordPressTest/PostCompactCellTests.swift +++ b/WordPress/WordPressTest/PostCompactCellTests.swift @@ -3,7 +3,7 @@ import XCTest @testable import WordPress -class PostCompactCellTests: XCTestCase { +class PostCompactCellTests: CoreDataTestCase { var postCell: PostCompactCell! @@ -12,7 +12,7 @@ class PostCompactCellTests: XCTestCase { } func testShowImageWhenAvailable() { - let post = PostBuilder().withImage().build() + let post = PostBuilder(mainContext).withImage().build() postCell.configure(with: post) @@ -20,7 +20,7 @@ class PostCompactCellTests: XCTestCase { } func testHideImageWhenNotAvailable() { - let post = PostBuilder().build() + let post = PostBuilder(mainContext).build() postCell.configure(with: post) @@ -28,7 +28,7 @@ class PostCompactCellTests: XCTestCase { } func testShowPostTitle() { - let post = PostBuilder().with(title: "Foo bar").build() + let post = PostBuilder(mainContext).with(title: "Foo bar").build() postCell.configure(with: post) @@ -36,7 +36,7 @@ class PostCompactCellTests: XCTestCase { } func testShowDate() { - let post = PostBuilder().with(remoteStatus: .sync) + let post = PostBuilder(mainContext).with(remoteStatus: .sync) .with(dateCreated: Date()).build() postCell.configure(with: post) @@ -44,20 +44,8 @@ class PostCompactCellTests: XCTestCase { XCTAssertEqual(postCell.timestampLabel.text, "now") } - func testMoreAction() { - let postActionSheetDelegateMock = PostActionSheetDelegateMock() - let post = PostBuilder().published().build() - postCell.configure(with: post) - postCell.setActionSheetDelegate(postActionSheetDelegateMock) - - postCell.menuButton.sendActions(for: .touchUpInside) - - XCTAssertEqual(postActionSheetDelegateMock.calledWithPost, post) - XCTAssertEqual(postActionSheetDelegateMock.calledWithView, postCell.menuButton) - } - func testStatusAndBadgeLabels() { - let post = PostBuilder().with(remoteStatus: .sync) + let post = PostBuilder(mainContext).with(remoteStatus: .sync) .with(dateCreated: Date()).is(sticked: true).build() postCell.configure(with: post) @@ -66,7 +54,7 @@ class PostCompactCellTests: XCTestCase { } func testHideBadgesWhenEmpty() { - let post = PostBuilder().build() + let post = PostBuilder(mainContext).build() postCell.configure(with: post) @@ -75,7 +63,7 @@ class PostCompactCellTests: XCTestCase { } func testShowBadgesWhenNotEmpty() { - let post = PostBuilder() + let post = PostBuilder(mainContext) .with(remoteStatus: .sync) .build() @@ -86,7 +74,7 @@ class PostCompactCellTests: XCTestCase { } func testShowProgressView() { - let post = PostBuilder() + let post = PostBuilder(mainContext) .with(remoteStatus: .pushing) .published().build() @@ -96,7 +84,7 @@ class PostCompactCellTests: XCTestCase { } func testHideProgressView() { - let post = PostBuilder() + let post = PostBuilder(mainContext) .with(remoteStatus: .sync) .published().build() @@ -107,7 +95,7 @@ class PostCompactCellTests: XCTestCase { func testShowsWarningMessageForFailedPublishedPosts() { // Given - let post = PostBuilder().published().with(remoteStatus: .failed).confirmedAutoUpload().build() + let post = PostBuilder(mainContext).published().with(remoteStatus: .failed).confirmedAutoUpload().build() // When postCell.configure(with: post) diff --git a/WordPress/WordPressTest/PostListExcessiveLoadMoreTests.swift b/WordPress/WordPressTest/PostListExcessiveLoadMoreTests.swift deleted file mode 100644 index fa12d5f5ca9d..000000000000 --- a/WordPress/WordPressTest/PostListExcessiveLoadMoreTests.swift +++ /dev/null @@ -1,40 +0,0 @@ -import XCTest -@testable import WordPress - -class PostListExcessiveLoadMoreTests: XCTestCase { - - func testCount1() { - let counter = LoadMoreCounter(startingCount: 0, dryRun: true) - XCTAssertTrue(counter.increment(properties: [:])) - } - - func testCount10() { - let counter = LoadMoreCounter(startingCount: 9, dryRun: true) - XCTAssertFalse(counter.increment(properties: [:])) - } - - func testCount100() { - let counter = LoadMoreCounter(startingCount: 99, dryRun: true) - XCTAssertTrue(counter.increment(properties: [:])) - } - - func testCount200() { - let counter = LoadMoreCounter(startingCount: 199, dryRun: true) - XCTAssertFalse(counter.increment(properties: [:])) - } - - func testCount1000() { - let counter = LoadMoreCounter(startingCount: 999, dryRun: true) - XCTAssertTrue(counter.increment(properties: [:])) - } - - func testCount10000() { - let counter = LoadMoreCounter(startingCount: 9999, dryRun: true) - XCTAssertTrue(counter.increment(properties: [:])) - } - - func testCount50() { - let counter = LoadMoreCounter(startingCount: 49, dryRun: true) - XCTAssertFalse(counter.increment(properties: [:])) - } -} diff --git a/WordPress/WordPressTest/PostListTableViewHandlerTests.swift b/WordPress/WordPressTest/PostListTableViewHandlerTests.swift deleted file mode 100644 index 90d812ebd94f..000000000000 --- a/WordPress/WordPressTest/PostListTableViewHandlerTests.swift +++ /dev/null @@ -1,51 +0,0 @@ -import UIKit -import XCTest - -@testable import WordPress - -class PostListTableViewHandlerTests: XCTestCase { - - func testReturnAResultsControllerForViewingAndOtherWhenSearching() { - let postListHandlerMock = PostListHandlerMock() - let postListHandler = PostListTableViewHandler() - postListHandler.delegate = postListHandlerMock - let defaultResultsController = postListHandler.resultsController - - postListHandler.isSearching = true - - XCTAssertNotEqual(defaultResultsController, postListHandler.resultsController) - } -} - -class PostListHandlerMock: NSObject, WPTableViewHandlerDelegate { - func managedObjectContext() -> NSManagedObjectContext { - return setUpInMemoryManagedObjectContext() - } - - func fetchRequest() -> NSFetchRequest? { - let a = NSFetchRequest(entityName: String(describing: Post.self)) - a.sortDescriptors = [NSSortDescriptor(key: BasePost.statusKeyPath, ascending: true)] - return a - } - - func configureCell(_ cell: UITableViewCell, at indexPath: IndexPath) { } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { } - - private func setUpInMemoryManagedObjectContext() -> NSManagedObjectContext { - let managedObjectModel = NSManagedObjectModel.mergedModel(from: [Bundle.main])! - - let persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: managedObjectModel) - - do { - try persistentStoreCoordinator.addPersistentStore(ofType: NSInMemoryStoreType, configurationName: nil, at: nil, options: nil) - } catch { - print("Adding in-memory persistent store failed") - } - - let managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) - managedObjectContext.persistentStoreCoordinator = persistentStoreCoordinator - - return managedObjectContext - } -} diff --git a/WordPress/WordPressTest/PostRepositoryPostsListTests.swift b/WordPress/WordPressTest/PostRepositoryPostsListTests.swift new file mode 100644 index 000000000000..2634d0f34353 --- /dev/null +++ b/WordPress/WordPressTest/PostRepositoryPostsListTests.swift @@ -0,0 +1,81 @@ +import Foundation +import XCTest +import OHHTTPStubs + +@testable import WordPress + +class PostRepositoryPostsListTests: CoreDataTestCase { + + var repository: PostRepository! + var blogID: TaggedManagedObjectID! + + override func setUp() async throws { + repository = PostRepository(coreDataStack: contextManager) + + let loggedIn = try await signIn() + blogID = try await contextManager.performAndSave { + let blog = try BlogBuilder($0) + .with(dotComID: 42) + .withAccount(id: loggedIn) + .build() + return TaggedManagedObjectID(blog) + } + } + + override func tearDown() async throws { + HTTPStubs.removeAllStubs() + } + + func testPagination() async throws { + // Given there are 15 published posts on the site + stubGetPostsList(type: "post", total: 15) + + // When fetching all of the posts + let firstPage = try await repository.paginate(type: Post.self, statuses: [.publish], offset: 0, number: 10, in: blogID) + let secondPage = try await repository.paginate(type: Post.self, statuses: [.publish], offset: 10, number: 10, in: blogID) + + XCTAssertEqual(firstPage.count, 10) + XCTAssertEqual(secondPage.count, 5) + + // All of the posts are saved + let total = await contextManager.performQuery { $0.countObjects(ofType: Post.self) } + XCTAssertEqual(total, 15) + } + + func testSearching() async throws { + // Given there are 15 published posts on the site + stubGetPostsList(type: "post", total: 15) + + // When fetching all of the posts + let _ = try await repository.paginate(type: Post.self, statuses: [.publish], offset: 0, number: 15, in: blogID) + + // There should 15 posts saved locally before performing search + var total = await contextManager.performQuery { $0.countObjects(ofType: Post.self) } + XCTAssertEqual(total, 15) + + // Perform search + let postIDs: [TaggedManagedObjectID] = try await repository.search(input: "1", statuses: [.publish], tag: nil, offset: 0, limit: 1, orderBy: .byDate, descending: true, in: blogID) + XCTAssertEqual(postIDs.count, 1) + + // There should still be 15 posts after the search: no local posts should be deleted + total = await contextManager.performQuery { $0.countObjects(ofType: Post.self) } + XCTAssertEqual(total, 15) + } + +} + +extension CoreDataTestCase { + + func signIn() async throws -> NSManagedObjectID { + let loggedIn = await contextManager.performQuery { + try? WPAccount.lookupDefaultWordPressComAccount(in: $0)?.objectID + } + if let loggedIn { + return loggedIn + } + + let service = AccountService(coreDataStack: contextManager) + return service.createOrUpdateAccount(withUsername: "test-user", authToken: "test-token") + } + +} diff --git a/WordPress/WordPressTest/PostRepositoryTests.swift b/WordPress/WordPressTest/PostRepositoryTests.swift index d5c020b324e4..1dbf28d09952 100644 --- a/WordPress/WordPressTest/PostRepositoryTests.swift +++ b/WordPress/WordPressTest/PostRepositoryTests.swift @@ -54,7 +54,7 @@ class PostRepositoryTests: CoreDataTestCase { func testDeletePost() async throws { let postID = try await contextManager.performAndSave { context in - let post = PostBuilder(context).withRemote().with(title: "Post: Test").build() + let post = PostBuilder(context).with(status: .trash).withRemote().with(title: "Post: Test").build() return TaggedManagedObjectID(post) } @@ -69,7 +69,7 @@ class PostRepositoryTests: CoreDataTestCase { func testDeletePostWithRemoteFailure() async throws { let postID = try await contextManager.performAndSave { context in - let post = PostBuilder(context).withRemote().with(title: "Post: Test").build() + let post = PostBuilder(context).with(status: .trash).withRemote().with(title: "Post: Test").build() return TaggedManagedObjectID(post) } @@ -89,7 +89,7 @@ class PostRepositoryTests: CoreDataTestCase { func testDeleteHistory() async throws { let (firstRevision, secondRevision) = try await contextManager.performAndSave { context in - let first = PostBuilder(context).withRemote().with(title: "Post: Test").build() + let first = PostBuilder(context).with(status: .trash).withRemote().with(title: "Post: Test").build() let second = first.createRevision() second.postTitle = "Edited" return (TaggedManagedObjectID(first), TaggedManagedObjectID(second)) @@ -107,7 +107,7 @@ class PostRepositoryTests: CoreDataTestCase { func testDeleteLatest() async throws { let (firstRevision, secondRevision) = try await contextManager.performAndSave { context in - let first = PostBuilder(context).withRemote().with(title: "Post: Test").build() + let first = PostBuilder(context).with(status: .trash).withRemote().with(title: "Post: Test").build() let second = first.createRevision() second.postTitle = "Edited" return (TaggedManagedObjectID(first), TaggedManagedObjectID(second)) @@ -174,6 +174,33 @@ class PostRepositoryTests: CoreDataTestCase { XCTAssertTrue(isPostDeleted) } + func testTrashingAPostWillUpdateItsRevisionStatusAfterSyncProperty() async throws { + // Arrange + let (postID, revisionID) = try await contextManager.performAndSave { context in + let post = PostBuilder(context).with(statusAfterSync: .publish).withRemote().build() + let revision = post.createRevision() + return (TaggedManagedObjectID(post), TaggedManagedObjectID(revision)) + } + + let remotePost = RemotePost(siteID: 1, status: "trash", title: "Post: Test", content: "New content")! + remotePost.type = "post" + remoteMock.trashPostResult = .success(remotePost) + + // Act + try await repository.trash(postID) + + // Assert + let postStatusAfterSync = try await contextManager.performQuery { try $0.existingObject(with: postID).statusAfterSync } + let postStatus = try await contextManager.performQuery { try $0.existingObject(with: postID).status } + let revisionStatusAfterSync = try await contextManager.performQuery { try $0.existingObject(with: revisionID).statusAfterSync } + let revisionStatus = try await contextManager.performQuery { try $0.existingObject(with: revisionID).status } + + XCTAssertEqual(postStatusAfterSync, .trash) + XCTAssertEqual(postStatus, .trash) + XCTAssertEqual(revisionStatusAfterSync, .trash) + XCTAssertEqual(revisionStatus, .trash) + } + func testRestorePost() async throws { let postID = try await contextManager.performAndSave { context in let post = PostBuilder(context).withRemote().with(status: .trash).with(title: "Post: Test").build() @@ -214,6 +241,196 @@ class PostRepositoryTests: CoreDataTestCase { } } + func testFetchAllPagesAPIError() async throws { + // Use an empty array to simulate an HTTP API error + remoteMock.remotePostsToReturnOnSyncPostsOfType = [] + + do { + let _ = try await repository.fetchAllPages(statuses: [], in: blogID).value + XCTFail("The above call should throw") + } catch { + // Do nothing. + } + } + + func testFetchAllPagesStopsOnEmptyAPIResponse() async throws { + // Given two pages of API result: first page returns 100 page instances, and the second page returns an empty result. + remoteMock.remotePostsToReturnOnSyncPostsOfType = [ + try (1...100).map { + let post = try XCTUnwrap(RemotePost(siteID: NSNumber(value: $0), status: "publish", title: "Post: Test", content: "This is a test post")) + post.type = "page" + return post + }, + [] + ] + + let pages = try await repository.fetchAllPages(statuses: [.publish], in: blogID).value + XCTAssertEqual(pages.count, 100) + } + + func testFetchAllPagesStopsOnNonFullPageAPIResponse() async throws { + // Given two pages of API result: first page returns 100 page instances, and the second page returns 10 (any amount that's less than 100) page instances. + remoteMock.remotePostsToReturnOnSyncPostsOfType = [ + try (1...100).map { + let post = try XCTUnwrap(RemotePost(siteID: NSNumber(value: $0), status: "publish", title: "Post: Test", content: "This is a test post")) + post.type = "page" + return post + }, + try (1...10).map { + let post = try XCTUnwrap(RemotePost(siteID: NSNumber(value: $0), status: "publish", title: "Post: Test", content: "This is a test post")) + post.type = "page" + return post + }, + ] + + let pages = try await repository.fetchAllPages(statuses: [.publish], in: blogID).value + XCTAssertEqual(pages.count, 110) + } + + func testCancelFetchAllPages() async throws { + remoteMock.remotePostsToReturnOnSyncPostsOfType = try (1...10).map { pageNo in + try (1...100).map { + let post = try XCTUnwrap(RemotePost(siteID: NSNumber(value: pageNo * 100 + $0), status: "publish", title: "Post: Test", content: "This is a test post")) + post.type = "page" + return post + } + } + + let cancelled = expectation(description: "Fetching task returns cancellation error") + let task = repository.fetchAllPages(statuses: [.publish], in: blogID) + + DispatchQueue.global().asyncAfter(deadline: .now() + .microseconds(100)) { + task.cancel() + } + + do { + let _ = try await task.value + } catch is CancellationError { + cancelled.fulfill() + } + + await fulfillment(of: [cancelled], timeout: 0.3) + } + + func testFetchTwoFullPages() async throws { + // `fetchAllPages` fetchs 100 page instances at at time. Here we simulate two full pages result. + remoteMock.remotePostsToReturnOnSyncPostsOfType = try (1...2).map { pageNo in + try (1...100).map { + let post = try XCTUnwrap(RemotePost(siteID: NSNumber(value: pageNo * 1000 + $0), status: "publish", title: "Post: Test", content: "This is a test post")) + post.type = "page" + return post + } + } + [[]] + + let allPages = try await repository.fetchAllPages(statuses: [.publish], in: blogID).value + XCTAssertEqual(allPages.count, 200) + } + + // This test takes one minute to complete on CI. We'll disable it for now and potentially re-enable + // it after CI is migrated to Apple Silicon agents. + func _testFetchManyManyPages() async throws { + // Here we simulate a site that has a super large number of pages. + remoteMock.remotePostsToReturnOnSyncPostsOfType = try (1...99).map { pageNo in + try (1...100).map { + let post = try XCTUnwrap(RemotePost(siteID: NSNumber(value: pageNo * 1000 + $0), status: "publish", title: "Post: Test", content: "This is a test post")) + post.type = "page" + return post + } + } + [[]] + + let allPages = try await repository.fetchAllPages(statuses: [.publish], in: blogID).value + XCTAssertEqual(allPages.count, 9_900) + } + + func testFetchAllPagesPurgesDeletedPages() async throws { + let postToBeKept = try XCTUnwrap(RemotePost(siteID: 1, status: "publish", title: "Post: Kept", content: "This is a test post")) + postToBeKept.postID = 100 + postToBeKept.type = "page" + let postToBeDeleted = try XCTUnwrap(RemotePost(siteID: 1, status: "publish", title: "Post: Deleted", content: "This is a test post")) + postToBeDeleted.postID = 200 + postToBeDeleted.type = "page" + + // The first fetch returns both page instances. + remoteMock.remotePostsToReturnOnSyncPostsOfType = [ + [postToBeKept, postToBeDeleted], + ] + let firstFetch = try await repository.fetchAllPages(statuses: [.publish], in: blogID).value + XCTAssertEqual(firstFetch.count, 2) + try await contextManager.performQuery { context in + let first = try context.existingObject(with: XCTUnwrap(firstFetch.first)) + let second = try context.existingObject(with: XCTUnwrap(firstFetch.last)) + XCTAssertEqual(first.postID, 100) + XCTAssertEqual(second.postID, 200) + } + + // The second fetch only returns one of them, to simulate a situation where the other page has been deleted from the site. + remoteMock.remotePostsToReturnOnSyncPostsOfType = [ + [postToBeKept], + ] + let secondFetch = try await repository.fetchAllPages(statuses: [.publish], in: blogID).value + XCTAssertEqual(secondFetch.count, 1) + try await contextManager.performQuery { context in + let page = try context.existingObject(with: XCTUnwrap(firstFetch.first)) + XCTAssertEqual(page.postID, postToBeKept.postID) + } + } + + func testFetchAllPagesKeepPagesWithOtherStatus() async throws { + let remotePage = try XCTUnwrap(RemotePost(siteID: 1, status: "publish", title: "Post: Kept", content: "This is a test post")) + remotePage.postID = 100 + remotePage.type = "page" + + // The first fetch returns a published post + remoteMock.remotePostsToReturnOnSyncPostsOfType = [[remotePage]] + let firstFetch = try await repository.fetchAllPages(statuses: [.publish], in: blogID).value + try await contextManager.performQuery { context in + let page = try context.existingObject(with: XCTUnwrap(firstFetch.first)) + XCTAssertEqual(page.postID, remotePage.postID) + } + + // The second fetch returns empty result, because there is no draft on the site + remoteMock.remotePostsToReturnOnSyncPostsOfType = [[]] + let secondFetch = try await repository.fetchAllPages(statuses: [.draft], in: blogID).value + XCTAssertTrue(secondFetch.isEmpty) + + let pageExists = await contextManager.performQuery { context in + (try? context.existingObject(with: XCTUnwrap(firstFetch.first))) != nil + } + XCTAssertTrue(pageExists, "The previously fetched published pages is not deleted by the draft pages fetching request") + } + + func testFetchAllPagesKeepLocalEdits() async throws { + let remotePage = try XCTUnwrap(RemotePost(siteID: 1, status: "publish", title: "Post: Kept", content: "This is a test post")) + remotePage.postID = 100 + remotePage.type = "page" + remoteMock.remotePostsToReturnOnSyncPostsOfType = [[remotePage], [remotePage]] + + // The first fetch returns a published post + let firstFetch = try await repository.fetchAllPages(statuses: [.publish], in: blogID).value + try await contextManager.performQuery { context in + let page = try context.existingObject(with: XCTUnwrap(firstFetch.first)) + XCTAssertEqual(page.postID, remotePage.postID) + } + + // Edit the fetched page. + let localEditID = try await contextManager.performAndSave { context in + let page = try context.existingObject(with: XCTUnwrap(firstFetch.first)) + let localEdit = page.createRevision() + localEdit.postTitle = "Changes changes and changes" + return TaggedManagedObjectID(localEdit) + } + + // The second fetch returns the same result as the previous one. + let secondFetch = try await repository.fetchAllPages(statuses: [.publish], in: blogID).value + XCTAssertEqual(firstFetch, secondFetch) + + try await contextManager.performQuery { context in + let localEdit = try context.existingObject(with: localEditID) + XCTAssertNotNil(localEdit.original) + try XCTAssertEqual(XCTUnwrap(localEdit.original).objectID, XCTUnwrap(secondFetch.first).objectID) + } + } + } // These mock classes are copied from PostServiceWPComTests. We can't simply remove the `private` in the original class @@ -238,7 +455,7 @@ private class PostServiceRESTMock: PostServiceRemoteREST { } var remotePostToReturnOnGetPostWithID: RemotePost? - var remotePostsToReturnOnSyncPostsOfType = [RemotePost]() + var remotePostsToReturnOnSyncPostsOfType = [[RemotePost]]() // Each element contains an array of RemotePost for one API request. var remotePostToReturnOnUpdatePost: RemotePost? var remotePostToReturnOnCreatePost: RemotePost? @@ -262,7 +479,15 @@ private class PostServiceRESTMock: PostServiceRemoteREST { } override func getPostsOfType(_ postType: String!, options: [AnyHashable: Any]! = [:], success: (([RemotePost]?) -> Void)!, failure: ((Error?) -> Void)!) { - success(self.remotePostsToReturnOnSyncPostsOfType) + guard !remotePostsToReturnOnSyncPostsOfType.isEmpty else { + failure(testError()) + return + } + + let result = remotePostsToReturnOnSyncPostsOfType.removeFirst() + DispatchQueue.main.asyncAfter(deadline: .now() + .microseconds(50)) { + success(result) + } } override func update(_ post: RemotePost!, success: ((RemotePost?) -> Void)!, failure: ((Error?) -> Void)!) { diff --git a/WordPress/WordPressTest/PostSearchViewModelTests.swift b/WordPress/WordPressTest/PostSearchViewModelTests.swift new file mode 100644 index 000000000000..97cf8001e0cc --- /dev/null +++ b/WordPress/WordPressTest/PostSearchViewModelTests.swift @@ -0,0 +1,39 @@ +import XCTest + +@testable import WordPress + +class PostSearchViewModelTests: XCTestCase { + func testThatAdjacentRangesAreCollapsed() throws { + // GIVEN + let string = NSMutableAttributedString(string: "one two xxxxx one") + + // WHEN + PostSearchViewModel.highlight(terms: ["one", "two"], in: string) + + // THEN + XCTAssertTrue(string.hasAttribute(.backgroundColor, in: NSRange(location: 0, length: 7))) + XCTAssertFalse(string.hasAttribute(.backgroundColor, in: NSRange(location: 7, length: 7))) + XCTAssertTrue(string.hasAttribute(.backgroundColor, in: NSRange(location: 14, length: 3))) + } + + func testThatCaseIsIgnored() { + // GIVEN + let string = NSMutableAttributedString(string: "One xxxxx óne") + + // WHEN + PostSearchViewModel.highlight(terms: ["one"], in: string) + + // THEN + XCTAssertTrue(string.hasAttribute(.backgroundColor, in: NSRange(location: 0, length: 3))) + XCTAssertFalse(string.hasAttribute(.backgroundColor, in: NSRange(location: 3, length: 7))) + XCTAssertTrue(string.hasAttribute(.backgroundColor, in: NSRange(location: 10, length: 3))) + } +} + +private extension NSAttributedString { + func hasAttribute(_ attribute: NSAttributedString.Key, in range: NSRange) -> Bool { + var effectiveRange: NSRange = .init() + let attributes = self.attributes(at: range.location, effectiveRange: &effectiveRange) + return attributes[attribute] != nil && effectiveRange == range + } +} diff --git a/WordPress/WordPressTest/PostTests.swift b/WordPress/WordPressTest/PostTests.swift index 1a2e672f5b8c..9e1299153a1d 100644 --- a/WordPress/WordPressTest/PostTests.swift +++ b/WordPress/WordPressTest/PostTests.swift @@ -254,6 +254,9 @@ class PostTests: CoreDataTestCase { post.postTitle = "hello world" XCTAssertEqual(post.titleForDisplay(), "hello world") + post.postTitle = "hello world" + XCTAssertEqual(post.titleForDisplay(), "hello world") + post.postTitle = " " XCTAssertEqual(post.titleForDisplay(), NSLocalizedString("(no title)", comment: "(no title)")) } diff --git a/WordPress/WordPressTest/PostsListAPIStub.swift b/WordPress/WordPressTest/PostsListAPIStub.swift new file mode 100644 index 000000000000..feca07dfef15 --- /dev/null +++ b/WordPress/WordPressTest/PostsListAPIStub.swift @@ -0,0 +1,143 @@ +import Foundation +import XCTest +import OHHTTPStubs + +@testable import WordPress + +extension XCTestCase { + /// This is a helper function to create HTTP stubs for fetching posts(GET /sites/%s/posts) requests. + /// + /// The returned fake posts have only basic properties. The stubs ensure post id is unique and starts from 1. + /// But it does not promise the returned posts match the request filters (like status). + /// + /// You can use the `update` closure to update the returned posts if needed. + /// + /// Here are the supported features: + /// - Pagination. The stubs simulates `total` number of posts in the site, to handle paginated request accordingly. + /// - Search, but limited. Search is based on title. All fake posts have a title like "Random Post - [post-id]", where post id starts from 1. So, search "1" returns the posts whose id has "1" in it (1, 1x, x1, and so on). + /// + /// Here are unsupported features: + /// - Order. The sorting related arguments are ignored. + /// - Filter by status. The status argument is ignored. + func stubGetPostsList(type: String, total: Int, update: ((inout [String: Any]) -> Void)? = nil) { + let allPosts = (1...total).map { id -> [String: Any] in + [ + "ID": id, + "title": "Random Post - \(id)", + "content": "This is a test.", + "status": BasePost.Status.publish.rawValue, + "type": type + ] + } + + let handle = stub(condition: isMethodGET() && pathMatches(#"/sites/\d+/posts"#, options: [])) { request in + let queryItems = URLComponents(url: request.url!, resolvingAgainstBaseURL: true)?.queryItems ?? [] + + var result = allPosts + + if let search = queryItems.first(where: { $0.name == "search" })?.value { + result = result.filter { + let title = $0["title"] as? String + return title?.contains(search) == true + } + } + + var number = (queryItems.first { $0.name == "number" }?.value.flatMap { Int($0) }) ?? 0 + number = number == 0 ? 20 : number // The REST API uses the default value 20 when number is 0. + let offset = (queryItems.first { $0.name == "offset" }?.value.flatMap { Int($0) }) ?? 0 + let upperBound = number == 0 ? result.endIndex : max(offset, offset + number - 1) + let allowed = 0..! + + override func setUp() async throws { + repository = PostRepository(coreDataStack: contextManager) + + let loggedIn = try await signIn() + blogID = try await contextManager.performAndSave { + let blog = try BlogBuilder($0) + .with(dotComID: 42) + .withAccount(id: loggedIn) + .build() + return TaggedManagedObjectID(blog) + } + } + + func testPostsListStubReturnPostsAsRequested() async throws { + stubGetPostsList(type: "post", total: 20) + + var result = try await repository.search(type: Post.self, input: nil, statuses: [], tag: nil, offset: 0, limit: 10, orderBy: .byDate, descending: true, in: blogID) + XCTAssertEqual(result.count, 10) + + result = try await repository.search(type: Post.self, input: nil, statuses: [], tag: nil, offset: 0, limit: 20, orderBy: .byDate, descending: true, in: blogID) + XCTAssertEqual(result.count, 20) + + result = try await repository.search(type: Post.self, input: nil, statuses: [], tag: nil, offset: 0, limit: 30, orderBy: .byDate, descending: true, in: blogID) + XCTAssertEqual(result.count, 20) + } + + func testPostsListStubReturnPostsAtCorrectPosition() async throws { + stubGetPostsList(type: "post", total: 20) + + let all = try await repository.search(type: Post.self, input: nil, statuses: [], tag: nil, offset: 0, limit: 30, orderBy: .byDate, descending: true, in: blogID) + + var result = try await repository.paginate(type: Post.self, statuses: [], offset: 0, number: 5, in: blogID) + XCTAssertEqual(result, Array(all[0..<5])) + + result = try await repository.paginate(type: Post.self, statuses: [], offset: 3, number: 2, in: blogID) + XCTAssertEqual(result, [all[3], all[4]]) + } + + func testPostsListStubReturnPostsSearch() async throws { + stubGetPostsList(type: "post", total: 10) + + let all = try await repository.search(type: Post.self, input: nil, statuses: [], tag: nil, offset: 0, limit: 30, orderBy: .byDate, descending: true, in: blogID) + + var result = try await repository.search(type: Post.self, input: "1", statuses: [], tag: nil, offset: 0, limit: 1, orderBy: .byDate, descending: true, in: blogID) + XCTAssertEqual(result, [all[0]]) + + result = try await repository.search(type: Post.self, input: "2", statuses: [], tag: nil, offset: 0, limit: 1, orderBy: .byDate, descending: true, in: blogID) + XCTAssertEqual(result, [all[1]]) + } + + func testPostsListStubReturnDefaultNumberOfPosts() async throws { + stubGetPostsList(type: "post", total: 100) + + let result = try await repository.search(type: Post.self, input: nil, statuses: [], tag: nil, offset: 0, limit: 0, orderBy: .byDate, descending: true, in: blogID) + XCTAssertEqual(result.count, 20) + } + +} diff --git a/WordPress/WordPressTest/PrepublishingNudgesViewControllerTests.swift b/WordPress/WordPressTest/PrepublishingNudgesViewControllerTests.swift index 61a236108ecc..dd08fd80ac39 100644 --- a/WordPress/WordPressTest/PrepublishingNudgesViewControllerTests.swift +++ b/WordPress/WordPressTest/PrepublishingNudgesViewControllerTests.swift @@ -3,7 +3,7 @@ import Nimble @testable import WordPress -class PrepublishingNudgesViewControllerTests: XCTestCase { +class PrepublishingNudgesViewControllerTests: CoreDataTestCase { override class func setUp() { super.setUp() @@ -21,7 +21,7 @@ class PrepublishingNudgesViewControllerTests: XCTestCase { /// Call the completion block when the "Publish" button is pressed /// func testCallCompletionBlockWhenButtonTapped() { - var post = PostBuilder().build() + var post = PostBuilder(mainContext).build() var returnedPost: AbstractPost? let prepublishingViewController = PrepublishingViewController(post: post, identifiers: [.schedule, .visibility, .tags, .categories]) { result in switch result { diff --git a/WordPress/WordPressTest/PromptRemindersSchedulerTests.swift b/WordPress/WordPressTest/PromptRemindersSchedulerTests.swift index 4b741274838a..ae2e6e07580c 100644 --- a/WordPress/WordPressTest/PromptRemindersSchedulerTests.swift +++ b/WordPress/WordPressTest/PromptRemindersSchedulerTests.swift @@ -119,6 +119,7 @@ class PromptRemindersSchedulerTests: XCTestCase { scheduler.schedule(schedule, for: blog, time: makeTime(hour: expectedHour, minute: expectedMinute)) { result in guard case .success = result else { XCTFail("Expected a success result, but got error: \(result)") + expectation.fulfill() return } @@ -405,17 +406,17 @@ private extension PromptRemindersSchedulerTests { objects.append([ "id": 100 + i, "text": "Prompt text \(i)", - "title": "Prompt title \(i)", - "content": "Prompt content \(i)", "attribution": "", "date": Self.dateFormatter.string(from: date), "answered": false, "answered_users_count": 0, - "answered_users_sample": [[String: Any]]() + "answered_users_sample": [[String: Any]](), + "answered_link": "", + "answered_link_text": "View all responses" ] as [String: Any]) } - return ["prompts": objects] + return objects } func makeBlog() -> Blog { diff --git a/WordPress/WordPressTest/ReaderDetailCoordinatorTests.swift b/WordPress/WordPressTest/ReaderDetailCoordinatorTests.swift index edb80831a362..ba300c8a224c 100644 --- a/WordPress/WordPressTest/ReaderDetailCoordinatorTests.swift +++ b/WordPress/WordPressTest/ReaderDetailCoordinatorTests.swift @@ -3,7 +3,7 @@ import Nimble @testable import WordPress -class ReaderDetailCoordinatorTests: XCTestCase { +class ReaderDetailCoordinatorTests: CoreDataTestCase { /// Given a post ID, site ID and isFeed fetches the post from the service /// @@ -36,7 +36,7 @@ class ReaderDetailCoordinatorTests: XCTestCase { /// Inform the view to render a post after it is fetched /// func testUpdateViewWithRetrievedPost() { - let post: ReaderPost = ReaderPostBuilder().build() + let post = makeReaderPost() let serviceMock = ReaderPostServiceMock() serviceMock.returnPost = post let viewMock = ReaderDetailViewMock() @@ -98,7 +98,7 @@ class ReaderDetailCoordinatorTests: XCTestCase { /// If a post is given, do not call the servce and render the content right away /// func testGivenAPostRenderItRightAway() { - let post: ReaderPost = ReaderPostBuilder().build() + let post = makeReaderPost() let serviceMock = ReaderPostServiceMock() let viewMock = ReaderDetailViewMock() let coordinator = ReaderDetailCoordinator(readerPostService: serviceMock, view: viewMock) @@ -113,7 +113,7 @@ class ReaderDetailCoordinatorTests: XCTestCase { /// Tell the view to show a loading indicator when start is called /// func testStartCallsTheViewToShowLoader() { - let post: ReaderPost = ReaderPostBuilder().build() + let post = makeReaderPost() let serviceMock = ReaderPostServiceMock() let viewMock = ReaderDetailViewMock() let coordinator = ReaderDetailCoordinator(readerPostService: serviceMock, view: viewMock) @@ -128,7 +128,7 @@ class ReaderDetailCoordinatorTests: XCTestCase { /// func testShowShareSheet() { let button = UIView() - let post: ReaderPost = ReaderPostBuilder().build() + let post = makeReaderPost() let serviceMock = ReaderPostServiceMock() let viewMock = ReaderDetailViewMock() let postSharingControllerMock = PostSharingControllerMock() @@ -149,7 +149,7 @@ class ReaderDetailCoordinatorTests: XCTestCase { /// Present a site preview in the current view stack /// func testShowPresentSitePreview() { - let post: ReaderPost = ReaderPostBuilder().build() + let post = makeReaderPost() post.siteID = 1 post.isExternal = false let serviceMock = ReaderPostServiceMock() @@ -168,7 +168,7 @@ class ReaderDetailCoordinatorTests: XCTestCase { /// Present a tag in the current view stack /// func testShowPresentTag() { - let post: ReaderPost = ReaderPostBuilder().build() + let post = makeReaderPost() post.primaryTagSlug = "tag" let serviceMock = ReaderPostServiceMock() let viewMock = ReaderDetailViewMock() @@ -186,7 +186,7 @@ class ReaderDetailCoordinatorTests: XCTestCase { /// Present an image in the view controller /// func testShowPresentImage() { - let post: ReaderPost = ReaderPostBuilder().build() + let post = makeReaderPost() let serviceMock = ReaderPostServiceMock() let viewMock = ReaderDetailViewMock() let coordinator = ReaderDetailCoordinator(readerPostService: serviceMock, view: viewMock) @@ -200,7 +200,7 @@ class ReaderDetailCoordinatorTests: XCTestCase { /// Present an URL in a new Reader Detail screen /// func testShowPresentURL() { - let post: ReaderPost = ReaderPostBuilder().build() + let post = makeReaderPost() let serviceMock = ReaderPostServiceMock() let viewMock = ReaderDetailViewMock() let coordinator = ReaderDetailCoordinator(readerPostService: serviceMock, view: viewMock) @@ -216,7 +216,7 @@ class ReaderDetailCoordinatorTests: XCTestCase { /// Present an URL in a webview controller /// func testShowPresentURLInWebViewController() { - let post: ReaderPost = ReaderPostBuilder().build() + let post = makeReaderPost() let serviceMock = ReaderPostServiceMock() let viewMock = ReaderDetailViewMock() let coordinator = ReaderDetailCoordinator(readerPostService: serviceMock, view: viewMock) @@ -231,7 +231,7 @@ class ReaderDetailCoordinatorTests: XCTestCase { /// Tell the view to scroll when URL is a hash link /// func testScrollWhenUrlIsHash() { - let post: ReaderPost = ReaderPostBuilder().build() + let post = makeReaderPost() let serviceMock = ReaderPostServiceMock() let viewMock = ReaderDetailViewMock() let coordinator = ReaderDetailCoordinator(readerPostService: serviceMock, view: viewMock) @@ -251,6 +251,10 @@ class ReaderDetailCoordinatorTests: XCTestCase { expect(coordinator.commentID).to(equal(10)) } + + func makeReaderPost() -> ReaderPost { + ReaderPostBuilder(mainContext).build() + } } // MARK: - Private Helpers diff --git a/WordPress/WordPressTest/ReaderDetailViewControllerTests.swift b/WordPress/WordPressTest/ReaderDetailViewControllerTests.swift index 36d2b86616eb..2830ce06c4d2 100644 --- a/WordPress/WordPressTest/ReaderDetailViewControllerTests.swift +++ b/WordPress/WordPressTest/ReaderDetailViewControllerTests.swift @@ -3,7 +3,7 @@ import Nimble @testable import WordPress -class ReaderDetailViewControllerTests: XCTestCase { +class ReaderDetailViewControllerTests: CoreDataTestCase { /// Given a post URL. returns a ReaderDetailViewController /// @@ -18,7 +18,7 @@ class ReaderDetailViewControllerTests: XCTestCase { /// Starts the coordinator with the ReaderPost and call start in viewDidLoad /// func testControllerWithPostRendersPostContent() { - let post: ReaderPost = ReaderPostBuilder().build() + let post: ReaderPost = ReaderPostBuilder(mainContext).build() let controller = ReaderDetailViewController.controllerWithPost(post) let coordinatorMock = ReaderDetailCoordinatorMock(view: controller) let originalCoordinator = controller.coordinator @@ -47,20 +47,6 @@ class ReaderDetailViewControllerTests: XCTestCase { } /// Builds a ReaderPost -/// -class ReaderPostBuilder: PostBuilder { - private let post: ReaderPost - - override init(_ context: NSManagedObjectContext = PostBuilder.setUpInMemoryManagedObjectContext(), blog: Blog? = nil) { - post = NSEntityDescription.insertNewObject(forEntityName: ReaderPost.entityName(), into: context) as! ReaderPost - } - - func build() -> ReaderPost { - post.blogURL = "https://wordpress.com" - post.permaLink = "https://wordpress.com" - return post - } -} private class ReaderDetailCoordinatorMock: ReaderDetailCoordinator { var didCallStart = false diff --git a/WordPress/WordPressTest/ReaderPostBuilder.swift b/WordPress/WordPressTest/ReaderPostBuilder.swift new file mode 100644 index 000000000000..abf5802c3b5b --- /dev/null +++ b/WordPress/WordPressTest/ReaderPostBuilder.swift @@ -0,0 +1,17 @@ +@testable import WordPress + +/// Builds a `ReaderPost` +/// +class ReaderPostBuilder { + private let post: ReaderPost + + init(_ context: NSManagedObjectContext, blog: Blog? = nil) { + post = NSEntityDescription.insertNewObject(forEntityName: ReaderPost.entityName(), into: context) as! ReaderPost + } + + func build() -> ReaderPost { + post.blogURL = "https://wordpress.com" + post.permaLink = "https://wordpress.com" + return post + } +} diff --git a/WordPress/WordPressTest/ReaderPostCellActionsTests.swift b/WordPress/WordPressTest/ReaderPostCellActionsTests.swift index 1166d88cedef..d277b7a04453 100644 --- a/WordPress/WordPressTest/ReaderPostCellActionsTests.swift +++ b/WordPress/WordPressTest/ReaderPostCellActionsTests.swift @@ -78,7 +78,7 @@ final class ReaderPostCellActionsTests: CoreDataTestCase { } private func makePost() -> ReaderPost { - let builder = ReaderPostBuilder() + let builder = ReaderPostBuilder(mainContext) let post: ReaderPost = builder.build() post.isWPCom = true return post diff --git a/WordPress/WordPressTest/RegisterDomainDetailsServiceProxyMock.swift b/WordPress/WordPressTest/RegisterDomainDetailsServiceProxyMock.swift index e49ded8c740b..4d9760c2f783 100644 --- a/WordPress/WordPressTest/RegisterDomainDetailsServiceProxyMock.swift +++ b/WordPress/WordPressTest/RegisterDomainDetailsServiceProxyMock.swift @@ -130,7 +130,7 @@ class RegisterDomainDetailsServiceProxyMock: RegisterDomainDetailsServiceProxyPr }, failure: failure) } - func createTemporaryDomainShoppingCart(siteID: Int, + func createTemporaryDomainShoppingCart(siteID: Int?, domainSuggestion: DomainSuggestion, privacyProtectionEnabled: Bool, success: @escaping (CartResponseProtocol) -> Void, @@ -143,7 +143,7 @@ class RegisterDomainDetailsServiceProxyMock: RegisterDomainDetailsServiceProxyPr success(response) } - func createPersistentDomainShoppingCart(siteID: Int, + func createPersistentDomainShoppingCart(siteID: Int?, domainSuggestion: DomainSuggestion, privacyProtectionEnabled: Bool, success: @escaping (CartResponseProtocol) -> Void, diff --git a/WordPress/WordPressTest/RegisterDomainDetailsViewModelLoadingStateTests.swift b/WordPress/WordPressTest/RegisterDomainDetailsViewModelLoadingStateTests.swift index f9675af290fb..05c74e7a7b0c 100644 --- a/WordPress/WordPressTest/RegisterDomainDetailsViewModelLoadingStateTests.swift +++ b/WordPress/WordPressTest/RegisterDomainDetailsViewModelLoadingStateTests.swift @@ -7,7 +7,7 @@ class RegisterDomainDetailsViewModelLoadingStateTests: XCTestCase { override func setUp() { super.setUp() - let domainSuggestion = try! FullyQuotedDomainSuggestion(json: ["domain_name": "" as AnyObject]) + let domainSuggestion = try! FullyQuotedDomainSuggestion(json: ["domain_name": "" as AnyObject]).remoteSuggestion() let siteID = 9001 viewModel = RegisterDomainDetailsViewModel(siteID: siteID, domain: domainSuggestion) { _ in return diff --git a/WordPress/WordPressTest/RegisterDomainDetailsViewModelTests.swift b/WordPress/WordPressTest/RegisterDomainDetailsViewModelTests.swift index b03271a78054..e9c38caf245e 100644 --- a/WordPress/WordPressTest/RegisterDomainDetailsViewModelTests.swift +++ b/WordPress/WordPressTest/RegisterDomainDetailsViewModelTests.swift @@ -51,7 +51,7 @@ class RegisterDomainDetailsViewModelTests: XCTestCase { override func setUp() { super.setUp() - let domainSuggestion = try! FullyQuotedDomainSuggestion(json: ["domain_name": "" as AnyObject]) + let domainSuggestion = try! FullyQuotedDomainSuggestion(json: ["domain_name": "" as AnyObject]).remoteSuggestion() let siteID = 9001 viewModel = RegisterDomainDetailsViewModel(siteID: siteID, domain: domainSuggestion) { _ in return } diff --git a/WordPress/WordPressTest/RemoteFeatureFlagStoreMock.swift b/WordPress/WordPressTest/RemoteFeatureFlagStoreMock.swift index 5c2b1933fdd7..2cb63959fd14 100644 --- a/WordPress/WordPressTest/RemoteFeatureFlagStoreMock.swift +++ b/WordPress/WordPressTest/RemoteFeatureFlagStoreMock.swift @@ -11,7 +11,17 @@ class RemoteFeatureFlagStoreMock: RemoteFeatureFlagStore { var removalPhaseSelfHosted = false var removalPhaseStaticScreens = false + var enabledFeatureFlags = Set() + var disabledFeatureFlag = Set() + + // MARK: - Access Remote Feature Flag Value + override func value(for flagKey: String) -> Bool? { + if enabledFeatureFlags.contains(flagKey) { + return true + } else if disabledFeatureFlag.contains(flagKey) { + return false + } switch flagKey { case RemoteFeatureFlag.jetpackFeaturesRemovalPhaseOne.remoteKey: return removalPhaseOne diff --git a/WordPress/WordPressTest/RouteMatcherTests.swift b/WordPress/WordPressTest/RouteMatcherTests.swift index 39228392439d..fd2b93841c9d 100644 --- a/WordPress/WordPressTest/RouteMatcherTests.swift +++ b/WordPress/WordPressTest/RouteMatcherTests.swift @@ -147,4 +147,31 @@ class RouteMatcherTests: XCTestCase { XCTAssertEqual(match.values[MatchedRouteURLComponentKey.source.rawValue], "widget") XCTAssertEqual(match.source, DeepLinkSource.widget) } + + // MARK: - AppBanner + + func testAppBannerRouter() throws { + // GIVEN a banner URL with a redirect in a fragment + let route = "https://apps.wordpress.com/get/?campaign=qr-code-media#%2Fmedia%2F1234567" + + // WHEN + routes = [AppBannerRoute()] + matcher = RouteMatcher(routes: routes) + let matches = matcher.routesMatching(URL(string: route)!) + + // THEN a match if found + let match = try XCTUnwrap(matches.first) + XCTAssert(match.values.count == 2) + XCTAssertNotNil(match.values[MatchedRouteURLComponentKey.url.rawValue]) + XCTAssertEqual(match.values[MatchedRouteURLComponentKey.fragment.rawValue], "%2Fmedia%2F1234567") + + // WHEN invoking a URL + var router = MockRouter(routes: []) + router.completion = { url, source in + // THEN it opens a universal link from a fragement and passes the campaign + XCTAssertEqual(url, URL(string: "https://wordpress.com/media/1234567?campaign=qr-code-media")) + XCTAssertEqual(source, DeepLinkSource.banner(campaign: "qr-code-media")) + } + AppBannerRoute().perform(match.values, router: router) + } } diff --git a/WordPress/WordPressTest/Services/PostServiceWPComTests.swift b/WordPress/WordPressTest/Services/PostServiceWPComTests.swift index 26e7a15dbe06..04bdfe1c5c64 100644 --- a/WordPress/WordPressTest/Services/PostServiceWPComTests.swift +++ b/WordPress/WordPressTest/Services/PostServiceWPComTests.swift @@ -107,29 +107,6 @@ class PostServiceWPComTests: CoreDataTestCase { XCTAssertEqual(postFromDB.status, .draft) } - func testTrashingAPostWillUpdateItsRevisionStatusAfterSyncProperty() { - // Arrange - let post = PostBuilder(mainContext).with(statusAfterSync: .publish).withRemote().build() - let revision = post.createRevision() - try! mainContext.save() - - let remotePost = createRemotePost(.trash) - remoteMock.remotePostToReturnOnTrashPost = remotePost - let expectation = XCTestExpectation() - - // Act - self.service.trashPost(post, success: { - expectation.fulfill() - }, failure: self.impossibleFailureBlock) - wait(for: [expectation], timeout: timeout) - - // Assert - XCTAssertEqual(post.statusAfterSync, .trash) - XCTAssertEqual(post.status, .trash) - XCTAssertEqual(revision.statusAfterSync, .trash) - XCTAssertEqual(revision.status, .trash) - } - func testAutoSavingALocalDraftWillCallTheCreateEndpointInstead() { // Arrange let post = PostBuilder(mainContext).drafted().with(remoteStatus: .local).build() diff --git a/WordPress/WordPressTest/ShareAppContentPresenterTests.swift b/WordPress/WordPressTest/ShareAppContentPresenterTests.swift index 3c7435f33a65..244bc6af03a8 100644 --- a/WordPress/WordPressTest/ShareAppContentPresenterTests.swift +++ b/WordPress/WordPressTest/ShareAppContentPresenterTests.swift @@ -24,7 +24,7 @@ final class ShareAppContentPresenterTests: CoreDataTestCase { super.setUp() TestAnalyticsTracker.setup() - account = AccountBuilder(contextManager).build() + account = AccountBuilder(contextManager.mainContext).build() mockRemote = MockShareAppContentServiceRemote() presenter = ShareAppContentPresenter(remote: mockRemote, account: account) viewController = MockViewController() diff --git a/WordPress/WordPressTest/SharingServiceTests.swift b/WordPress/WordPressTest/SharingServiceTests.swift index 120a560c2794..3abcab3ad6a3 100644 --- a/WordPress/WordPressTest/SharingServiceTests.swift +++ b/WordPress/WordPressTest/SharingServiceTests.swift @@ -10,7 +10,7 @@ class SharingServiceTests: CoreDataTestCase { private let blogID = 10 private lazy var account: WPAccount = { - AccountBuilder(contextManager) + AccountBuilder(contextManager.mainContext) .with(id: Int64(userID)) .with(username: "username") .with(authToken: "authToken") diff --git a/WordPress/WordPressTest/SiteAddressServiceTests.swift b/WordPress/WordPressTest/SiteAddressServiceTests.swift index d40fdf1ffc7c..816ac00c37dd 100644 --- a/WordPress/WordPressTest/SiteAddressServiceTests.swift +++ b/WordPress/WordPressTest/SiteAddressServiceTests.swift @@ -21,7 +21,7 @@ class SiteAddressServiceTests: CoreDataTestCase { let searchTerm = "domaintesting" let waitExpectation = expectation(description: "Domains should be successfully fetched") - service.addresses(for: searchTerm) { (results) in + service.addresses(for: searchTerm, type: .wordPressDotComAndDotBlogSubdomains) { (results) in switch results { case .success(let fetchedResults): self.resultsAreSorted(fetchedResults, forQuery: searchTerm, expectMatch: true) @@ -49,7 +49,7 @@ class SiteAddressServiceTests: CoreDataTestCase { let searchTerm = "notIncludedResult" let waitExpectation = expectation(description: "Domains should be successfully fetched") - service.addresses(for: searchTerm) { (results) in + service.addresses(for: searchTerm, type: .wordPressDotComAndDotBlogSubdomains) { (results) in switch results { case .success(let fetchedResults): self.resultsAreSorted(fetchedResults, forQuery: searchTerm, expectMatch: false) @@ -73,36 +73,6 @@ class SiteAddressServiceTests: CoreDataTestCase { waitForExpectations(timeout: 0.1) } - func testSuggestionsBySegmentSuccess() { - let searchTerm = "domaintesting" - - let waitExpectation = expectation(description: "Domains should be successfully fetched") - service.addresses(for: searchTerm, segmentID: 2) { (results) in - switch results { - case .success(let fetchedResults): - self.resultsAreSorted(fetchedResults, forQuery: searchTerm, expectMatch: true) - case .failure: - fail("This is using a mocked endpoint so there is a test error") - } - - waitExpectation.fulfill() - } - - expect(self.remoteApi.getMethodCalled).to(beTrue()) - - // Respond with mobile editor not yet set on the server - remoteApi.successBlockPassedIn!(mockedResponse as AnyObject, HTTPURLResponse()) - expect(self.remoteApi.URLStringPassedIn!).to(equal("rest/v1.1/domains/suggestions")) - let parameters = remoteApi.parametersPassedIn as! [String: AnyObject] - - expect(parameters["query"] as? String).to(equal(searchTerm)) - expect(parameters["quantity"] as? Int).toNot(beNil()) - expect(parameters["segment_id"] as? Int).toNot(beNil()) - - waitForExpectations(timeout: 0.1) - - } - // Helpers func resultsAreSorted(_ results: SiteAddressServiceResult, forQuery query: String, expectMatch: Bool) { let suggestions = results.domainSuggestions diff --git a/WordPress/WordPressTest/SiteCreation/PlanWizardContentViewModelTests.swift b/WordPress/WordPressTest/SiteCreation/PlanWizardContentViewModelTests.swift index 73db109819b9..6fe7a88a321b 100644 --- a/WordPress/WordPressTest/SiteCreation/PlanWizardContentViewModelTests.swift +++ b/WordPress/WordPressTest/SiteCreation/PlanWizardContentViewModelTests.swift @@ -21,7 +21,7 @@ final class PlanWizardContentViewModelTests: XCTestCase { func testIsPlanSelectedWithPlanSlugParameters() { var components = URLComponents(string: PlanWizardContentViewModel.Constants.plansWebAddress)! - components.queryItems = [.init(name: PlanWizardContentViewModel.Constants.planSlugParameter, value: "free_plan")] + components.queryItems = [.init(name: PlanWizardContentViewModel.Constants.OutputParameter.planSlug, value: "free_plan")] XCTAssertTrue(sut.isPlanSelected(components.url!)) } @@ -29,8 +29,8 @@ final class PlanWizardContentViewModelTests: XCTestCase { func testIsPlanSelectedWithPlanSlugAndPlanIdParameters() { var components = URLComponents(string: PlanWizardContentViewModel.Constants.plansWebAddress)! components.queryItems = [ - .init(name: PlanWizardContentViewModel.Constants.planSlugParameter, value: "paid_plan"), - .init(name: PlanWizardContentViewModel.Constants.planIdParameter, value: "1009") + .init(name: PlanWizardContentViewModel.Constants.OutputParameter.planSlug, value: "paid_plan"), + .init(name: PlanWizardContentViewModel.Constants.OutputParameter.planId, value: "1009") ] XCTAssertTrue(sut.isPlanSelected(components.url!)) @@ -46,7 +46,7 @@ final class PlanWizardContentViewModelTests: XCTestCase { func testSelectedPlanId() { var components = URLComponents(string: PlanWizardContentViewModel.Constants.plansWebAddress)! - components.queryItems = [.init(name: PlanWizardContentViewModel.Constants.planIdParameter, value: "125")] + components.queryItems = [.init(name: PlanWizardContentViewModel.Constants.OutputParameter.planId, value: "125")] XCTAssertEqual(sut.selectedPlanId(from: components.url!), 125) } @@ -55,7 +55,7 @@ final class PlanWizardContentViewModelTests: XCTestCase { var components = URLComponents(string: PlanWizardContentViewModel.Constants.plansWebAddress)! components.queryItems = [ .init(name: "parameter", value: "5"), - .init(name: PlanWizardContentViewModel.Constants.planIdParameter, value: "125"), + .init(name: PlanWizardContentViewModel.Constants.OutputParameter.planId, value: "125"), .init(name: "parameter2", value: "abc") ] @@ -70,7 +70,7 @@ final class PlanWizardContentViewModelTests: XCTestCase { let url = URLComponents(url: sut.url, resolvingAgainstBaseURL: true) - let parameter = url?.queryItems?.first(where: { $0.name == PlanWizardContentViewModel.Constants.paidDomainNameParameter }) + let parameter = url?.queryItems?.first(where: { $0.name == PlanWizardContentViewModel.Constants.InputParameter.paidDomainName }) XCTAssertEqual(parameter?.value, domainName) } @@ -80,7 +80,7 @@ final class PlanWizardContentViewModelTests: XCTestCase { let url = URLComponents(url: sut.url, resolvingAgainstBaseURL: true) - let parameter = url?.queryItems?.first(where: { $0.name == PlanWizardContentViewModel.Constants.paidDomainNameParameter }) + let parameter = url?.queryItems?.first(where: { $0.name == PlanWizardContentViewModel.Constants.InputParameter.paidDomainName }) XCTAssertEqual(parameter, nil) } } diff --git a/WordPress/WordPressTest/StockPhotosDataSourceTests.swift b/WordPress/WordPressTest/StockPhotosDataSourceTests.swift deleted file mode 100644 index 615ec52bced3..000000000000 --- a/WordPress/WordPressTest/StockPhotosDataSourceTests.swift +++ /dev/null @@ -1,119 +0,0 @@ -import XCTest -@testable import WordPress - -final class MockMediaGroup: NSObject, WPMediaGroup { - struct Constants { - static let name = "🦄" - } - - func name() -> String { - return Constants.name - } - - func image(with size: CGSize, completionHandler: @escaping WPMediaImageBlock) -> WPMediaRequestID { - return 0 - } - - func cancelImageRequest(_ requestID: WPMediaRequestID) { - // - } - - func baseGroup() -> Any { - return "" - } - - func identifier() -> String { - return "group id" - } - - func numberOfAssets(of mediaType: WPMediaType, completionHandler: WPMediaCountBlock? = nil) -> Int { - return 10 - } -} - - -final class StockPhotosDataSourceTests: XCTestCase { - private var dataSource: StockPhotosDataSource? - private var mockService: StockPhotosService? - - private struct Constants { - static let searchTerm = "unicorns" - static let groupCount = 1 - static let groupName = String.freePhotosLibrary - - static func itemCount() -> Int { - return searchTerm.count - } - } - - override func setUp() { - super.setUp() - mockService = MockStockPhotosService(mediaCount: Constants.itemCount()) - dataSource = StockPhotosDataSource(service: mockService!) - } - - override func tearDown() { - dataSource = nil - mockService = nil - super.tearDown() - } - -// func testDataSourceReceivesRequestedCount() { -// dataSource?.search(for: Constants.searchTerm) -// -// //Searches are debounced for half a second -// wait(for: 1) -// -// XCTAssertEqual(dataSource?.numberOfAssets(), Constants.itemCount()) -// } - - func testDataSourceManagesExpectedNumberOfGroups() { - let groupCount = dataSource?.numberOfGroups() - - XCTAssertEqual(groupCount, Constants.groupCount) - } - - func testSearchCancelledClearsData() { - dataSource?.searchCancelled() - - XCTAssertEqual(dataSource?.numberOfAssets(), 0) - } - - func testClearRemovesData() { - dataSource?.clearSearch(notifyObservers: false) - - XCTAssertEqual(dataSource?.numberOfAssets(), 0) - } - - func testGroupIsNamePhotosLibrary() { - let groupAtIndexZero = dataSource?.group(at: 0) - let groupName = groupAtIndexZero?.name() - - XCTAssertEqual(groupName, Constants.groupName) - } - - func testDataSourceOnlyManagesImages() { - let mediaType = dataSource?.mediaTypeFilter() - - XCTAssertEqual(mediaType, WPMediaType.image) - } - - func testDataSourceIsSortedAscending() { - XCTAssertTrue(dataSource!.ascendingOrdering()) - } - - func testSetSelectedGroupIsIgnored() { - let groupToBeIgnored = MockMediaGroup() - dataSource?.setSelectedGroup(groupToBeIgnored) - - let selectedGroup = dataSource?.selectedGroup() - - XCTAssertEqual(selectedGroup?.name(), Constants.groupName) - } - - func testOrderingCanNotBeChanged() { - dataSource?.setAscendingOrdering(false) - - XCTAssertTrue(dataSource!.ascendingOrdering()) - } -} diff --git a/WordPress/WordPressTest/StockPhotosMediaGroupTests.swift b/WordPress/WordPressTest/StockPhotosMediaGroupTests.swift deleted file mode 100644 index 456c498e9b5e..000000000000 --- a/WordPress/WordPressTest/StockPhotosMediaGroupTests.swift +++ /dev/null @@ -1,47 +0,0 @@ -import XCTest -@testable import WordPress - -final class StockPhotosMediaGroupTests: XCTestCase { - private var mediaGroup: StockPhotosMediaGroup? - - private struct Constants { - static let name = String.freePhotosLibrary - static let mediaRequestID: WPMediaRequestID = 0 - static let baseGroup = "" - static let identifier = "group id" - static let numberOfAssets = 10 - } - - override func setUp() { - super.setUp() - mediaGroup = StockPhotosMediaGroup() - } - - override func tearDown() { - mediaGroup = nil - super.tearDown() - } - - func testGroupNameMatchesExpectation() { - XCTAssertEqual(mediaGroup!.name(), Constants.name) - } - - func testGroupMediaRequestIDMatchesExpectation() { - XCTAssertEqual(mediaGroup!.image(with: .zero, completionHandler: { (image, error) in - - }), Constants.mediaRequestID) - } - - func testBaseGroupIsEmpty() { - XCTAssertEqual(mediaGroup!.baseGroup() as! String, Constants.baseGroup) - } - - func testIdentifierMatchesExpectation() { - XCTAssertEqual(mediaGroup!.identifier(), Constants.identifier) - } - - func testNumberOfAssetsMatchExpectation() { - let numberOfAssets = mediaGroup?.numberOfAssets(of: .image) - XCTAssertEqual(numberOfAssets, Constants.numberOfAssets) - } -} diff --git a/WordPress/WordPressTest/ThumbnailCollectionTests.swift b/WordPress/WordPressTest/StockPhotosThumbnailCollectionTests.swift similarity index 79% rename from WordPress/WordPressTest/ThumbnailCollectionTests.swift rename to WordPress/WordPressTest/StockPhotosThumbnailCollectionTests.swift index 2f3b70d1ab20..e079ff23adcc 100644 --- a/WordPress/WordPressTest/ThumbnailCollectionTests.swift +++ b/WordPress/WordPressTest/StockPhotosThumbnailCollectionTests.swift @@ -1,8 +1,8 @@ import XCTest @testable import WordPress -final class ThumbnailCollectionTests: XCTestCase { - private var subject: ThumbnailCollection? +final class StockPhotosThumbnailCollectionTests: XCTestCase { + private var subject: StockPhotosMedia.ThumbnailCollection? private struct MockValues { static let largeURL = URL(string: "https://images.pexels.com/photos/946630/pexels-photo-946630.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940")! @@ -14,11 +14,11 @@ final class ThumbnailCollectionTests: XCTestCase { override func setUp() { super.setUp() - let json = Bundle(for: ThumbnailCollectionTests.self).url(forResource: "thumbnail-collection", withExtension: "json")! + let json = Bundle(for: StockPhotosThumbnailCollectionTests.self).url(forResource: "thumbnail-collection", withExtension: "json")! let data = try! Data(contentsOf: json) let jsonDecoder = JSONDecoder() - subject = try! jsonDecoder.decode(ThumbnailCollection.self, from: data) + subject = try! jsonDecoder.decode(StockPhotosMedia.ThumbnailCollection.self, from: data) } override func tearDown() { diff --git a/WordPress/WordPressTest/StringRankedSearchTests.swift b/WordPress/WordPressTest/StringRankedSearchTests.swift new file mode 100644 index 000000000000..c715ce186996 --- /dev/null +++ b/WordPress/WordPressTest/StringRankedSearchTests.swift @@ -0,0 +1,85 @@ +import XCTest + +@testable import WordPress + +final class StringRankedSearchTests: XCTestCase { + func testScoreInRange() { + // High confidence + XCTAssertInRange(0.8...1.0, score("Appleseed", "Appleseed")) + XCTAssertInRange(0.8...1.0, score("John Appleseed", "Appleseed")) + XCTAssertInRange(0.8...1.0, score("John Appleseed", "John")) + XCTAssertInRange(0.8...1.0, score("John Appleseed", "App")) + XCTAssertInRange(0.8...1.0, score("John O'Appleseed", "App")) + XCTAssertInRange(0.8...1.0, score("john-appleseed", "j-a")) + XCTAssertInRange(0.8...1.0, score("#john-appleseed", "john")) + XCTAssertInRange(0.8...1.0, score("John Appleseed", "Apseed")) + + // Medium confidence + XCTAssertInRange(0.5...0.8, score("John Appleseed", "A")) + XCTAssertInRange(0.5...0.8, score("John Appleseed", "Ap")) + XCTAssertInRange(0.5...0.8, score("John Appleseed", "ohn")) + XCTAssertInRange(0.5...0.8, score("#john-appleseed", "j-a")) + XCTAssertInRange(0.5...0.8, score("John Appleseed", "applex")) + + // Low confidence + XCTAssertInRange(0.2...0.5, score("John Appleseed", "Ae")) + XCTAssertInRange(0.2...0.5, score("John Appleseed", "Jn")) + + // Very low confidence + XCTAssertInRange(0.0...0.2, score("John Appleseed", "o")) + XCTAssertInRange(0.0...0.2, score("John Appleseed", "X")) + XCTAssertInRange(0.0...0.2, score("John Appleseed", "x")) + XCTAssertInRange(0.0...0.2, score("John Appleseed", "applexx")) + } + + func testBonuses() { + // Bonus for the number of the matching words in the input. + XCTAssertLessThan(score("John Appleseed", "App"), score("Appleseed", "App")) + + // Bonus for distance between matches + XCTAssertLessThan(score("John Xxxx Appleseed", "John Appleseed"), score("John Appleseed Xxxx", "John Appleseed")) + + // Bonus for distance between matches + XCTAssertLessThan(score("John Xxxx Appleseed", "John Appleseed"), score("John Appleseed Xxxx", "John Appleseed")) + + // Bonus for distance between matches + XCTAssertLessThan(score("John Xxxx Appleseed", "John Appleseed"), score("Xxxx John Appleseed", "John Appleseed")) + + // Bonus for more characters in a row + XCTAssertLessThan(score("Apxplesee", "App"), score("Appleseed", "App")) + + // Bonus for more characters in a row is higher than the penalty for a number of matches + XCTAssertLessThan(score("Apxplesee", "App"), score("John Appleseed", "App")) + + // Bonus for more characters in a row is higher than the penalty for mismatches case. + XCTAssertLessThan(score("Apxplesee", "App"), score("appleseed", "App")) + + // The diacritics are considered a match + XCTAssertLessThan(score("Kxhu", "Kahu"), score("Kāhu", "Kahu")) + + // Bonus for exact match diacritics are present + XCTAssertLessThan(score("Kāhu", "Kahu"), score("Kahu", "Kahu")) + + // Bonus for exact match diacritics are present + XCTAssertLessThan(score("Kāhu", "Kahu"), score("Kāhu", "Kāhu")) + + // Bonus for number length match + XCTAssertLessThan(score("john-appleseed-xxxx", "project"), score("john-appleseed", "project")) + } + + func xtestPerformance() throws { + measure { + for _ in 0..<10000 { + _ = score("John Appleseed", "John") + } + } + } +} + +private func score(_ lhs: String, _ rhs: String) -> Double { + StringRankedSearch(searchTerm: rhs).score(for: lhs) +} + +private func XCTAssertInRange(_ range: some RangeExpression, _ value: T, file: StaticString = #filePath, line: UInt = #line) { + XCTAssert(range.contains(value), "(\"\(value)\") is not in (\"\(range)\")", file: file, line: line) +} diff --git a/WordPress/WordPressTest/Test Data/Dashboard/dashboard-200-with-multiple-dynamic-cards.json b/WordPress/WordPressTest/Test Data/Dashboard/dashboard-200-with-multiple-dynamic-cards.json new file mode 100644 index 000000000000..51800bfc5d63 --- /dev/null +++ b/WordPress/WordPressTest/Test Data/Dashboard/dashboard-200-with-multiple-dynamic-cards.json @@ -0,0 +1,178 @@ +{ + "dynamic": [ + { + "id": "id_12345", + "title": "Title 12345", + "remote_feature_flag": "feature_flag_12345", + "featured_image": "https://example.com/image12345", + "url": "https://example.com/url12345", + "action": "Action 12345", + "order": "top", + "rows": [ + { + "icon": "https://example.com/icon12345", + "title": "Row Title 1", + "description": "Row Description 1" + }, + { + "icon": "https://example.com/icon67890", + "title": "Row Title 2", + "description": "Row Description 2" + } + ] + }, + { + "id": "id_67890", + "title": "Title 67890", + "remote_feature_flag": "feature_flag_67890", + "featured_image": "https://example.com/image67890", + "url": "https://example.com/url67890", + "action": "Action 67890", + "order": "top", + "rows": [ + { + "icon": "https://example.com/icon54321", + "title": "Row Title 3", + "description": "Row Description 3" + }, + { + "icon": "https://example.com/icon98765", + "title": "Row Title 4", + "description": "Row Description 4" + } + ] + }, + { + "id": "id_13579", + "title": "Title 13579", + "remote_feature_flag": "feature_flag_13579", + "featured_image": "https://example.com/image13579", + "url": "https://example.com/url13579", + "action": "Action 13579", + "order": "bottom", + "rows": [ + { + "icon": "https://example.com/icon24680", + "title": "Row Title 5", + "description": "Row Description 5" + }, + { + "icon": "https://example.com/icon112233", + "title": "Row Title 6", + "description": "Row Description 6" + } + ] + } + ], + "posts": { + "has_published": true, + "draft": [{ + "id": 3246, + "title": "Foo", + "content": "", + "featured_image": null, + "date": "2022-01-13 00:30:56" + }, { + "id": 3120, + "title": "Bar", + "content": "", + "featured_image": null, + "date": "2021-03-05 21:04:44" + }, { + "id": 3109, + "title": "Foobar", + "content": "", + "featured_image": null, + "date": "0000-00-00 00:00:00" + }], + "scheduled": [{ + "id": 3109, + "title": "Foobar", + "content": "", + "featured_image": null, + "date": "0000-00-00 00:00:00" + }] + }, + "todays_stats": { + "views": 0, + "visitors": 0, + "likes": 0, + "comments": 0 + }, + "pages": [ + { + "id": 0, + "title": "string", + "date": "0000-00-00 00:00:00", + "modified": "0000-00-00 00:00:00", + "status": "publish" + }, + { + "id": 1, + "title": "string", + "date": "0000-00-00 00:00:00", + "modified": "0000-00-00 00:00:00", + "status": "publish" + } + ], + "activity": { + "current": { + "orderedItems": [ + { + "summary": "Setting changed", + "content": { + "text": "Encouraged search engines to index the site" + }, + "name": "setting__changed_blog_public", + "actor": { + "type": "Person", + "name": "John Doe", + "external_user_id": 0, + "wpcom_user_id": 1, + "icon": null, + "role": "administrator" + }, + "type": "Announce", + "published": "2023-03-13T16:03:15.230+00:00", + "generator": { + "jetpack_version": 0, + "blog_id": 1 + }, + "is_rewindable": false, + "rewind_id": "", + "gridicon": "cog", + "status": null, + "activity_id": "", + "is_discarded": false + }, + { + "summary": "Setting changed Two", + "content": { + "text": "Encouraged search engines to index the site" + }, + "name": "setting__changed_blog_public", + "actor": { + "type": "Person", + "name": "John Doe", + "external_user_id": 0, + "wpcom_user_id": 1, + "icon": null, + "role": "administrator" + }, + "type": "Announce", + "published": "2023-03-13T16:03:15.230+00:00", + "generator": { + "jetpack_version": 0, + "blog_id": 1 + }, + "is_rewindable": false, + "rewind_id": "", + "gridicon": "cog", + "status": null, + "activity_id": "", + "is_discarded": false + } + ] + } + } +} diff --git a/WordPress/WordPressTest/Test Data/Dashboard/dashboard-200-with-only-one-dynamic-card.json b/WordPress/WordPressTest/Test Data/Dashboard/dashboard-200-with-only-one-dynamic-card.json new file mode 100644 index 000000000000..88e239b41588 --- /dev/null +++ b/WordPress/WordPressTest/Test Data/Dashboard/dashboard-200-with-only-one-dynamic-card.json @@ -0,0 +1,23 @@ +{ + "dynamic": [ + { + "id": "id_12345", + "title": "Title 12345", + "remote_feature_flag": "feature_flag_12345", + "featured_image": "https://example.com/image12345", + "url": "https://example.com/url12345", + "action": "Action 12345", + "order": "top", + "rows": [ + { + "icon": "https://example.com/icon12345", + "title": "Row Title 1" + }, + { + "title": "Row Title 2", + "description": "Row Description 2" + } + ] + } + ] +} diff --git a/WordPress/WordPressTest/Test Data/blogging-prompts-bloganuary.json b/WordPress/WordPressTest/Test Data/blogging-prompts-bloganuary.json new file mode 100644 index 000000000000..5c1dd4637e50 --- /dev/null +++ b/WordPress/WordPressTest/Test Data/blogging-prompts-bloganuary.json @@ -0,0 +1,32 @@ +[ + { + "id": 239, + "date": "2023-12-31", + "label": "Daily writing prompt", + "text": "Was there a toy or thing you always wanted as a child, during the holidays or on your birthday, but never received? Tell us about it.", + "attribution": "dayone", + "answered": false, + "answered_users_count": 0, + "answered_users_sample": [], + "answered_link": "https:\/\/wordpress.com\/tag\/dailyprompt-239", + "answered_link_text": "View all responses", + "bloganuary_id": "" + }, + { + "id": 248, + "date": "2024-01-01", + "label": "Daily writing prompt", + "text": "Tell us about a time when you felt out of place.", + "attribution": "", + "answered": true, + "answered_users_count": 1, + "answered_users_sample": [ + { + "avatar": "https://0.gravatar.com/avatar/example?s=96&d=identicon&r=G" + } + ], + "answered_link": "https:\/\/wordpress.com\/tag\/dailyprompt-248", + "answered_link_text": "View all responses", + "bloganuary_id": "bloganuary-2024-01" + } +] diff --git a/WordPress/WordPressTest/Test Data/blogging-prompts-fetch-success.json b/WordPress/WordPressTest/Test Data/blogging-prompts-fetch-success.json index f7a8d7ee646d..7371170647c9 100644 --- a/WordPress/WordPressTest/Test Data/blogging-prompts-fetch-success.json +++ b/WordPress/WordPressTest/Test Data/blogging-prompts-fetch-success.json @@ -1,30 +1,30 @@ -{ - "prompts": [ +[ { "id": 239, + "date": "2022-05-03", + "label": "Daily writing prompt", "text": "Was there a toy or thing you always wanted as a child, during the holidays or on your birthday, but never received? Tell us about it.", - "title": "Prompt number 1", - "content": "\n

Was there a toy or thing you always wanted as a child, during the holidays or on your birthday, but never received? Tell us about it.

(courtesy of plinky.com)
\n", "attribution": "dayone", - "date": "2022-05-03", "answered": false, "answered_users_count": 0, - "answered_users_sample": [] + "answered_users_sample": [], + "answered_link": "https:\/\/wordpress.com\/tag\/dailyprompt-239", + "answered_link_text": "View all responses" }, { "id": 248, + "date": "2021-09-12", + "label": "Daily writing prompt", "text": "Tell us about a time when you felt out of place.", - "title": "Prompt number 10", - "content": "\n

Tell us about a time when you felt out of place.

(courtesy of plinky.com)
\n", "attribution": "", - "date": "2021-09-12", "answered": true, "answered_users_count": 1, "answered_users_sample": [ { "avatar": "https://0.gravatar.com/avatar/example?s=96&d=identicon&r=G" } - ] + ], + "answered_link": "https:\/\/wordpress.com\/tag\/dailyprompt-248", + "answered_link_text": "View all responses" } - ] -} +] diff --git a/WordPress/WordPressTest/TestUtilities/PageBuilder.swift b/WordPress/WordPressTest/TestUtilities/PageBuilder.swift index d56cd8bad1cb..43c58e9f4df6 100644 --- a/WordPress/WordPressTest/TestUtilities/PageBuilder.swift +++ b/WordPress/WordPressTest/TestUtilities/PageBuilder.swift @@ -6,11 +6,11 @@ import Foundation class PageBuilder { private let page: Page - init(_ context: NSManagedObjectContext) { + init(_ context: NSManagedObjectContext, canBlaze: Bool = false) { page = NSEntityDescription.insertNewObject(forEntityName: Page.entityName(), into: context) as! Page // Non-null Core Data properties - page.blog = BlogBuilder(context).build() + page.blog = canBlaze ? BlogBuilder(context).canBlaze().build() : BlogBuilder(context).build() } func with(status: BasePost.Status) -> Self { diff --git a/WordPress/WordPressTest/ViewRelated/Post/Controllers/PostListViewControllerTests.swift b/WordPress/WordPressTest/ViewRelated/Post/Controllers/PostListViewControllerTests.swift deleted file mode 100644 index 9850ecab5d69..000000000000 --- a/WordPress/WordPressTest/ViewRelated/Post/Controllers/PostListViewControllerTests.swift +++ /dev/null @@ -1,76 +0,0 @@ -import UIKit -import XCTest -import Nimble - -@testable import WordPress - -class PostListViewControllerTests: CoreDataTestCase { - - func testShowsGhostableTableView() { - let blog = BlogBuilder(mainContext).build() - let postListViewController = PostListViewController.controllerWithBlog(blog) - let _ = postListViewController.view - - postListViewController.startGhost() - - expect(postListViewController.ghostableTableView.isHidden).to(beFalse()) - } - - func testHidesGhostableTableView() { - let blog = BlogBuilder(mainContext).build() - let postListViewController = PostListViewController.controllerWithBlog(blog) - let _ = postListViewController.view - - postListViewController.stopGhost() - - expect(postListViewController.ghostableTableView.isHidden).to(beTrue()) - } - - func testShowTenMockedItemsInGhostableTableView() { - let blog = BlogBuilder(mainContext).build() - let postListViewController = PostListViewController.controllerWithBlog(blog) - let _ = postListViewController.view - - postListViewController.startGhost() - - expect(postListViewController.ghostableTableView.numberOfRows(inSection: 0)).to(equal(50)) - } - - func testItCanHandleNewPostUpdatesEvenIfTheGhostViewIsStillVisible() throws { - // This test simulates and proves that the app will no longer crash on these conditions: - // - // 1. The app is built using Xcode 11 and running on iOS 13.1 - // 2. The user has no cached data on the device - // 3. The user navigates to the Post List → Drafts - // 4. The user taps on the plus (+) button which adds a post in the Drafts list - // - // Please see https://git.io/JeK3y for more information about this crash. - // - // This test fails when executed on 00c88b9b - - // Given - let blog = BlogBuilder(mainContext).build() - try mainContext.save() - - let postListViewController = PostListViewController.controllerWithBlog(blog) - let _ = postListViewController.view - - let draftsIndex = postListViewController.filterTabBar.items.firstIndex(where: { $0.title == i18n("Drafts") }) ?? 1 - postListViewController.updateFilter(index: draftsIndex) - - postListViewController.startGhost() - - // When: Simulate a post being created - // Then: This should not cause a crash - // - // Note that `XCTAssertNoThrow` catches `NSException`s as well as Swift's `throw`. - // - // This test originally used Nimble's `raiseException` but that matcher is no longer available in the SPM build. - // See https://github.com/Quick/Nimble/blob/e313d9a67ec2e4171d416c61282e49fc3aadc7a4/Sources/Nimble/Matchers/RaisesException.swift#L1 - XCTAssertNoThrow(try { - _ = PostBuilder(self.mainContext, blog: blog).with(status: .draft).build() - try self.mainContext.save() - }()) - } - -} diff --git a/WordPress/WordPressTest/ViewRelated/Post/Views/PostCardStatusViewModelTests.swift b/WordPress/WordPressTest/ViewRelated/Post/Views/PostCardStatusViewModelTests.swift index 7d06e88c2c04..109d4c3b09d2 100644 --- a/WordPress/WordPressTest/ViewRelated/Post/Views/PostCardStatusViewModelTests.swift +++ b/WordPress/WordPressTest/ViewRelated/Post/Views/PostCardStatusViewModelTests.swift @@ -1,74 +1,129 @@ import Nimble import XCTest - @testable import WordPress -private typealias ButtonGroups = PostCardStatusViewModel.ButtonGroups - class PostCardStatusViewModelTests: CoreDataTestCase { - func testExpectedButtonGroupsForVariousPostAttributeCombinations() { - // Arrange - let expectations: [(String, Post, ButtonGroups)] = [ - ( - "Draft with remote", - PostBuilder(mainContext).drafted().withRemote().build(), - ButtonGroups(primary: [.edit, .view, .more], secondary: [.publish, .duplicate, .copyLink, .trash]) - ), - ( - "Draft that was not uploaded to the server", - PostBuilder(mainContext).drafted().with(remoteStatus: .failed).build(), - ButtonGroups(primary: [.edit, .publish, .more], secondary: [.duplicate, .copyLink, .trash]) - ), - ( - "Draft with remote and confirmed local changes", - PostBuilder(mainContext).drafted().withRemote().with(remoteStatus: .failed).confirmedAutoUpload().build(), - ButtonGroups(primary: [.edit, .cancelAutoUpload, .more], secondary: [.publish, .duplicate, .copyLink, .trash]) - ), - ( - "Draft with remote and canceled local changes", - PostBuilder(mainContext).drafted().withRemote().with(remoteStatus: .failed).confirmedAutoUpload().cancelledAutoUpload().build(), - ButtonGroups(primary: [.edit, .publish, .more], secondary: [.duplicate, .copyLink, .trash]) - ), - ( - "Local published draft with confirmed auto-upload", - PostBuilder(mainContext).published().with(remoteStatus: .failed).confirmedAutoUpload().build(), - ButtonGroups(primary: [.edit, .cancelAutoUpload, .more], secondary: [.duplicate, .moveToDraft, .copyLink, .trash]) - ), - ( - "Local published draft with canceled auto-upload", - PostBuilder(mainContext).published().with(remoteStatus: .failed).build(), - ButtonGroups(primary: [.edit, .publish, .more], secondary: [.duplicate, .moveToDraft, .copyLink, .trash]) - ), - ( - "Published post", - PostBuilder(mainContext).published().withRemote().build(), - ButtonGroups(primary: [.edit, .view, .more], secondary: [.stats, .share, .duplicate, .moveToDraft, .copyLink, .trash]) - ), - ( - "Published post with local confirmed changes", - PostBuilder(mainContext).published().withRemote().with(remoteStatus: .failed).confirmedAutoUpload().build(), - ButtonGroups(primary: [.edit, .cancelAutoUpload, .more], secondary: [.stats, .share, .duplicate, .moveToDraft, .copyLink, .trash]) - ), - ( - "Post with the max number of auto uploades retry reached", - PostBuilder(mainContext).with(remoteStatus: .failed) - .with(autoUploadAttemptsCount: 3).confirmedAutoUpload().build(), - ButtonGroups(primary: [.edit, .retry, .more], secondary: [.publish, .duplicate, .moveToDraft, .copyLink, .trash]) - ), + + func testPublishedPostButtons() { + // Given + let post = PostBuilder(mainContext, canBlaze: true) + .withRemote() + .published() + .build() + let viewModel = PostCardStatusViewModel(post: post, isJetpackFeaturesEnabled: true, isBlazeFlagEnabled: true) + + // When & Then + let buttons = viewModel.buttonSections + .filter { !$0.buttons.isEmpty } + .map { $0.buttons } + let expectedButtons: [[AbstractPostButton]] = [ + [.view], + [.moveToDraft, .duplicate, .share], + [.blaze], + [.stats, .comments, .settings], + [.trash] ] + expect(buttons).to(equal(expectedButtons)) + } + + func testPublishedPostButtonsWithBlazeDisabled() { + // Given + let post = PostBuilder(mainContext, canBlaze: false) + .withRemote() + .published() + .build() + let viewModel = PostCardStatusViewModel(post: post, isJetpackFeaturesEnabled: true, isBlazeFlagEnabled: true) - // Act and Assert - expectations.forEach { scenario, post, expectedButtonGroups in - let viewModel = PostCardStatusViewModel(post: post, isInternetReachable: false) - - guard viewModel.buttonGroups == expectedButtonGroups else { - let reason = "The scenario \"\(scenario)\" failed. " - + " Expected buttonGroups to be: \(expectedButtonGroups.prettifiedDescription)." - + " Actual: \(viewModel.buttonGroups.prettifiedDescription)" - XCTFail(reason) - return - } - } + // When & Then + let buttons = viewModel.buttonSections + .filter { !$0.buttons.isEmpty } + .map { $0.buttons } + let expectedButtons: [[AbstractPostButton]] = [ + [.view], + [.moveToDraft, .duplicate, .share], + [.stats, .comments, .settings], + [.trash] + ] + expect(buttons).to(equal(expectedButtons)) + } + + func testPublishedPostButtonsWithJetpackFeaturesDisabled() { + // Given + let post = PostBuilder(mainContext) + .withRemote() + .published() + .build() + let viewModel = PostCardStatusViewModel(post: post, isJetpackFeaturesEnabled: false, isBlazeFlagEnabled: false) + + // When & Then + let buttons = viewModel.buttonSections + .filter { !$0.buttons.isEmpty } + .map { $0.buttons } + let expectedButtons: [[AbstractPostButton]] = [ + [.view], + [.moveToDraft, .duplicate, .share], + [.settings], + [.trash] + ] + expect(buttons).to(equal(expectedButtons)) + } + + func testDraftPostButtons() { + // Given + let post = PostBuilder(mainContext) + .drafted() + .build() + let viewModel = PostCardStatusViewModel(post: post, isJetpackFeaturesEnabled: true, isBlazeFlagEnabled: true) + + // When & Then + let buttons = viewModel.buttonSections + .filter { !$0.buttons.isEmpty } + .map { $0.buttons } + let expectedButtons: [[AbstractPostButton]] = [ + [.view], + [.duplicate, .publish], + [.settings], + [.trash] + ] + expect(buttons).to(equal(expectedButtons)) + } + + func testScheduledPostButtons() { + // Given + let post = PostBuilder(mainContext) + .scheduled() + .build() + let viewModel = PostCardStatusViewModel(post: post, isJetpackFeaturesEnabled: true, isBlazeFlagEnabled: true) + + // When & Then + let buttons = viewModel.buttonSections + .filter { !$0.buttons.isEmpty } + .map { $0.buttons } + let expectedButtons: [[AbstractPostButton]] = [ + [.view], + [.moveToDraft], + [.settings], + [.trash] + ] + expect(buttons).to(equal(expectedButtons)) + } + + func testTrashedPostButtons() { + // Given + let post = PostBuilder(mainContext) + .trashed() + .build() + let viewModel = PostCardStatusViewModel(post: post, isJetpackFeaturesEnabled: true, isBlazeFlagEnabled: true) + + // When & Then + let buttons = viewModel.buttonSections + .filter { !$0.buttons.isEmpty } + .map { $0.buttons } + let expectedButtons: [[AbstractPostButton]] = [ + [.moveToDraft], + [.trash] + ] + expect(buttons).to(equal(expectedButtons)) } /// If the post fails to upload and there is internet connectivity, show "Upload failed" message @@ -93,15 +148,3 @@ class PostCardStatusViewModelTests: CoreDataTestCase { expect(viewModel.statusColor).to(equal(.warning)) } } - -private extension ButtonGroups { - var prettifiedDescription: String { - return "{ primary: \(primary.prettifiedDescription), secondary: \(secondary.prettifiedDescription) }" - } -} - -private extension Array where Element == PostCardStatusViewModel.Button { - var prettifiedDescription: String { - return "[" + map { String(describing: $0) }.joined(separator: ", ") + "]" - } -} diff --git a/WordPress/WordPressTest/ViewRelated/Tools/Time Zone/TimeZoneFormatterTests.swift b/WordPress/WordPressTest/ViewRelated/Tools/Time Zone/TimeZoneFormatterTests.swift index f1207bbca331..dcfeb9e21072 100644 --- a/WordPress/WordPressTest/ViewRelated/Tools/Time Zone/TimeZoneFormatterTests.swift +++ b/WordPress/WordPressTest/ViewRelated/Tools/Time Zone/TimeZoneFormatterTests.swift @@ -38,13 +38,30 @@ class TimeZoneFormatterTests: XCTestCase { // Then TimeAtZone = "6:00 PM" var timeAtZone = formatter.getTimeAtZone(timeZone) - XCTAssertEqual("6:00 PM", timeAtZone) + // As of iOS 17.0, `DateFormatter` uses a narrow non-breaking space (U+202F) in output such as "7:00 PM". + // + // See: + // - https://unicode-explorer.com/c/202F + // - https://href.li/?https://developer.apple.com/forums/thread/731850 + // + // An argument could be made to modify these tests, or the whole component, so that we don't need + // to assert on what `DateFormatter` does for us. In the meantime, let's use the proper Unicode + // character in the expectation. + if #available(iOS 17.0, *) { + XCTAssertEqual("6:00\u{202F}PM", timeAtZone) + } else { + XCTAssertEqual("6:00 PM", timeAtZone) + } // When end of May date formatter = TimeZoneFormatter(currentDate: testEndOfMayDate) // Then TimeAtZone = "7:00 PM" timeAtZone = formatter.getTimeAtZone(timeZone) - XCTAssertEqual("7:00 PM", timeAtZone) + if #available(iOS 17.0, *) { + XCTAssertEqual("7:00\u{202F}PM", timeAtZone) + } else { + XCTAssertEqual("7:00 PM", timeAtZone) + } } } diff --git a/WordPress/WordPressTest/WKCookieJarTests.swift b/WordPress/WordPressTest/WKCookieJarTests.swift index f90ddb5c3300..0822af226ecc 100644 --- a/WordPress/WordPressTest/WKCookieJarTests.swift +++ b/WordPress/WordPressTest/WKCookieJarTests.swift @@ -3,7 +3,7 @@ import WebKit @testable import WordPress class WKCookieJarTests: XCTestCase { - var wkCookieStore = WKWebsiteDataStore.nonPersistent().httpCookieStore + var wkCookieStore: WKHTTPCookieStore! var cookieJar: CookieJar { return wkCookieStore } @@ -20,6 +20,11 @@ class WKCookieJarTests: XCTestCase { } func testGetCookies() { + XCTExpectFailure( + "WKHTTPCookieStore tests fail on Xcode 15+. The calling setCookie on the store does not seem to set the cookie...", + options: .nonStrict() + ) + let expectation = self.expectation(description: "getCookies completion called") cookieJar.getCookies(url: wordPressComLoginURL) { (cookies) in XCTAssertEqual(cookies.count, 1, "Should be one cookie for wordpress.com") @@ -29,6 +34,11 @@ class WKCookieJarTests: XCTestCase { } func testHasCookieMatching() { + XCTExpectFailure( + "WKHTTPCookieStore tests fail on Xcode 15+. The calling setCookie on the store does not seem to set the cookie...", + options: .nonStrict() + ) + let expectation = self.expectation(description: "hasCookie completion called") cookieJar.hasWordPressComAuthCookie(username: "testuser", atomicSite: false) { (matches) in XCTAssertTrue(matches, "Cookies should exist for wordpress.com + testuser") @@ -37,7 +47,13 @@ class WKCookieJarTests: XCTestCase { waitForExpectations(timeout: 5, handler: nil) } + func testHasCookieNotMatching() { + XCTExpectFailure( + "WKHTTPCookieStore tests fail on Xcode 15+. The calling setCookie on the store does not seem to set the cookie...", + options: .nonStrict() + ) + let expectation = self.expectation(description: "hasCookie completion called") cookieJar.hasWordPressComAuthCookie(username: "anotheruser", atomicSite: false) { (matches) in XCTAssertFalse(matches, "Cookies should not exist for wordpress.com + anotheruser") @@ -47,9 +63,14 @@ class WKCookieJarTests: XCTestCase { } func testRemoveCookies() { + XCTExpectFailure( + "WKHTTPCookieStore tests fail on Xcode 15+. The calling setCookie on the store does not seem to set the cookie...", + options: .nonStrict() + ) + let expectation = self.expectation(description: "removeCookies completion called") cookieJar.removeWordPressComCookies { [wkCookieStore] in - wkCookieStore.getAllCookies { cookies in + wkCookieStore!.getAllCookies { cookies in XCTAssertEqual(cookies.count, 1) expectation.fulfill() } diff --git a/WordPress/WordPressTest/WPAccount+LookupTests.swift b/WordPress/WordPressTest/WPAccount+LookupTests.swift index 10532279cbff..d7952493c124 100644 --- a/WordPress/WordPressTest/WPAccount+LookupTests.swift +++ b/WordPress/WordPressTest/WPAccount+LookupTests.swift @@ -5,18 +5,18 @@ class WPAccountLookupTests: CoreDataTestCase { func testIsDefaultWordPressComAccountIsFalseWhenNoAccountIsSet() { UserSettings.defaultDotComUUID = nil - let account = AccountBuilder(contextManager).build() + let account = makeAccount() XCTAssertFalse(account.isDefaultWordPressComAccount) } func testIsDefaultWordPressComAccountIsTrueWhenUUIDMatches() { - let account = AccountBuilder(contextManager).build() + let account = makeAccount() UserSettings.defaultDotComUUID = account.uuid XCTAssertTrue(account.isDefaultWordPressComAccount) } func testHasBlogsReturnsFalseWhenNoBlogsArePresentForAccount() { - let account = AccountBuilder(contextManager).build() + let account = makeAccount() XCTAssertFalse(account.hasBlogs) } @@ -28,12 +28,12 @@ class WPAccountLookupTests: CoreDataTestCase { } func testLookupDefaultWordPressComAccountReturnsNilWhenNoAccountIsSet() throws { - let _ = AccountBuilder(contextManager).build() + makeAccount() try XCTAssertNil(WPAccount.lookupDefaultWordPressComAccount(in: contextManager.mainContext)) } func testLookupDefaultWordPressComAccountReturnsAccount() throws { - let account = AccountBuilder(contextManager).build() + let account = makeAccount() UserSettings.defaultDotComUUID = account.uuid try XCTAssertEqual(WPAccount.lookupDefaultWordPressComAccount(in: contextManager.mainContext)?.uuid, account.uuid) @@ -47,40 +47,40 @@ class WPAccountLookupTests: CoreDataTestCase { } func testLookupAccountByUUIDReturnsNilForInvalidAccount() throws { - AccountBuilder(contextManager).build() + makeAccount() try XCTAssertNil(WPAccount.lookup(withUUIDString: "", in: contextManager.mainContext)) } func testLookupAccountByUUIDReturnsAccount() throws { let uuid = UUID().uuidString - AccountBuilder(contextManager).with(uuid: uuid).build() + makeAccount { $0.with(uuid: uuid) } try XCTAssertEqual(WPAccount.lookup(withUUIDString: uuid, in: contextManager.mainContext)?.uuid, uuid) } func testLookupAccountByUsernameReturnsNilIfNotFound() throws { - AccountBuilder(contextManager).build() + makeAccount() try XCTAssertNil(WPAccount.lookup(withUsername: "", in: contextManager.mainContext)) } func testLookupAccountByUsernameReturnsAccountForUsername() throws { let username = UUID().uuidString - AccountBuilder(contextManager).with(username: username).build() + makeAccount { $0.with(username: username) } try XCTAssertEqual(WPAccount.lookup(withUsername: username, in: contextManager.mainContext)?.username, username) } func testLookupAccountByUsernameReturnsAccountForEmailAddress() throws { let email = UUID().uuidString - AccountBuilder(contextManager).with(email: email).build() + makeAccount { $0.with(email: email) } try XCTAssertEqual(WPAccount.lookup(withUsername: email, in: contextManager.mainContext)?.email, email) } func testLookupByUserIdReturnsNilIfNotFound() throws { - AccountBuilder(contextManager).with(id: 1).build() // Make a test account that we don't want to match + makeAccount { $0.with(id: 1) } // Make a test account that we don't want to match try XCTAssertNil(WPAccount.lookup(withUserID: 2, in: contextManager.mainContext)) } func testLookupByUserIdReturnsAccount() throws { - AccountBuilder(contextManager).with(id: 1).build() + makeAccount { $0.with(id: 1) } try XCTAssertEqual(WPAccount.lookup(withUserID: 1, in: contextManager.mainContext)?.userID, 1) } @@ -89,10 +89,15 @@ class WPAccountLookupTests: CoreDataTestCase { } func testLookupNumberOfAccountsReturnsCorrectValue() throws { - let _ = AccountBuilder(contextManager).build() - let _ = AccountBuilder(contextManager).build() - let _ = AccountBuilder(contextManager).build() + makeAccount() + makeAccount() + makeAccount() try XCTAssertEqual(WPAccount.lookupNumberOfAccounts(in: contextManager.mainContext), 3) } + + @discardableResult + func makeAccount(_ additionalSetup: (AccountBuilder) -> (AccountBuilder) = { $0 }) -> WPAccount { + additionalSetup(AccountBuilder(contextManager.mainContext)).build() + } } diff --git a/WordPress/WordPressTest/WPAccount+ObjCLookupTests.m b/WordPress/WordPressTest/WPAccount+ObjCLookupTests.m index ca5cbc10d239..b0bf828b6066 100644 --- a/WordPress/WordPressTest/WPAccount+ObjCLookupTests.m +++ b/WordPress/WordPressTest/WPAccount+ObjCLookupTests.m @@ -15,12 +15,12 @@ - (void) setUp { } - (void) testLookupDefaultWordPressComAccountReturnsNilWhenNoAccountIsSet { - [[[AccountBuilder alloc] init: self.contextManager] build]; + [[[AccountBuilder alloc] initWithContext:self.contextManager.mainContext] build]; XCTAssertNil([WPAccount lookupDefaultWordPressComAccountInContext: self.contextManager.mainContext]); } - (void) testLookupDefaultWordPressComAccountReturnsAccount { - WPAccount *account = [[[AccountBuilder alloc] init: self.contextManager] build]; + WPAccount *account = [[[AccountBuilder alloc] initWithContext:self.contextManager.mainContext] build]; [UserSettings setDefaultDotComUUID: account.uuid]; XCTAssertEqual([WPAccount lookupDefaultWordPressComAccountInContext:self.contextManager.mainContext].uuid, account.uuid); } @@ -30,27 +30,27 @@ - (void) testLookupNumberOfAccountsReturnsZeroByDefault { } - (void) testLookupNumberOfAccountsReturnsCorrectValue { - [[[AccountBuilder alloc] init: self.contextManager] build]; - [[[AccountBuilder alloc] init: self.contextManager] build]; - [[[AccountBuilder alloc] init: self.contextManager] build]; + [[[AccountBuilder alloc] initWithContext:self.contextManager.mainContext] build]; + [[[AccountBuilder alloc] initWithContext:self.contextManager.mainContext] build]; + [[[AccountBuilder alloc] initWithContext:self.contextManager.mainContext] build]; XCTAssertEqual([WPAccount lookupNumberOfAccountsInContext: self.contextManager.mainContext], 3); } - (void) testLookupAccountByUsernameReturnsNilIfNotFound { - [[[AccountBuilder alloc] init: self.contextManager] build]; + [[[AccountBuilder alloc] initWithContext:self.contextManager.mainContext] build]; XCTAssertNil([WPAccount lookupWithUsername:@"" context:self.contextManager.mainContext]); } - (void) testLookupAccountByUsernameReturnsAccountForUsername { NSString *username = [[NSUUID new] UUIDString]; - [[[[AccountBuilder alloc] init:self.contextManager] withUsername: username] build]; + [[[[AccountBuilder alloc] initWithContext:self.contextManager.mainContext] withUsername:username] build]; XCTAssertEqual([WPAccount lookupWithUsername:username context:self.contextManager.mainContext].username, username); } - (void) testLookupAccountByUsernameReturnsAccountForEmailAddress { NSString *email = [[NSUUID new] UUIDString]; - [[[[AccountBuilder alloc] init: self.contextManager] withEmail:email] build]; + [[[[AccountBuilder alloc] initWithContext:self.contextManager.mainContext] withEmail:email] build]; XCTAssertEqual([WPAccount lookupWithUsername:email context:self.contextManager.mainContext].email, email); } diff --git a/WordPress/WordPressTest/WPCrashLoggingDataProviderTests.swift b/WordPress/WordPressTest/WPCrashLoggingDataProviderTests.swift index 8e82948a1e52..44e9e46bb73d 100644 --- a/WordPress/WordPressTest/WPCrashLoggingDataProviderTests.swift +++ b/WordPress/WordPressTest/WPCrashLoggingDataProviderTests.swift @@ -43,7 +43,7 @@ final class WPCrashLoggingDataProviderTests: XCTestCase { private func makeCoreDataStack() -> ContextManager { let contextManager = ContextManager.forTesting() - let account = AccountBuilder(contextManager) + let account = AccountBuilder(contextManager.mainContext) .with(id: Constants.defaultAccountID) .with(email: Constants.defaultAccountEmail) .with(username: Constants.defaultAccountUsername) diff --git a/WordPress/WordPressTest/WPUserAgentTests.m b/WordPress/WordPressTest/WPUserAgentTests.m index bac4c7379911..7c83f1b140a7 100644 --- a/WordPress/WordPressTest/WPUserAgentTests.m +++ b/WordPress/WordPressTest/WPUserAgentTests.m @@ -40,13 +40,20 @@ - (void)testUseWordPressUserAgentInWebViews XCTAssertEqualObjects([self currentUserAgentFromUserDefaults], defaultUA); XCTAssertEqualObjects([self currentUserAgentFromWebView], defaultUA); + if (@available(iOS 17, *)) { + XCTSkip("In iOS 17, WKWebView no longer reads User Agent from UserDefaults. Skipping while working on an alternative setup."); + } + [WPUserAgent useWordPressUserAgentInWebViews]; - XCTAssertEqualObjects([self currentUserAgentFromUserDefaults], wordPressUA); XCTAssertEqualObjects([self currentUserAgentFromWebView], wordPressUA); } - (void)testThatOriginalRemovalOfWPUseKeyUserAgentDoesntWork { + if (@available(iOS 17, *)) { + XCTSkip("In iOS 17, WKWebView no longer reads User Agent from UserDefaults. Skipping while working on an alternative setup."); + } + // get the original user agent NSString *originalUserAgentInWebView = [self currentUserAgentFromWebView]; NSLog(@"OriginalUserAgent (WebView): %@", originalUserAgentInWebView); @@ -64,7 +71,7 @@ - (void)testThatOriginalRemovalOfWPUseKeyUserAgentDoesntWork { NSString *shouldBeOriginalInWebView = [self currentUserAgentFromWebView]; NSLog(@"shouldBeOriginal (WebView): %@", shouldBeOriginalInWebView); - XCTAssertNotEqualObjects(originalUserAgentInWebView, shouldBeOriginalInWebView, "This agent should be the same"); + XCTAssertNotEqualObjects(originalUserAgentInWebView, shouldBeOriginalInWebView); } - (void)testThatCallingFromAnotherThreadWorks { diff --git a/WordPress/WordPressTest/Widgets/WidgetDataReaderTests.swift b/WordPress/WordPressTest/Widgets/WidgetDataReaderTests.swift deleted file mode 100644 index 4c00034a0bc2..000000000000 --- a/WordPress/WordPressTest/Widgets/WidgetDataReaderTests.swift +++ /dev/null @@ -1,164 +0,0 @@ -import XCTest -@testable import WordPress - -final class WidgetDataReaderTests: XCTestCase { - func testNoSiteWhenWidgetDataNotFound() { - let intent = SelectSiteIntent() - intent.site = Site(identifier: "test", display: "") - let sut = makeSUT( - makeUserDefaults(suiteName: #function), - makeCacheReader(isCacheExisted: false), - isLoggedIn: true - ) - - verifyWidgetStatus(sut, configuration: intent, defaultSiteID: 123, expectNoSite: true) - } - - func testSiteSelectedWithNoDefaultSiteAvailable() { - let intent = SelectSiteIntent() - intent.site = Site(identifier: nil, display: "") - let sut = makeSUT( - makeUserDefaults(suiteName: #function), - makeCacheReader(isCacheExisted: true), - isLoggedIn: true - ) - - verifyWidgetStatus(sut, configuration: intent, defaultSiteID: nil, expectSiteSelected: true) - } - - func testLoggedOut() { - let intent = SelectSiteIntent() - intent.site = Site(identifier: nil, display: "") - let sut = makeSUT( - makeUserDefaults(suiteName: #function), - makeCacheReader(isCacheExisted: true), - isLoggedIn: false - ) - - verifyWidgetStatus(sut, configuration: intent, defaultSiteID: nil, expectLoggedOut: true) - } - - func testNoDataWhenNoUserDefaults() { - let intent = SelectSiteIntent() - intent.site = Site(identifier: nil, display: "") - let sut = makeSUT( - nil, - makeCacheReader(isCacheExisted: true), - isLoggedIn: false - ) - - verifyWidgetStatus(sut, configuration: intent, defaultSiteID: 123, expectNoData: true) - } - - func testSiteSelected() { - let intent = SelectSiteIntent() - intent.site = Site(identifier: "test", display: "") - let sut = makeSUT( - makeUserDefaults(suiteName: #function), - makeCacheReader(isCacheExisted: true), - isLoggedIn: true - ) - - verifyWidgetStatus(sut, configuration: intent, defaultSiteID: 123, expectSiteSelected: true) - } -} - -extension WidgetDataReaderTests { - func makeSUT( - _ userDefaults: UserDefaults?, - _ cacheReader: WidgetDataCacheReader, - isLoggedIn: Bool - ) -> WidgetDataReader { - userDefaults?.set(isLoggedIn, forKey: AppConfiguration.Widget.Stats.userDefaultsLoggedInKey) - return WidgetDataReader(userDefaults, cacheReader) - } - - func makeUserDefaults(suiteName: String) -> UserDefaults? { - let userDefaults = UserDefaults(suiteName: suiteName) - userDefaults?.removePersistentDomain(forName: suiteName) - return userDefaults - } - - func makeCacheReader(isCacheExisted: Bool) -> MockHomeWidgetDataFileReader { - MockHomeWidgetDataFileReader(isMockDataReturned: isCacheExisted) - } - - func verifyWidgetStatus( - _ sut: WidgetDataReader, - configuration: SelectSiteIntent, - defaultSiteID: Int?, - expectDisabled: Bool = false, - expectNoData: Bool = false, - expectNoSite: Bool = false, - expectLoggedOut: Bool = false, - expectSiteSelected: Bool = false - ) { - let disabledExpectation = XCTestExpectation(description: "Disabled Expectation") - disabledExpectation.isInverted = !expectDisabled - let noDataExpectation = XCTestExpectation(description: "NoData Expectation") - noDataExpectation.isInverted = !expectNoData - let noSiteExpectation = XCTestExpectation(description: "NoSite Expectation") - noSiteExpectation.isInverted = !expectNoSite - let loggedOutExpectation = XCTestExpectation(description: "LoggedOut Expectation") - loggedOutExpectation.isInverted = !expectLoggedOut - let siteSelectedExpectation = XCTestExpectation(description: "NoSiteSelected Expectation") - siteSelectedExpectation.isInverted = !expectSiteSelected - - switch sut.widgetData( - for: configuration, - defaultSiteID: defaultSiteID - ) { - case .success: - siteSelectedExpectation.fulfill() - case .failure(let error): - switch error { - case .noData: - noDataExpectation.fulfill() - case .noSite: - noSiteExpectation.fulfill() - case .loggedOut: - loggedOutExpectation.fulfill() - case .jetpackFeatureDisabled: - disabledExpectation.fulfill() - } - } - wait(for: [ - disabledExpectation, - noDataExpectation, - noSiteExpectation, - loggedOutExpectation, - siteSelectedExpectation - ], timeout: 0.1) - } -} - -struct MockHomeWidgetDataFileReader: WidgetDataCacheReader { - let mockData = HomeWidgetTodayData(siteID: 0, - siteName: "My WordPress Site", - url: "", - timeZone: TimeZone.current, - date: Date(), - stats: TodayWidgetStats( - views: 649, - visitors: 572, - likes: 16, - comments: 8 - )) - let isMockDataReturned: Bool - - func widgetData(for siteID: String) -> T? { - if isMockDataReturned { - return mockData as? T - } else { - return nil - } - } - - func widgetData() -> [T]? { - if isMockDataReturned { - return [mockData] as? [T] - } else { - return nil - } - } -} diff --git a/WordPress/WordPressTest/WordPressUnitTests.xctestplan b/WordPress/WordPressTest/WordPressUnitTests.xctestplan index 005d677391c3..a42c2f0bf5ae 100644 --- a/WordPress/WordPressTest/WordPressUnitTests.xctestplan +++ b/WordPress/WordPressTest/WordPressUnitTests.xctestplan @@ -21,6 +21,8 @@ "environmentVariableEntries" : [ ], + "language" : "en", + "region" : "US", "targetForVariableExpansion" : { "containerPath" : "container:WordPress.xcodeproj", "identifier" : "1D6058900D05DD3D006BFB54", @@ -36,6 +38,13 @@ "name" : "WordPressFluxTests" } }, + { + "target" : { + "containerPath" : "container:..\/Modules", + "identifier" : "JetpackStatsWidgetsCoreTests", + "name" : "JetpackStatsWidgetsCoreTests" + } + }, { "target" : { "containerPath" : "container:WordPress.xcodeproj", diff --git a/WordPress/WordPress_Prefix.pch b/WordPress/WordPress_Prefix.pch index 537195ba27aa..87efd53d7df5 100644 --- a/WordPress/WordPress_Prefix.pch +++ b/WordPress/WordPress_Prefix.pch @@ -17,7 +17,6 @@ @import WordPressShared.WPMapFilterReduce; // Project-specific - #import "WPStyleGuide+ReadableMargins.h" #import "WPError.h" #ifndef IS_IPAD diff --git a/config/Version.internal.xcconfig b/config/Version.internal.xcconfig index 94c618f81f50..8016e20c9511 100644 --- a/config/Version.internal.xcconfig +++ b/config/Version.internal.xcconfig @@ -1,4 +1,2 @@ -VERSION_SHORT=23.5 - -// Internal long version example: VERSION_LONG=9.9.0.20180423 -VERSION_LONG=23.5.0.20231020 +VERSION_LONG = 24.0.0.20240108 +VERSION_SHORT = 24.0 diff --git a/config/Version.public.xcconfig b/config/Version.public.xcconfig index b8df3a3db349..1636e87e6260 100644 --- a/config/Version.public.xcconfig +++ b/config/Version.public.xcconfig @@ -1,4 +1,2 @@ -VERSION_SHORT=23.5 - -// Public long version example: VERSION_LONG=9.9.0.0 -VERSION_LONG=23.5.0.2 +VERSION_LONG = 24.0.0.0 +VERSION_SHORT = 24.0 diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 84c86771edd3..f5db36ffd890 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -26,9 +26,6 @@ SECRETS_DIR = File.join(Dir.home, '.configure', 'wordpress-ios', 'secrets') PROJECT_ENV_FILE_PATH = File.join(SECRETS_DIR, 'project.env') APP_STORE_CONNECT_KEY_PATH = File.join(SECRETS_DIR, 'app_store_connect_fastlane_api_key.json') -# Other defines used across multiple lanes -REPOSITORY_NAME = 'WordPress-iOS' - WORDPRESS_BUNDLE_IDENTIFIER = 'org.wordpress' WORDPRESS_EXTENSIONS_BUNDLE_IDENTIFIERS = %w[ WordPressShare @@ -52,24 +49,39 @@ ALL_JETPACK_BUNDLE_IDENTIFIERS = [JETPACK_BUNDLE_IDENTIFIER, *JETPACK_EXTENSIONS Dotenv.load(USER_ENV_FILE_PATH) Dotenv.load(PROJECT_ENV_FILE_PATH) GITHUB_REPO = 'wordpress-mobile/wordpress-iOS' -ENV['PROJECT_NAME'] = 'WordPress' -PUBLIC_CONFIG_FILE = File.join(PROJECT_ROOT_FOLDER, 'config', 'Version.Public.xcconfig') +DEFAULT_BRANCH = 'trunk' +PUBLIC_CONFIG_FILE = File.join(PROJECT_ROOT_FOLDER, 'config', 'Version.public.xcconfig') +# Unfortunately, ios_current_branch_is_hotfix relies on this ENV var under the hood. +# We can't get rid of it just yet. ENV['PUBLIC_CONFIG_FILE'] = PUBLIC_CONFIG_FILE -ENV['INTERNAL_CONFIG_FILE'] = File.join(PROJECT_ROOT_FOLDER, 'config', 'Version.internal.xcconfig') -ENV['PROJECT_ROOT_FOLDER'] = "#{PROJECT_ROOT_FOLDER}/" -ENV['APP_STORE_STRINGS_FILE_NAME'] = 'AppStoreStrings.po' +INTERNAL_CONFIG_FILE = File.join(PROJECT_ROOT_FOLDER, 'config', 'Version.internal.xcconfig') ENV['FASTLANE_WWDR_USE_HTTP1_AND_RETRIES'] = 'true' +# Fastlane's `git_branch` action and its relevant helpers use environment variables to modify the output. +# That means if we change the branch as part of an action, it'll return the incorrect branch. +# This environment variable disables that behavior. +# See https://github.com/fastlane/fastlane/pull/21597 +ENV['FL_GIT_BRANCH_DONT_USE_ENV_VARS'] = 'true' + +# Instanstiate versioning classes +VERSION_CALCULATOR = Fastlane::Wpmreleasetoolkit::Versioning::MarketingVersionCalculator.new +VERSION_FORMATTER = Fastlane::Wpmreleasetoolkit::Versioning::FourPartVersionFormatter.new +BUILD_CODE_FORMATTER = Fastlane::Wpmreleasetoolkit::Versioning::FourPartBuildCodeFormatter.new +PUBLIC_VERSION_FILE = Fastlane::Wpmreleasetoolkit::Versioning::IOSVersionFile.new(xcconfig_path: PUBLIC_CONFIG_FILE) + +# Instantiate internal versioning classes +INTERNAL_BUILD_CODE_CALCULATOR = Fastlane::Wpmreleasetoolkit::Versioning::DateBuildCodeCalculator.new +INTERNAL_VERSION_FILE = Fastlane::Wpmreleasetoolkit::Versioning::IOSVersionFile.new(xcconfig_path: INTERNAL_CONFIG_FILE) + +BUILDKITE_ORGANIZATION = 'automattic' +BUILDKITE_PIPELINE = 'wordpress-ios' + # Use this instead of getting values from ENV directly. It will throw an error if the requested value is missing def get_required_env(key) UI.user_error!("Environment variable '#{key}' is not set. Have you setup #{USER_ENV_FILE_PATH} correctly?") unless ENV.key?(key) ENV.fetch(key, nil) end -def get_app_version(xcconfig_path: PUBLIC_CONFIG_FILE) - ios_get_app_version(public_version_xcconfig_file: xcconfig_path) -end - def gutenberg_config! require 'yaml' @@ -78,12 +90,142 @@ def gutenberg_config! UI.user_error!("Could not find config YAML at path #{gutenberg_config_path}") unless File.exist?(gutenberg_config_path) begin - YAML.safe_load(File.read(gutenberg_config_path), symbolize_names: true) + YAML.safe_load_file(gutenberg_config_path, symbolize_names: true) rescue StandardError => e UI.user_error!("Could not parse config YAML. Failed with: #{e.message}") end end +######################################################################## +# Version Methods +######################################################################## + +# Returns the release version of the app in the format `1.2` or `1.2.3` if it is a hotfix +# +def release_version_current + # Read the current release version from the .xcconfig file and parse it into an AppVersion object + current_version = VERSION_FORMATTER.parse(PUBLIC_VERSION_FILE.read_release_version) + # Return the formatted release version + VERSION_FORMATTER.release_version(current_version) +end + +# Returns the internal release version of the app in the format `1.2` or `1.2.3` if it is a hotfix +# +def release_version_current_internal + # Read the current release version from the .xcconfig file and parse it into an AppVersion object + current_version = VERSION_FORMATTER.parse(INTERNAL_VERSION_FILE.read_release_version) + # Return the formatted release version + VERSION_FORMATTER.release_version(current_version) +end + +# Returns the next release version of the app in the format `1.2` or `1.2.3` if it is a hotfix +# +def release_version_next + # Read the current release version from the .xcconfig file and parse it into an AppVersion object + current_version = VERSION_FORMATTER.parse(PUBLIC_VERSION_FILE.read_release_version) + # Calculate the next release version + next_calculated_release_version = VERSION_CALCULATOR.next_release_version(version: current_version) + # Return the formatted release version + VERSION_FORMATTER.release_version(next_calculated_release_version) +end + +# Returns the current build code of the app +# +def build_code_current + # Read the current build code from the .xcconfig file and parse it into an AppVersion object + # The AppVersion is used because WP/JPiOS uses the four part (1.2.3.4) build code format, so the version + # calculator can be used to calculate the next four-part version + version = VERSION_FORMATTER.parse(PUBLIC_VERSION_FILE.read_build_code(attribute_name: 'VERSION_LONG')) + # Return the formatted build code + BUILD_CODE_FORMATTER.build_code(version:) +end + +# Returns the current internal build code of the app +# +def build_code_current_internal + # Read the current build code from the .xcconfig file and parse it into an AppVersion object + # The AppVersion is used because WP/JPiOS uses the four part (1.2.3.4) build code format, so the version + # calculator can be used to calculate the next four-part version + version = VERSION_FORMATTER.parse(INTERNAL_VERSION_FILE.read_build_code(attribute_name: 'VERSION_LONG')) + # Return the formatted build code + BUILD_CODE_FORMATTER.build_code(version:) +end + +# Returns the build code of the app for the code freeze. It is the release version name plus sets the build number to 0 +# +def build_code_code_freeze + # Read the current build code from the .xcconfig file and parse it into an AppVersion object + # The AppVersion is used because WP/JPiOS uses the four part (1.2.3.4) build code format, so the version + # calculator can be used to calculate the next four-part version + release_version_current = VERSION_FORMATTER.parse(INTERNAL_VERSION_FILE.read_release_version) + # Calculate the next release version, which will be used as the basis of the new build code + build_code_code_freeze = VERSION_CALCULATOR.next_release_version(version: release_version_current) + # Return the formatted build code + BUILD_CODE_FORMATTER.build_code(version: build_code_code_freeze) +end + +# Returns the internal build code of the app for the code freeze. It is the release version name plus sets the build +# number to 0 +# +def build_code_code_freeze_internal + # Read the current build code from the .xcconfig file and parse it into an AppVersion object + # The AppVersion is used because WP/JPiOS uses the four part (1.2.3.4) build code format, so the version + # calculator can be used to calculate the next four-part version + release_version_current = VERSION_FORMATTER.parse(INTERNAL_VERSION_FILE.read_release_version) + # Calculate the next release version, which will be used as the basis of the new build code + release_version_next = VERSION_CALCULATOR.next_release_version(version: release_version_current) + build_code_code_freeze = INTERNAL_BUILD_CODE_CALCULATOR.next_build_code(version: release_version_next) + # Return the formatted build code + BUILD_CODE_FORMATTER.build_code(version: build_code_code_freeze) +end + +# Returns the build code of the app for the code freeze. It is the hotfix version name plus sets the build number to 0 +# +def build_code_hotfix(release_version:) + version = VERSION_FORMATTER.parse(release_version) + # Return the formatted build code + BUILD_CODE_FORMATTER.build_code(version:) +end + +# Returns the next build code of the app +# +def build_code_next + # Read the current build code from the .xcconfig file and parse it into an AppVersion object + # The AppVersion is used because WP/JPiOS uses the four part (1.2.3.4) build code format, so the version + # calculator can be used to calculate the next four-part version + build_code_current = VERSION_FORMATTER.parse(PUBLIC_VERSION_FILE.read_build_code(attribute_name: 'VERSION_LONG')) + # Calculate the next build code + build_code_next = VERSION_CALCULATOR.next_build_number(version: build_code_current) + # Return the formatted build code + BUILD_CODE_FORMATTER.build_code(version: build_code_next) +end + +# Returns the next internal build code of the app +# +def build_code_next_internal + # Read the current build code from the .xcconfig file and parse it into an AppVersion object + # The AppVersion is used because WP/JPiOS uses the four part (1.2.3.4) build code format, so the version + # calculator can be used to calculate the next four-part version + build_code_current = VERSION_FORMATTER.parse(INTERNAL_VERSION_FILE.read_build_code(attribute_name: 'VERSION_LONG')) + # Calculate the next build code + build_code_next = INTERNAL_BUILD_CODE_CALCULATOR.next_build_code(version: build_code_current) + # Return the formatted build code + BUILD_CODE_FORMATTER.build_code(version: build_code_next) +end + +# Returns the next internal hotfix build code of the app +# +def build_code_hotfix_internal(release_version:) + # Read the current build code from the .xcconfig file and parse it into an AppVersion object + # The AppVersion is used because WP/JPiOS uses the four part (1.2.3.4) build code format, so the version + # calculator can be used to calculate the next four-part version + build_code_current = VERSION_FORMATTER.parse(release_version) + # Calculate the next build code + build_code_next = INTERNAL_BUILD_CODE_CALCULATOR.next_build_code(version: build_code_current) + # Return the formatted build code + BUILD_CODE_FORMATTER.build_code(version: build_code_next) +end + ######################################################################## # Group buildkite logs by action ######################################################################## @@ -118,6 +260,7 @@ import 'lanes/codesign.rb' import 'lanes/localization.rb' import 'lanes/release.rb' import 'lanes/screenshots.rb' +import 'lanes/release_management_in_ci.rb' ######################################################################## @@ -145,3 +288,28 @@ before_all do |lane| end # rubocop:enable Style/IfUnlessModifier end + +def compute_release_branch_name(options:, version: release_version_current) + branch_option = :branch + branch_name = options[branch_option] + + if branch_name.nil? + branch_name = release_branch_name(version:) + UI.message("No branch given via option '#{branch_option}'. Defaulting to #{branch_name}.") + end + + branch_name +end + +def release_branch_name(version: release_version_current) + "release/#{version}" +end + +def editorial_branch_name(version: release_version_current) + "release_notes/#{version}" +end + +def ensure_git_branch_is_release_branch + # Verify that the current branch is a release branch. Notice that `ensure_git_branch` expects a RegEx parameter + ensure_git_branch(branch: '^release/') +end diff --git a/fastlane/jetpack_metadata/ar-SA/release_notes.txt b/fastlane/jetpack_metadata/ar-SA/release_notes.txt new file mode 100644 index 000000000000..bbce8b261bff --- /dev/null +++ b/fastlane/jetpack_metadata/ar-SA/release_notes.txt @@ -0,0 +1,5 @@ +قمنا بتحديث المحرر التقليدي من خلال أدوات انتقاء الوسائط الجديدة في الصور ووسائط الموقع. لا داعي للقلق، لا يزال بإمكانك رفع الوسائط والفيديوهات والمزيد إلى موقعك. + +عند الحديث عن أنواع الوسائط، أصبح بإمكانك الآن إضافة عوامل تصفية الوسائط إلى شاشة وسائط الموقع. إذا كنت تستخدم iPhone، فستلاحظ وضع نسبة الارتفاع إلى العرض الجديد كذلك. يتوافر كلا الخيارين عند النقر على قائمة العنوان. + +أخيرًا، أصلحنا النافذة المنبثقة للامتثال المعطّلة التي تظهر في أثناء التحقق من الإحصاءات خلال عملية الإعداد. أصلحنا أيضًا عطلاً نادرًا حدث في أثناء تسجيل الخروج. رائع. diff --git a/fastlane/jetpack_metadata/de-DE/release_notes.txt b/fastlane/jetpack_metadata/de-DE/release_notes.txt new file mode 100644 index 000000000000..876e13b36c82 --- /dev/null +++ b/fastlane/jetpack_metadata/de-DE/release_notes.txt @@ -0,0 +1,5 @@ +Der klassische Editor wurde mit neuen Medienauswahlen für Fotos und Website-Medien ersetzt. Keine Sorge: Du kannst weiterhin Bilder, Videos und mehr auf deine Website hochladen. + +Apropos Medientypen: Ab sofort kannst du Medienfilter zum Bildschirm für Website-Medien hinzufügen. iPhone-Benutzern wird auch der neue Modus für das Bildformat auffallen. Beide Optionen sind verfügbar, wenn du auf das Titelmenü tippst. + +Außerdem haben wir das fehlerhafte Compliance-Pop-up korrigiert, das angezeigt wurde, wenn du Statistiken während des Onboarding-Prozesses überprüft hast. Zu guter Letzt wurde ein seltener Fehler während der Abmeldung behoben. Das ist doch super. diff --git a/fastlane/jetpack_metadata/default/name.txt b/fastlane/jetpack_metadata/default/name.txt index d5c4ce56b424..887a271e63e1 100644 --- a/fastlane/jetpack_metadata/default/name.txt +++ b/fastlane/jetpack_metadata/default/name.txt @@ -1 +1 @@ -Jetpack – Website Builder +Jetpack for WordPress diff --git a/fastlane/metadata/ar-SA/privacy_url.txt b/fastlane/jetpack_metadata/default/privacy_url.txt similarity index 100% rename from fastlane/metadata/ar-SA/privacy_url.txt rename to fastlane/jetpack_metadata/default/privacy_url.txt diff --git a/fastlane/jetpack_metadata/default/release_notes.txt b/fastlane/jetpack_metadata/default/release_notes.txt index 29374fdbe065..93a123fb2d20 100644 --- a/fastlane/jetpack_metadata/default/release_notes.txt +++ b/fastlane/jetpack_metadata/default/release_notes.txt @@ -1,7 +1,5 @@ -* [*] Fix a crash when the blog's blogging prompt settings contain invalid JSON [#21677] -* [*] Block Editor: Split formatted text on triple Enter [https://github.com/WordPress/gutenberg/pull/53354] -* [*] Block Editor: Quote block: Ensure border is visible with block-based themes in dark [https://github.com/WordPress/gutenberg/pull/54964] -* [*] (Internal) Remove .nativePhotoPicker feature flag and the disabled code [#21681](https://github.com/wordpress-mobile/WordPress-iOS/pull/21681) -* [**] Reader: Improvement of core UI elements, including feed cards, tag and site headers, buttons and recommendation sections. [#21772] -* [***] [internal] Added paid domain selection, plan selection, and checkout screens in site creation flow [#21688] +We updated the classic editor with new media pickers for Photos and Site Media. Don’t worry, you can still upload images, videos, and more to your site. +Speaking of media types—you can now add media filters to the Site Media screen. If you’re using an iPhone, you’ll notice the new aspect ratio mode, too. Both options are available when you tap the title menu. + +Finally, we fixed the broken compliance pop-up that appears while you’re checking stats during the onboarding process. We also fixed a rare crash that happened while logging out. Sweet. diff --git a/fastlane/jetpack_metadata/es-ES/release_notes.txt b/fastlane/jetpack_metadata/es-ES/release_notes.txt new file mode 100644 index 000000000000..6239cda1574f --- /dev/null +++ b/fastlane/jetpack_metadata/es-ES/release_notes.txt @@ -0,0 +1,5 @@ +Hemos actualizado el editor clásico con nuevos selectores de medios para fotos y medios del sitio. No te preocupes: puedes seguir subiendo imágenes, vídeos y mucho más a tu sitio. + +Hablando de tipos de medios: ahora puedes añadir filtros de medios a la pantalla de medios del sitio. Si usas un iPhone, también notarás el nuevo modo de relación de aspecto. Ambas opciones están disponibles al tocar el menú de títulos. + +Por último, hemos corregido la ventana emergente de cumplimiento que aparecía mientras comprobabas las estadísticas durante el proceso de incorporación. También hemos corregido un fallo poco frecuente que se producía al salir de la sesión. Perfecto. diff --git a/fastlane/jetpack_metadata/fr-FR/release_notes.txt b/fastlane/jetpack_metadata/fr-FR/release_notes.txt new file mode 100644 index 000000000000..745c60cdc32d --- /dev/null +++ b/fastlane/jetpack_metadata/fr-FR/release_notes.txt @@ -0,0 +1,5 @@ +Nous avons mis à jour l’éditeur classique avec de nouveaux sélecteurs de médias pour les photos et les médias du site. Pas d’inquiétude, vous pouvez toujours mettre en ligne des images, des vidéos, et plus encore sur votre site. + +En parlant de types de médias : vous pouvez désormais ajouter des filtres médias à l’écran Médias du site. Si vous utilisez un iPhone, vous remarquerez également le nouveau mode Proportions. Les deux options sont disponibles lorsque vous appuyez sur le menu Titre. + +Enfin, nous avons réparé la pop-up qui apparaît lorsque vous consultez les statistiques à l’occasion du processus de configuration. Nous avons par ailleurs corrigé un incident rare qui se produisait lors de la déconnexion. Pas mal. diff --git a/fastlane/jetpack_metadata/he/release_notes.txt b/fastlane/jetpack_metadata/he/release_notes.txt new file mode 100644 index 000000000000..81455740c244 --- /dev/null +++ b/fastlane/jetpack_metadata/he/release_notes.txt @@ -0,0 +1,5 @@ +עדכנו את העורך הקלאסי בבוררי מדיה חדשים לתמונות מצולמות ולמדיה באתר. לא לדאוג – אין בעיה להמשיך להעלות לאתר תמונות, סרטונים ועוד. + +ואם כבר מדברים על סוגי מדיה – מעכשיו אפשר להוסיף מסנני מדיה למסך 'מדיה באתר'. משתמשי iPhone יבחינו גם במצב יחס תצוגה חדש. שתי האפשרויות זמינות בהקשה על תפריט שם האתר. + +ולבסוף, תוקנו החלונות הקופצים השבורים של התאמה לדרישות, שצצו תוך כדי בדיקת נתונים סטטיסטיים בתהליך ההצטרפות. תיקנו גם בעיית קריסה נדירה שהייתה מתרחשת בעת התנתקות. נחמד. diff --git a/fastlane/jetpack_metadata/id/release_notes.txt b/fastlane/jetpack_metadata/id/release_notes.txt new file mode 100644 index 000000000000..17aa0e3eaa84 --- /dev/null +++ b/fastlane/jetpack_metadata/id/release_notes.txt @@ -0,0 +1,5 @@ +Kami memperbarui editor klasik dengan pemilih media baru untuk Foto dan Media Situs. Namun, Anda masih dapat mengunggah gambar, video, dan lain-lain ke situs Anda. + +Terkait dengan tipe media, kini Anda dapat menambahkan filter media ke layar Media Situs. Jika menggunakan iPhone, Anda pasti juga akan melihat mode rasio aspek yang baru. Kedua pilihan tersedia jika Anda mengetuk menu judul. + +Kami telah memperbaiki kerusakan pop-up kepatuhan yang muncul ketika Anda memeriksa statistik selama proses penyiapan. Kami juga memperbaiki crash yang jarang terjadi selama logout. Mantap. diff --git a/fastlane/jetpack_metadata/it/release_notes.txt b/fastlane/jetpack_metadata/it/release_notes.txt new file mode 100644 index 000000000000..51e7377b14b1 --- /dev/null +++ b/fastlane/jetpack_metadata/it/release_notes.txt @@ -0,0 +1,5 @@ +Abbiamo aggiornato l'editor classico con nuovi contenuti multimediali per Foto e Media sito. Non preoccuparti, puoi ancora caricare immagini, video e altro sul tuo sito. + +A proposito di tipi di media, ora puoi aggiungere filtri per i contenuti multimediali nella schermata Media sito. Se usi un iPhone, noterai anche la nuova modalità di rapporto d'aspetto. Entrambe le opzioni sono disponibili quando clicchi sul titolo del menu. + +Infine, abbiamo sistemato il pop-up di conformità non funzionante che appare mentre si controllano le statistiche durante il processo di onboarding. Abbiamo anche risolto un raro crash che si verificava durante la disconnessione. Carino. diff --git a/fastlane/jetpack_metadata/ja/release_notes.txt b/fastlane/jetpack_metadata/ja/release_notes.txt new file mode 100644 index 000000000000..23b58dafb7dd --- /dev/null +++ b/fastlane/jetpack_metadata/ja/release_notes.txt @@ -0,0 +1,5 @@ +クラシックエディターを更新し、写真とサイトメディア用の新しいメディアピッカーを追加しました。 引き続き、画像や動画などをサイトにアップロードできます。 + +メディアタイプでは、サイトメディア画面にメディアフィルターを追加できるようになりました。 iPhone を使用している場合、新しい縦横比モードでも表示されます。 タイトルメニューをタップすると、両方のオプションが利用可能になります。 + +ようやく、オンボーディングプロセス中に統計情報を確認しているときに表示される、機能しないコンプライアンスポップアップを修正しました。 ログアウト中にまれに発生するクラッシュも修正されました。 ぜひ活用してください。 diff --git a/fastlane/jetpack_metadata/ko/release_notes.txt b/fastlane/jetpack_metadata/ko/release_notes.txt new file mode 100644 index 000000000000..d123967af569 --- /dev/null +++ b/fastlane/jetpack_metadata/ko/release_notes.txt @@ -0,0 +1,5 @@ +사진 및 사이트 미디어에 대한 새로운 미디어 선택기로 구 버전 편집기를 업데이트했습니다. 걱정하지 마세요. 여전히 사이트에 이미지, 비디오 등을 업로드할 수 있습니다. + +미디어 유형의 경우 이제 사이트 미디어 화면에 미디어 필터를 추가할 수 있습니다. iPhone을 사용하는 경우 새로운 화면 비율 모드도 표시됩니다. 두 가지 옵션 모두 제목 메뉴를 눌러서 이용할 수 있습니다. + +마지막으로, 온보딩 프로세스 도중에 통계를 확인하는 동안 나타나는 손상된 규정 준수 팝업을 해결했습니다. 로그아웃하는 동안 드물게 발생하는 충돌도 해결했습니다. 상쾌합니다. diff --git a/fastlane/jetpack_metadata/nl-NL/release_notes.txt b/fastlane/jetpack_metadata/nl-NL/release_notes.txt new file mode 100644 index 000000000000..1d6040799515 --- /dev/null +++ b/fastlane/jetpack_metadata/nl-NL/release_notes.txt @@ -0,0 +1,5 @@ +We hebben de klassieke editor bijgewerkt met nieuwe mediakiezers voor foto's en sitemedia. Geen zorgen, je kan nog steeds afbeeldingen, video's en meer uploaden naar je site. + +Over mediatypen gesproken: je kan nu mediafilters toevoegen aan het scherm Sitemedia. Als je een iPhone gebruikt, zie je ook de nieuwe modus voor beeldverhouding. Beide opties zijn beschikbaar als je op het titelmenu tikt. + +Tot slot hebben we de defecte pop-up voor naleving gemaakt die verschijnt als je statistieken bekijkt tijdens het onboardingproces. We hebben ook een zeldzame crash bij het uitloggen opgelost. Handig, toch? diff --git a/fastlane/jetpack_metadata/pt-BR/release_notes.txt b/fastlane/jetpack_metadata/pt-BR/release_notes.txt new file mode 100644 index 000000000000..6cee530ca0bd --- /dev/null +++ b/fastlane/jetpack_metadata/pt-BR/release_notes.txt @@ -0,0 +1,5 @@ +Atualizamos o editor clássico com novos seletores de mídia para Mídia do site e Fotos. Não se preocupe, você ainda pode fazer upload de imagens, vídeos e muito mais no seu site. + +E por falar nisso, agora é possível adicionar filtros de mídia à tela Mídia do site. Se você estiver usando um iPhone, notará um novo modo de proporção de tela também. Ambas as opções estão disponíveis ao tocar no menu de título. + +Por fim, corrigimos o pop-up de conformidade corrompido que aparece ao verificar as estatísticas durante o processo de integração. Também corrigimos uma falha rara que ocorria ao fazer logout. Incrível. diff --git a/fastlane/jetpack_metadata/ru/release_notes.txt b/fastlane/jetpack_metadata/ru/release_notes.txt new file mode 100644 index 000000000000..835ef24019ec --- /dev/null +++ b/fastlane/jetpack_metadata/ru/release_notes.txt @@ -0,0 +1,5 @@ +Мы обновили классический редактор, добавив новые инструменты выбора медиафайлов в разделы «Фотографии» и «Медиафайлы сайта». Не беспокойтесь, вы по-прежнему можете загружать на свой сайт изображения, видео и всё остальное. + +Что касается типов медиафайлов, теперь можно добавлять их фильтры на экран «Медиафайлы сайта». Если вы пользуетесь iPhone, вы также заметите новый режим соотношения сторон. Доступ к обеим опциям открывается при нажатии меню заголовка. + +Ну и наконец, мы исправили сбой всплывающего окна с предупреждением о соответствии требованиям, которое появляется, когда вы проверяете статистику в процессе регистрации. Мы также устранили ошибку, которая иногда приводила к аварийному завершению работы при выходе из системы. Отлично. diff --git a/fastlane/jetpack_metadata/sv/release_notes.txt b/fastlane/jetpack_metadata/sv/release_notes.txt new file mode 100644 index 000000000000..9fbb706f3d2a --- /dev/null +++ b/fastlane/jetpack_metadata/sv/release_notes.txt @@ -0,0 +1,5 @@ +Vi har uppdaterat den klassiska redigeraren med nya mediaväljare för foton och webbplatsmedia. Oroa dig inte, du kan fortfarande ladda upp bilder, videoklipp och annat till din webbplats. + +På tal om olika typer av media – du kan nu lägga till mediafilter på skärmen Webbplatsmedia. Om du använder en iPhone kommer du även att märka det nya bildförhållandeläget. Båda alternativen är tillgängliga när du trycker på rubrikmenyn. + +Slutligen har vi åtgärdat det trasiga popup-fönstret rörande efterlevnad som visas när man kollar statistik under onboardingprocessen. Vi har också åtgärdat en sällsynt krasch som kunde uppstå vid utloggning. Perfekt. diff --git a/fastlane/jetpack_metadata/tr/release_notes.txt b/fastlane/jetpack_metadata/tr/release_notes.txt new file mode 100644 index 000000000000..b13e1ca5241b --- /dev/null +++ b/fastlane/jetpack_metadata/tr/release_notes.txt @@ -0,0 +1,5 @@ +Fotoğraflar ve Site Ortamı için yeni ortam seçicilerle klasik düzenleyiciyi güncelledik. Endişelenmeyin; görselleri, videoları ve dahasını sitenize yüklemeye devam edebilirsiniz. + +Ortam türleriyle ilgili konuşuyorken artık Site Ortamı ekranına ortam filtreleri ekleyebileceğinizi de paylaşmak isteriz. iPhone kullanıyorsanız yeni en boy oranı modunu da fark edeceksiniz. Başlık menüsüne dokunduğunuzda iki seçenek de kullanılabilir. + +Son olarak, siz hazırlık süreci sırasında istatistikleri kontrol ederken görünen bozuk uyumluluk açılır penceresini düzelttik. Ayrıca oturum kapatılırken gerçekleşen nadir bir kilitlenme sorununu da düzelttik. Çok hoş. diff --git a/fastlane/jetpack_metadata/zh-Hans/release_notes.txt b/fastlane/jetpack_metadata/zh-Hans/release_notes.txt new file mode 100644 index 000000000000..ffd34b42f11c --- /dev/null +++ b/fastlane/jetpack_metadata/zh-Hans/release_notes.txt @@ -0,0 +1,5 @@ +我们更新了经典编辑器,添加了用于照片和站点媒体的新媒体选择器。 别担心,您仍然可以将图片、视频等内容上传至您的站点。 + +至于媒体类型,您现在可以在“站点媒体”屏幕上添加媒体过滤器。 如果您使用的是 iPhone,您还会注意到新的宽高比模式。 轻点标题菜单,即可在两个选项中进行切换。 + +最后,我们修复了在入门流程中查看统计信息时出现的合规性弹窗不完整的问题。 我们还修复了一个在注销时极少出现的崩溃问题。 很贴心。 diff --git a/fastlane/jetpack_metadata/zh-Hant/release_notes.txt b/fastlane/jetpack_metadata/zh-Hant/release_notes.txt new file mode 100644 index 000000000000..24003017c2a2 --- /dev/null +++ b/fastlane/jetpack_metadata/zh-Hant/release_notes.txt @@ -0,0 +1,5 @@ +我們更新了傳統編輯器,為照片和網站媒體加入全新的媒體選擇器。 別擔心,你仍可以上傳圖片、影片和更多內容到網站上。 + +說到媒體類型,你現在可以在「網站媒體」畫面新增媒體篩選條件。 若你使用 iPhone,也會注意到新加入的畫面比例模式。 點選標題選單時,會出現兩種選項。 + +最後,我們修復了在新手體驗流程中,當你在查看統計資料時,會出現的故障合規快顯視窗。 我們也修正了登出時偶爾會出現的當機問題。 真是太棒了。 diff --git a/fastlane/lanes/build.rb b/fastlane/lanes/build.rb index 8cafe6f91465..362d25fa7db8 100644 --- a/fastlane/lanes/build.rb +++ b/fastlane/lanes/build.rb @@ -101,7 +101,16 @@ # Only run Jetpack UI tests in parallel. # At the time of writing, we need to explicitly set this value despite using test plans that configure parallelism. - parallel_testing_value = options[:name].include?('Jetpack') + # + # Disabled to test if it makes a difference performance wise in Xcode 15.0.1 in CI as we've seen errors such as this one: + # https://github.com/wordpress-mobile/WordPress-iOS/pull/21921#issuecomment-1820707121 + # + # Also, simply disabling at the test plan level doesn't seem to have effect. + # In this CI run, it can be seen that there are at least two clones (UI tests logs on iPad, lines 1930 to 1934): + # https://buildkite.com/automattic/wpios-macv2-test/builds/14#018bfb60-6b6e-4a31-9acd-d27ee6f053e8/398-1930 + # + # parallel_testing_value = options[:name].include?('Jetpack') + parallel_testing_value = false run_tests( workspace: WORKSPACE_PATH, @@ -127,7 +136,7 @@ # Builds the WordPress app and uploads it to TestFlight, for beta-testing or final release # # @option [Boolean] skip_confirm (default: false) If true, avoids any interactive prompt - # @option [Boolean] skip_prechecks (default: false) If true, don't run the ios_build_prechecks and ios_build_preflight + # @option [Boolean] skip_prechecks (default: false) If true, don't run the prechecks and ios_build_preflight # @option [Boolean] create_release If true, creates a GitHub Release draft after the upload, with zipped xcarchive as artefact # @option [Boolean] beta_release If true, the GitHub release will be marked as being a pre-release # @@ -135,10 +144,16 @@ # desc 'Builds and uploads for distribution to App Store Connect' lane :build_and_upload_app_store_connect do |options| - ios_build_prechecks(skip_confirm: options[:skip_confirm], external: true) unless options[:skip_prechecks] - ios_build_preflight unless options[:skip_prechecks] + unless options[:skip_prechecks] + ensure_git_status_clean unless is_ci + ios_build_preflight + end + + UI.important("Building version #{release_version_current} (#{build_code_current}) and uploading to TestFlight") + UI.user_error!('Aborted by user request') unless options[:skip_confirm] || UI.confirm('Do you want to continue?') sentry_check_cli_installed + appstore_code_signing gym( @@ -165,7 +180,7 @@ archive_zip_path = File.join(PROJECT_ROOT_FOLDER, 'WordPress.xarchive.zip') zip(path: lane_context[SharedValues::XCODEBUILD_ARCHIVE], output_path: archive_zip_path) - version = options[:beta_release] ? ios_get_build_version : get_app_version + version = options[:beta_release] ? build_code_current : release_version_current create_release( repository: GITHUB_REPO, version:, @@ -216,8 +231,13 @@ # desc 'Builds and uploads for distribution to App Center' lane :build_and_upload_app_center do |options| - ios_build_prechecks(skip_confirm: options[:skip_confirm], internal: true) unless options[:skip_prechecks] - ios_build_preflight unless options[:skip_prechecks] + unless options[:skip_prechecks] + ensure_git_status_clean unless is_ci + ios_build_preflight + end + + UI.important("Building internal version #{release_version_current_internal} (#{build_code_current_internal}) and uploading to App Center") + UI.user_error!('Aborted by user request') unless options[:skip_confirm] || UI.confirm('Do you want to continue?') sentry_check_cli_installed diff --git a/fastlane/lanes/localization.rb b/fastlane/lanes/localization.rb index bd46e7fb7359..cad385821e8c 100644 --- a/fastlane/lanes/localization.rb +++ b/fastlane/lanes/localization.rb @@ -101,7 +101,7 @@ # Used in `update_*_metadata_on_app_store_connect` lanes. # UPLOAD_TO_APP_STORE_COMMON_PARAMS = { - app_version: get_app_version, + app_version: release_version_current, skip_binary_upload: true, overwrite_screenshots: true, phased_release: true, @@ -187,8 +187,33 @@ # desc 'Updates the AppStoreStrings.po file with the latest data' lane :update_appstore_strings do |options| + ensure_git_status_clean + + release_version = release_version_current + + unless Fastlane::Helper::GitHelper.checkout_and_pull(editorial_branch_name(version: release_version)) + UI.user_error!("Editorialization branch for version #{release_version} doesn't exist.") + end + update_wordpress_appstore_strings(options) update_jetpack_appstore_strings(options) + + unless options[:skip_confirm] || UI.confirm('Ready to push changes to remote and continue with the editorialization process?') + UI.message("Aborting as requested. Don't forget to push the changes and create the integration PR manually.") + next + end + + push_to_git_remote(tags: false) + + pr_url = create_release_management_pull_request( + base_branch: compute_release_branch_name(options:, version: release_version), + title: "Merge editorialized release notes in #{release_version}" + ) + + message = <<~MESSAGE + Release notes and metadata localization sources successfully generated. Next, review and merge the [integration PR](#{pr_url}). + MESSAGE + buildkite_annotate(context: 'editorialization-completed', style: 'success', message:) if is_ci end # Updates the `AppStoreStrings.po` file for WordPress, with the latest content from the `release_notes.txt` file and the other text sources @@ -199,7 +224,7 @@ lane :update_wordpress_appstore_strings do |options| source_metadata_folder = File.join(PROJECT_ROOT_FOLDER, 'fastlane', 'metadata', 'default') custom_metadata_folder = File.join(PROJECT_ROOT_FOLDER, 'fastlane', 'appstoreres', 'metadata', 'source') - version = options.fetch(:version, get_app_version) + version = options.fetch(:version, release_version_current) files = { whats_new: WORDPRESS_RELEASE_NOTES_PATH, @@ -237,7 +262,7 @@ # See details below for why that was done and why we should keep the definition in the codebase for future use. # source_metadata_folder = File.join(PROJECT_ROOT_FOLDER, 'fastlane', 'jetpack_metadata', 'default') # custom_metadata_folder = File.join(PROJECT_ROOT_FOLDER, 'fastlane', 'appstoreres', 'jetpack_metadata', 'source') - version = options.fetch(:version, get_app_version) + version = options.fetch(:version, release_version_current) files = { whats_new: JETPACK_RELEASE_NOTES_PATH @@ -354,7 +379,7 @@ def download_localized_app_store_metadata(glotpress_project_url:, locales:, meta locales_map = GLOTPRESS_TO_ASC_METADATA_LOCALE_CODES.slice(*locales) target_files = { - "v#{get_app_version}-whats-new": { desc: 'release_notes.txt', max_size: 4000 }, + "v#{release_version_current}-whats-new": { desc: 'release_notes.txt', max_size: 4000 }, app_store_name: { desc: 'name.txt', max_size: 30 }, app_store_subtitle: { desc: 'subtitle.txt', max_size: 30 }, app_store_desc: { desc: 'description.txt', max_size: 4000 }, diff --git a/fastlane/lanes/release.rb b/fastlane/lanes/release.rb index 1bc4ffe3cf73..50b545a14fa3 100644 --- a/fastlane/lanes/release.rb +++ b/fastlane/lanes/release.rb @@ -13,11 +13,61 @@ # desc 'Executes the initial steps needed during code freeze' lane :code_freeze do |options| + # Verify that there's nothing in progress in the working copy + ensure_git_status_clean + + # Check out the up-to-date default branch, the designated starting point for the code freeze + Fastlane::Helper::GitHelper.checkout_and_pull(DEFAULT_BRANCH) + + # Make sure that Gutenberg is configured as expected for a successful code freeze gutenberg_dep_check - ios_codefreeze_prechecks(options) - ios_bump_version_release - new_version = get_app_version + release_branch_name = compute_release_branch_name(options:, version: release_version_next) + + # The `release_version_next` is used as the `new internal release version` value because the external and internal + # release versions are always the same. + message = <<~MESSAGE + Code Freeze: + • New release branch from #{DEFAULT_BRANCH}: #{release_branch_name} + + • Current release version and build code: #{release_version_current} (#{build_code_current}). + • New release version and build code: #{release_version_next} (#{build_code_code_freeze}). + + • Current internal release version and build code: #{release_version_current_internal} (#{build_code_current_internal}) + • New internal release version and build code: #{release_version_next} (#{build_code_code_freeze_internal}) + MESSAGE + + UI.important(message) + + skip_user_confirmation = options[:skip_confirm] + + UI.user_error!('Aborted by user request') unless skip_user_confirmation || UI.confirm('Do you want to continue?') + + UI.message 'Creating release branch...' + Fastlane::Helper::GitHelper.create_branch(release_branch_name, from: DEFAULT_BRANCH) + UI.success "Done! New release branch is: #{git_branch}" + + # Bump the release version and build code and write it to the `xcconfig` file + UI.message 'Bumping release version and build code...' + PUBLIC_VERSION_FILE.write( + version_short: release_version_next, + version_long: build_code_code_freeze + ) + UI.success "Done! New Release Version: #{release_version_current}. New Build Code: #{build_code_current}" + + # Bump the internal release version and build code and write it to the `xcconfig` file + UI.message 'Bumping internal release version and build code...' + INTERNAL_VERSION_FILE.write( + # The external and internal release versions are always the same. Because the external release version was + # already bumped, we want to just use the `release_version_current` + version_short: release_version_current, + version_long: build_code_code_freeze_internal + ) + UI.success "Done! New Internal Release Version: #{release_version_current_internal}. New Internal Build Code: #{build_code_current_internal}" + + commit_version_and_build_files + + new_version = release_version_current release_notes_source_path = File.join(PROJECT_ROOT_FOLDER, 'RELEASE-NOTES.txt') extract_release_notes_for_version( @@ -42,22 +92,49 @@ release_notes_file_path: release_notes_source_path, extracted_notes_file_path: extracted_release_notes_file_path(app: :jetpack) ) - ios_update_release_notes(new_version:) - - if prompt_for_confirmation( - message: 'Ready to push changes to remote to let the automation configure it on GitHub?', - bypass: ENV.fetch('RELEASE_TOOLKIT_SKIP_PUSH_CONFIRM', nil) + ios_update_release_notes( + new_version:, + release_notes_file_path: release_notes_source_path ) - push_to_git_remote(tags: false) - else - UI.message('Aborting code completion. See you later.') + + unless skip_user_confirmation || UI.confirm('Ready to push changes to remote to let the automation configure it on GitHub?') + UI.message("Terminating as requested. Don't forget to run the remainder of this automation manually.") + next + end + + push_to_git_remote(tags: false) + + attempts = 0 + begin + attempts += 1 + set_branch_protection(repository: GITHUB_REPO, branch: release_branch_name) + rescue StandardError => e + if attempts < 2 + sleep_time = 5 + UI.message("Failed to set branch protection on GitHub. Retrying in #{sleep_time} seconds in case it was because the API hadn't noticed the new branch yet.") + sleep(sleep_time) + retry + else + UI.error("Failed to set branch protection on GitHub after #{attempts} attempts") + raise e + end end - setbranchprotection(repository: GITHUB_REPO, branch: "release/#{new_version}") setfrozentag(repository: GITHUB_REPO, milestone: new_version) ios_check_beta_deps(podfile: File.join(PROJECT_ROOT_FOLDER, 'Podfile')) print_release_notes_reminder + + message = <<~MESSAGE + Code freeze started successfully. + + Next steps: + + - Checkout `#{release_branch_name}` branch locally + - Update pods and release notes + - Finalize the code freeze + MESSAGE + buildkite_annotate(context: 'code-freeze-success', style: 'success', message:) if is_ci end # Executes the final steps for the code freeze @@ -69,43 +146,129 @@ # desc 'Completes the final steps for the code freeze' lane :complete_code_freeze do |options| - ios_completecodefreeze_prechecks(options) + ensure_git_branch_is_release_branch + + # Verify that there's nothing in progress in the working copy + ensure_git_status_clean + + version = release_version_current + + UI.important("Completing code freeze for: #{version}") + + skip_user_confirmation = options[:skip_confirm] + + UI.user_error!('Aborted by user request') unless skip_user_confirmation || UI.confirm('Do you want to continue?') + generate_strings_file_for_glotpress - if prompt_for_confirmation( - message: 'Ready to push changes to remote and trigger the beta build?', - bypass: ENV.fetch('RELEASE_TOOLKIT_SKIP_PUSH_CONFIRM', nil) - ) - push_to_git_remote(tags: false) - trigger_beta_build - else - UI.message('Aborting code freeze completion. See you later.') + unless skip_user_confirmation || UI.confirm('Ready to push changes to remote and trigger the beta build?') + UI.message("Terminating as requested. Don't forget to run the remainder of this automation manually.") + next end + + push_to_git_remote(tags: false) + + trigger_beta_build + + pr_url = create_release_management_pull_request( + base_branch: DEFAULT_BRANCH, + title: "Merge #{version} code freeze" + ) + + message = <<~MESSAGE + Code freeze completed successfully. Next, review and merge the [integration PR](#{pr_url}). + MESSAGE + buildkite_annotate(context: 'code-freeze-completed', style: 'success', message:) if is_ci end # Creates a new beta by bumping the app version appropriately then triggering a beta build on CI # # @option [Boolean] skip_confirm (default: false) If true, avoids any interactive prompt - # @option [String] base_version (default: _current app version_) If set, bases the beta on the specified version - # and `release/` branch instead of the current one. Useful for triggering betas on hotfixes for example. # desc 'Trigger a new beta build on CI' lane :new_beta_release do |options| - ios_betabuild_prechecks(options) + ensure_git_status_clean + + Fastlane::Helper::GitHelper.checkout_and_pull(DEFAULT_BRANCH) + + release_version = release_version_current + + # Check branch + unless Fastlane::Helper::GitHelper.checkout_and_pull(compute_release_branch_name(options:, version: release_version)) + UI.user_error!("Release branch for version #{release_version} doesn't exist.") + end + + ensure_git_branch_is_release_branch # This check is mostly redundant + + # The `release_version_next` is used as the `new internal release version` value because the external and internal + # release versions are always the same. + message = <<~MESSAGE + • Current build code: #{build_code_current} + • New build code: #{build_code_next} + + • Current internal build code: #{build_code_current_internal} + • New internal build code: #{build_code_next_internal} + MESSAGE + + UI.important(message) + + skip_user_confirmation = options[:skip_confirm] + + UI.user_error!('Aborted by user request') unless skip_user_confirmation || UI.confirm('Do you want to continue?') + generate_strings_file_for_glotpress download_localized_strings_and_metadata(options) - lint_localizations - ios_bump_version_beta + lint_localizations(allow_retry: skip_user_confirmation == false) + + bump_build_codes - if prompt_for_confirmation( - message: 'Ready to push changes to remote and trigger the beta build?', - bypass: ENV.fetch('RELEASE_TOOLKIT_SKIP_PUSH_CONFIRM', nil) + unless skip_user_confirmation || UI.confirm('Ready to push changes to remote and trigger the beta build?') + UI.message("Terminating as requested. Don't forget to run the remainder of this automation manually.") + next + end + + push_to_git_remote(tags: false) + + trigger_beta_build + + # Create an intermediate branch to avoid conflicts when integrating the changes + Fastlane::Helper::GitHelper.create_branch("new_beta/#{release_version}") + push_to_git_remote(tags: false) + + pr_url = create_release_management_pull_request( + base_branch: DEFAULT_BRANCH, + title: "Merge changes from #{build_code_current}" ) - push_to_git_remote(tags: false) - trigger_beta_build - else - UI.message('Aborting beta deployment. See you later.') + + message = <<~MESSAGE + Beta deployment was successful. Next, review and merge the [integration PR](#{pr_url}). + MESSAGE + buildkite_annotate(context: 'beta-completed', style: 'success', message:) if is_ci + end + + lane :create_editorial_branch do |options| + ensure_git_status_clean + + release_version = release_version_current + + unless Fastlane::Helper::GitHelper.checkout_and_pull(compute_release_branch_name(options:, version: release_version)) + UI.user_error!("Release branch for version #{release_version} doesn't exist.") end + + ensure_git_branch_is_release_branch # This check is mostly redundant + + git_pull + + Fastlane::Helper::GitHelper.create_branch(editorial_branch_name(version: release_version)) + + unless options[:skip_confirm] || UI.confirm('Ready to push editorial branch to remote?') + UI.message("Aborting as requested. Don't forget to push the branch to the remote manually.") + next + end + + # We need to also set upstream so the branch created in our local tracks the remote counterpart. + # Otherwise, when the next automation step will run and try to push changes made on that branch, it will fail. + push_to_git_remote(tags: false, set_upstream: true) end # Sets the stage to start working on a hotfix @@ -118,11 +281,59 @@ # desc 'Creates a new hotfix branch for the given `version:x.y.z`. The branch will be cut from the `x.y` tag.' lane :new_hotfix_release do |options| - prev_ver = ios_hotfix_prechecks(options) - ios_bump_version_hotfix( - previous_version: prev_ver, - version: options[:version] + # Verify that there's nothing in progress in the working copy + ensure_git_status_clean + + new_version = options[:version] || UI.input('Version number for the new hotfix?') + build_code_hotfix = build_code_hotfix(release_version: new_version) + build_code_hotfix_internal = build_code_hotfix_internal(release_version: new_version) + + # Parse the provided version into an AppVersion object + parsed_version = VERSION_FORMATTER.parse(new_version) + previous_version = VERSION_FORMATTER.release_version(VERSION_CALCULATOR.previous_patch_version(version: parsed_version)) + + # Check versions + message = <<~MESSAGE + New Hotfix: + + • Current release version and build code: #{release_version_current} (#{build_code_current}). + • New release version and build code: #{new_version} (#{build_code_hotfix}). + + • Current internal release version and build code: #{release_version_current_internal} (#{build_code_current_internal}). + • New internal release version and build code: #{new_version} (#{build_code_hotfix_internal}). + + Branching from tag: #{previous_version} + MESSAGE + + UI.important(message) + UI.user_error!('Aborted by user request') unless options[:skip_confirm] || UI.confirm('Do you want to continue?') + + # Check tags + UI.user_error!("Version #{new_version} already exists! Abort!") if git_tag_exists(tag: new_version) + UI.user_error!("Version #{previous_version} is not tagged! A hotfix branch cannot be created.") unless git_tag_exists(tag: previous_version) + + # Create the hotfix branch + UI.message 'Creating hotfix branch...' + Fastlane::Helper::GitHelper.create_branch(compute_release_branch_name(options:, version: new_version), from: previous_version) + UI.success "Done! New hotfix branch is: #{git_branch}" + + # Bump the hotfix version and build code and write it to the `xcconfig` file + UI.message 'Bumping hotfix version and build code...' + PUBLIC_VERSION_FILE.write( + version_short: new_version, + version_long: build_code_hotfix + ) + UI.success "Done! New Release Version: #{release_version_current}. New Build Code: #{build_code_current}" + + # Bump the internal hotfix version and build code and write it to the `xcconfig` file + UI.message 'Bumping internal hotfix version and build code...' + INTERNAL_VERSION_FILE.write( + version_short: new_version, + version_long: build_code_hotfix_internal ) + UI.success "Done! New Internal Release Version: #{release_version_current_internal}. New Internal Build Code: #{build_code_current_internal}" + + commit_version_and_build_files end # Finalizes a hotfix, by triggering a release build on CI @@ -131,18 +342,18 @@ # desc 'Performs the final checks and triggers a release build for the hotfix in the current branch' lane :finalize_hotfix_release do |options| - ios_finalize_prechecks(options) + ensure_git_branch_is_release_branch + + # Verify that there's nothing in progress in the working copy + ensure_git_status_clean + + # Pull the latest hotfix release branch changes git_pull - if prompt_for_confirmation( - message: 'Ready to push changes to remote and trigger the release build?', - bypass: ENV.fetch('RELEASE_TOOLKIT_SKIP_PUSH_CONFIRM', nil) - ) - push_to_git_remote(tags: false) - trigger_release_build - else - UI.message('Aborting hotfix finalization. See you later.') - end + UI.important("Triggering hotfix build for version: #{release_version_current}") + UI.user_error!('Aborted by user request') unless options[:skip_confirm] || UI.confirm('Do you want to continue?') + + trigger_release_build(branch_to_build: "release/#{release_version_current}") end # Finalizes a release at the end of a sprint to submit to the App Store @@ -158,31 +369,49 @@ lane :finalize_release do |options| UI.user_error!('To finalize a hotfix, please use the finalize_hotfix_release lane instead') if ios_current_branch_is_hotfix - ios_finalize_prechecks(options) + ensure_git_branch_is_release_branch + + # Verify that there's nothing in progress in the working copy + ensure_git_status_clean + + skip_user_confirmation = options[:skip_confirm] + + UI.important("Finalizing release: #{release_version_current}") + UI.user_error!('Aborted by user request') unless skip_user_confirmation || UI.confirm('Do you want to continue?') + git_pull - check_all_translations(interactive: true) + check_all_translations(interactive: skip_user_confirmation == false) download_localized_strings_and_metadata(options) - lint_localizations - ios_bump_version_beta + lint_localizations(allow_retry: skip_user_confirmation == false) + + bump_build_codes + + unless skip_user_confirmation || UI.confirm('Ready to push changes to remote and trigger the release build?') + UI.message("Terminating as requested. Don't forget to run the remainder of this automation manually.") + next + end + + push_to_git_remote(tags: false) - # Wrap up - version = get_app_version - removebranchprotection(repository: GITHUB_REPO, branch: release_branch_name) + version = release_version_current + remove_branch_protection(repository: GITHUB_REPO, branch: release_branch_name) setfrozentag(repository: GITHUB_REPO, milestone: version, freeze: false) create_new_milestone(repository: GITHUB_REPO) close_milestone(repository: GITHUB_REPO, milestone: version) - if prompt_for_confirmation( - message: 'Ready to push changes to remote and trigger the release build?', - bypass: ENV.fetch('RELEASE_TOOLKIT_SKIP_PUSH_CONFIRM', nil) + trigger_release_build + + pr_url = create_release_management_pull_request( + base_branch: DEFAULT_BRANCH, + title: "Merge #{version} release finalization" ) - push_to_git_remote(tags: false) - trigger_release_build - else - UI.message('Aborting release finalization. See you later.') - end + + message = <<~MESSAGE + Release successfully finalized. Next, review and merge the [integration PR](#{pr_url}). + MESSAGE + buildkite_annotate(context: 'finalization-completed', style: 'success', message:) if is_ci end # Triggers a beta build on CI @@ -215,8 +444,8 @@ # def trigger_buildkite_release_build(branch:, beta:) buildkite_trigger_build( - buildkite_organization: 'automattic', - buildkite_pipeline: 'wordpress-ios', + buildkite_organization: BUILDKITE_ORGANIZATION, + buildkite_pipeline: BUILDKITE_PIPELINE, branch:, environment: { BETA_RELEASE: beta }, pipeline_file: 'release-builds.yml', @@ -293,18 +522,43 @@ def prompt_for_confirmation(message:, bypass:) UI.confirm(message) end -def compute_release_branch_name(options:) - branch_option = :branch - branch_name = options[branch_option] +def bump_build_codes + bump_production_build_code + bump_internal_build_code + commit_version_and_build_files +end - if branch_name.nil? - branch_name = release_branch_name - UI.message("No branch given via option '#{branch_option}'. Defaulting to #{branch_name}.") - end +def bump_production_build_code + UI.message 'Bumping build code...' + PUBLIC_VERSION_FILE.write(version_long: build_code_next) + UI.success "Done. New Build Code: #{build_code_current}" +end - branch_name +def bump_internal_build_code + UI.message 'Bumping internal build code...' + INTERNAL_VERSION_FILE.write(version_long: build_code_next_internal) + UI.success "Done. New Internal Build Code: #{build_code_current_internal}" end -def release_branch_name - "release/#{get_app_version}" +def commit_version_and_build_files + git_commit( + path: [PUBLIC_CONFIG_FILE, INTERNAL_CONFIG_FILE], + message: 'Bump version number', + allow_nothing_to_commit: false + ) +end + +def create_release_management_pull_request(base_branch:, title:) + token = ENV.fetch('GITHUB_TOKEN', nil) + + UI.user_error!('Please export a GitHub API token in the environment as GITHUB_TOKEN') if token.nil? + + create_pull_request( + api_token: token, + repo: 'wordpress-mobile/WordPress-iOS', + title:, + head: Fastlane::Helper::GitHelper.current_git_branch, + base: base_branch, + labels: 'Releases' + ) end diff --git a/fastlane/lanes/release_management_in_ci.rb b/fastlane/lanes/release_management_in_ci.rb new file mode 100644 index 000000000000..af94dd97b9d9 --- /dev/null +++ b/fastlane/lanes/release_management_in_ci.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +PIPELINES_ROOT = 'release-pipelines' + +platform :ios do + lane :trigger_code_freeze_in_ci do + buildkite_trigger_build( + buildkite_organization: BUILDKITE_ORGANIZATION, + buildkite_pipeline: BUILDKITE_PIPELINE, + branch: DEFAULT_BRANCH, + pipeline_file: File.join(PIPELINES_ROOT, 'code-freeze.yml'), + message: 'Code Freeze' + ) + end + + lane :trigger_complete_code_freeze_in_ci do |options| + release_version_key = :release_version + release_version = options[release_version_key] + + UI.user_error!("You must specify a release version by calling this lane with a #{release_version_key} parameter") unless release_version + + buildkite_trigger_build( + buildkite_organization: BUILDKITE_ORGANIZATION, + buildkite_pipeline: BUILDKITE_PIPELINE, + branch: compute_release_branch_name(options:, version: release_version), + pipeline_file: File.join(PIPELINES_ROOT, 'complete-code-freeze.yml'), + message: "Complete Code Freeze for #{release_version}", + environment: { RELEASE_VERSION: release_version } + ) + end + + lane :trigger_new_beta_release_in_ci do |options| + release_version_key = :release_version + release_version = options[release_version_key] + + UI.user_error!("You must specify a release version by calling this lane with a #{release_version_key} parameter") unless release_version + + buildkite_trigger_build( + buildkite_organization: BUILDKITE_ORGANIZATION, + buildkite_pipeline: BUILDKITE_PIPELINE, + branch: compute_release_branch_name(options:, version: release_version), + pipeline_file: File.join(PIPELINES_ROOT, 'new-beta-release.yml'), + message: "New Beta Release for #{release_version}", + environment: { RELEASE_VERSION: release_version } + ) + end + + lane :trigger_update_app_store_strings_in_ci do |options| + release_version_key = :release_version + release_version = options[release_version_key] + + UI.user_error!("You must specify a release version by calling this lane with a #{release_version_key} parameter") unless release_version + + buildkite_trigger_build( + buildkite_organization: BUILDKITE_ORGANIZATION, + buildkite_pipeline: BUILDKITE_PIPELINE, + branch: editorial_branch_name(version: release_version), + pipeline_file: File.join(PIPELINES_ROOT, 'update-app-store-strings.yml'), + message: "Update Editorialized Release Notes and App Store Metadata for #{release_version}" + ) + end + + lane :trigger_finalize_release_in_ci do |options| + release_version_key = :release_version + release_version = options[release_version_key] + + UI.user_error!("You must specify a release version by calling this lane with a #{release_version_key} parameter") unless release_version + + buildkite_trigger_build( + buildkite_organization: BUILDKITE_ORGANIZATION, + buildkite_pipeline: BUILDKITE_PIPELINE, + branch: compute_release_branch_name(options:, version: release_version), + pipeline_file: File.join(PIPELINES_ROOT, 'finalize-release.yml'), + message: "Finalize Release #{release_version}", + environment: { RELEASE_VERSION: release_version } + ) + end +end diff --git a/fastlane/metadata/ar-SA/release_notes.txt b/fastlane/metadata/ar-SA/release_notes.txt new file mode 100644 index 000000000000..0de2faa4c735 --- /dev/null +++ b/fastlane/metadata/ar-SA/release_notes.txt @@ -0,0 +1,5 @@ +قمنا بتحديث المحرر التقليدي من خلال أدوات انتقاء الوسائط الجديد في الصور ووسائط الموقع. لا داعي للقلق، لا يزال بإمكانك رفع الوسائط والفيديوهات والمزيد إلى موقعك. + +عند الحديث عن أنواع الوسائط، أصبح بإمكانك الآن إضافة عوامل تصفية الوسائط إلى شاشة وسائط الموقع. إذا كنت تستخدم iPhone، فستلاحظ وضع نسبة الارتفاع إلى العرض الجديد كذلك. يتوافر كلا الخيارين عند النقر على قائمة العنوان. + +أخيرًا، أصلحنا النافذة المنبثقة للامتثال المعطّلة التي تظهر في أثناء التحقق من الإحصاءات خلال عملية الإعداد. رائع. diff --git a/fastlane/metadata/de-DE/privacy_url.txt b/fastlane/metadata/de-DE/privacy_url.txt deleted file mode 100644 index c65496c078ad..000000000000 --- a/fastlane/metadata/de-DE/privacy_url.txt +++ /dev/null @@ -1 +0,0 @@ -https://automattic.com/privacy/ diff --git a/fastlane/metadata/de-DE/release_notes.txt b/fastlane/metadata/de-DE/release_notes.txt new file mode 100644 index 000000000000..e69829d872ee --- /dev/null +++ b/fastlane/metadata/de-DE/release_notes.txt @@ -0,0 +1,5 @@ +Der klassische Editor wurde mit neuen Medienauswahlen für Fotos und Website-Medien ersetzt. Keine Sorge: Du kannst weiterhin Bilder, Videos und mehr auf deine Website hochladen. + +Apropos Medientypen: Ab sofort kannst du Medienfilter zum Bildschirm für Website-Medien hinzufügen. iPhone-Benutzern wird auch der neue Modus für das Bildformat auffallen. Beide Optionen sind verfügbar, wenn du auf das Titelmenü tippst. + +Außerdem haben wir das fehlerhafte Compliance-Pop-up korrigiert, das angezeigt wurde, wenn du Statistiken während des Onboarding-Prozesses überprüft hast. Das ist doch super. diff --git a/fastlane/metadata/da/privacy_url.txt b/fastlane/metadata/default/privacy_url.txt similarity index 100% rename from fastlane/metadata/da/privacy_url.txt rename to fastlane/metadata/default/privacy_url.txt diff --git a/fastlane/metadata/default/release_notes.txt b/fastlane/metadata/default/release_notes.txt index 201bdecede95..50f66336896a 100644 --- a/fastlane/metadata/default/release_notes.txt +++ b/fastlane/metadata/default/release_notes.txt @@ -1,6 +1,5 @@ -* [*] Fix a crash when the blog's blogging prompt settings contain invalid JSON [#21677] -* [*] Fixes an issue where users would land on the Reader after signup while it should not be accessible. [#21751] -* [*] Block Editor: Split formatted text on triple Enter [https://github.com/WordPress/gutenberg/pull/53354] -* [*] Block Editor: Quote block: Ensure border is visible with block-based themes in dark [https://github.com/WordPress/gutenberg/pull/54964] -* [*] (Internal) Remove .nativePhotoPicker feature flag and the disabled code [#21681](https://github.com/wordpress-mobile/WordPress-iOS/pull/21681) -* [*] Fixes an issue where users attempting to create a .com site in the post-sign-up flow are presented with two consecutive overlays. [#21752] +We updated the classic editor with new media pickers for Photos and Site Media. Don’t worry, you can still upload images, videos, and more to your site. + +Speaking of media types—you can now add media filters to the Site Media screen. If you’re using an iPhone, you’ll notice the new aspect ratio mode, too. Both options are available when you tap the title menu. + +Finally, we fixed the broken compliance pop-up that appears while you’re checking stats during the onboarding process. Sweet. diff --git a/fastlane/metadata/en-AU/description.txt b/fastlane/metadata/en-AU/description.txt new file mode 100644 index 000000000000..60819d65fc8e --- /dev/null +++ b/fastlane/metadata/en-AU/description.txt @@ -0,0 +1,11 @@ +Manage or create your WordPress blog or website right from your iOS device: create and edit posts and pages, upload your favourite photos and videos, view stats and reply to comments. + +With WordPress for iOS, you have the power to publish in the palm of your hand. Draft a spontaneous haiku from the couch. Snap and post a photo on your lunch break. Respond to your latest comments, or check your stats to see what new countries today’s visitors are coming from. + +WordPress for iOS is an Open Source project, which means you too can contribute to its development. Learn more at https://apps.wordpress.com/contribute/. + +WordPress for iOS supports WordPress.com and self-hosted WordPress.org sites running WordPress 4.0 or higher. + +Need help with the app? Visit the forums at https://wordpress.org/support/forum/mobile/ or tweet us @WordPressiOS. + +View the Privacy Notice for California Users at https://automattic.com/privacy/#california-consumer-privacy-act-ccpa. diff --git a/fastlane/metadata/en-AU/privacy_url.txt b/fastlane/metadata/en-AU/privacy_url.txt deleted file mode 100644 index c65496c078ad..000000000000 --- a/fastlane/metadata/en-AU/privacy_url.txt +++ /dev/null @@ -1 +0,0 @@ -https://automattic.com/privacy/ diff --git a/fastlane/metadata/en-AU/release_notes.txt b/fastlane/metadata/en-AU/release_notes.txt new file mode 100644 index 000000000000..50f66336896a --- /dev/null +++ b/fastlane/metadata/en-AU/release_notes.txt @@ -0,0 +1,5 @@ +We updated the classic editor with new media pickers for Photos and Site Media. Don’t worry, you can still upload images, videos, and more to your site. + +Speaking of media types—you can now add media filters to the Site Media screen. If you’re using an iPhone, you’ll notice the new aspect ratio mode, too. Both options are available when you tap the title menu. + +Finally, we fixed the broken compliance pop-up that appears while you’re checking stats during the onboarding process. Sweet. diff --git a/fastlane/metadata/en-CA/description.txt b/fastlane/metadata/en-CA/description.txt new file mode 100644 index 000000000000..2e2576d7a613 --- /dev/null +++ b/fastlane/metadata/en-CA/description.txt @@ -0,0 +1,11 @@ +Manage or create your WordPress blog or website right from your iOS device: create and edit posts and pages, upload your favorite photos and videos, view stats and reply to comments. + +With WordPress for iOS, you have the power to publish in the palm of your hand. Draft a spontaneous haiku from the couch. Snap and post a photo on your lunch break. Respond to your latest comments, or check your stats to see what new countries today’s visitors are coming from. + +WordPress for iOS is an Open Source project, which means you too can contribute to its development. Learn more at https://apps.wordpress.com/contribute/. + +WordPress for iOS supports WordPress.com and self-hosted WordPress.org sites running WordPress 4.0 or higher. + +Need help with the app? Visit the forums at https://wordpress.org/support/forum/mobile/ or tweet us @WordPressiOS. + +View the Privacy Notice for California Users at https://automattic.com/privacy/#california-consumer-privacy-act-ccpa. diff --git a/fastlane/metadata/en-CA/privacy_url.txt b/fastlane/metadata/en-CA/privacy_url.txt deleted file mode 100644 index c65496c078ad..000000000000 --- a/fastlane/metadata/en-CA/privacy_url.txt +++ /dev/null @@ -1 +0,0 @@ -https://automattic.com/privacy/ diff --git a/fastlane/metadata/en-GB/privacy_url.txt b/fastlane/metadata/en-GB/privacy_url.txt deleted file mode 100644 index c65496c078ad..000000000000 --- a/fastlane/metadata/en-GB/privacy_url.txt +++ /dev/null @@ -1 +0,0 @@ -https://automattic.com/privacy/ diff --git a/fastlane/metadata/en-GB/release_notes.txt b/fastlane/metadata/en-GB/release_notes.txt new file mode 100644 index 000000000000..fcebbfb1a1a7 --- /dev/null +++ b/fastlane/metadata/en-GB/release_notes.txt @@ -0,0 +1,5 @@ +We updated the Classic Editor with new media pickers for Photos and Site Media. Don’t worry, you can still upload images, videos, and more to your site. + +Speaking of media types – you can now add media filters to the Site Media screen. If you’re using an iPhone, you’ll notice the new aspect ratio mode, too. Both options are available when you tap the title menu. + +Finally, we fixed the broken compliance pop-up that appears while you’re checking stats during the onboarding process. Sweet. diff --git a/fastlane/metadata/en-US/privacy_url.txt b/fastlane/metadata/en-US/privacy_url.txt deleted file mode 100644 index c65496c078ad..000000000000 --- a/fastlane/metadata/en-US/privacy_url.txt +++ /dev/null @@ -1 +0,0 @@ -https://automattic.com/privacy/ diff --git a/fastlane/metadata/es-ES/privacy_url.txt b/fastlane/metadata/es-ES/privacy_url.txt deleted file mode 100644 index c65496c078ad..000000000000 --- a/fastlane/metadata/es-ES/privacy_url.txt +++ /dev/null @@ -1 +0,0 @@ -https://automattic.com/privacy/ diff --git a/fastlane/metadata/es-ES/release_notes.txt b/fastlane/metadata/es-ES/release_notes.txt new file mode 100644 index 000000000000..1cf99dfd4cb9 --- /dev/null +++ b/fastlane/metadata/es-ES/release_notes.txt @@ -0,0 +1,5 @@ +Hemos actualizado el editor clásico con nuevos selectores de medios para fotos y medios del sitio. No te preocupes, puedes seguir subiendo imágenes, vídeos y mucho más a tu sitio. + +Hablando de tipos de medios—ahora puedes añadir filtros de medios a la pantalla de medios del sitio. Si utilizas un iPhone, también notarás el nuevo modo de relación de aspecto. Ambas opciones están disponibles cuando tocas el menú del título. + +Por último, hemos arreglado la ventana emergente de cumplimiento que aparece mientras compruebas las estadísticas durante el proceso de puesta en marcha. ¡Genial! diff --git a/fastlane/metadata/es-MX/privacy_url.txt b/fastlane/metadata/es-MX/privacy_url.txt deleted file mode 100644 index c65496c078ad..000000000000 --- a/fastlane/metadata/es-MX/privacy_url.txt +++ /dev/null @@ -1 +0,0 @@ -https://automattic.com/privacy/ diff --git a/fastlane/metadata/fr-FR/privacy_url.txt b/fastlane/metadata/fr-FR/privacy_url.txt deleted file mode 100644 index c65496c078ad..000000000000 --- a/fastlane/metadata/fr-FR/privacy_url.txt +++ /dev/null @@ -1 +0,0 @@ -https://automattic.com/privacy/ diff --git a/fastlane/metadata/fr-FR/release_notes.txt b/fastlane/metadata/fr-FR/release_notes.txt new file mode 100644 index 000000000000..1ec1eeeefecb --- /dev/null +++ b/fastlane/metadata/fr-FR/release_notes.txt @@ -0,0 +1,5 @@ +Nous avons mis à jour l’éditeur classique avec de nouveaux sélecteurs de médias pour les photos et les médias du site. Pas d’inquiétude, vous pouvez toujours mettre en ligne des images, des vidéos, et plus encore sur votre site. + +En parlant de types de médias : vous pouvez désormais ajouter des filtres médias à l’écran Médias du site. Si vous utilisez un iPhone, vous remarquerez également le nouveau mode Proportions. Les deux options sont disponibles lorsque vous appuyez sur le menu Titre. + +Enfin, nous avons réparé la pop-up qui apparaît lorsque vous consultez les statistiques à l’occasion du processus de configuration. Pas mal. diff --git a/fastlane/metadata/he/release_notes.txt b/fastlane/metadata/he/release_notes.txt new file mode 100644 index 000000000000..234f1b5315d1 --- /dev/null +++ b/fastlane/metadata/he/release_notes.txt @@ -0,0 +1,5 @@ +עדכנו את העורך הקלאסי בבוררי מדיה חדשים לתמונות מצולמות ולמדיה באתר. לא לדאוג – אין בעיה להמשיך להעלות לאתר תמונות, סרטונים ועוד. + +ואם כבר מדברים על סוגי מדיה – מעכשיו אפשר להוסיף מסנני מדיה למסך 'מדיה באתר'. משתמשי iPhone יבחינו גם במצב יחס תצוגה חדש. שתי האפשרויות זמינות בהקשה על תפריט שם האתר. + +ולבסוף, תוקנו החלונות הקופצים השבורים של התאמה לדרישות, שצצו תוך כדי בדיקת נתונים סטטיסטיים בתהליך ההצטרפות. נחמד. diff --git a/fastlane/metadata/id/privacy_url.txt b/fastlane/metadata/id/privacy_url.txt deleted file mode 100644 index c65496c078ad..000000000000 --- a/fastlane/metadata/id/privacy_url.txt +++ /dev/null @@ -1 +0,0 @@ -https://automattic.com/privacy/ diff --git a/fastlane/metadata/id/release_notes.txt b/fastlane/metadata/id/release_notes.txt new file mode 100644 index 000000000000..a37afdb01a63 --- /dev/null +++ b/fastlane/metadata/id/release_notes.txt @@ -0,0 +1,5 @@ +Kami memperbarui editor klasik dengan pemilih media baru untuk Foto dan Media Situs. Namun, Anda masih dapat mengunggah gambar, video, dan lain-lain ke situs Anda. + +Terkait dengan tipe media, kini Anda dapat menambahkan filter media ke layar Media Situs. Jika menggunakan iPhone, Anda pasti juga akan melihat mode rasio aspek yang baru. Kedua pilihan tersedia jika Anda mengetuk menu judul. + +Terakhir, kami memperbaiki kerusakan pop-up kepatuhan yang muncul ketika Anda memeriksa statistik selama proses penyiapan. Mantap. diff --git a/fastlane/metadata/it/description.txt b/fastlane/metadata/it/description.txt index 755616a12cf9..234e54cc38b4 100644 --- a/fastlane/metadata/it/description.txt +++ b/fastlane/metadata/it/description.txt @@ -1,11 +1,11 @@ -Gestisci o crea il tuo blog o il tuo sito WordPress direttamente dal tuo dispositivo iOS: crea e modifica articoli e pagine, carica foto e video preferiti, visualizza le statistiche e rispondi ai commenti. +Gestisci o crea il tuo blog o il tuo sito WordPress direttamente dal tuo dispositivo iOS: crea e modifica articoli e pagine, carica le foto e i video che preferisci, visualizza le statistiche e rispondi ai commenti. -Con WordPress per iOS, hai il potere di pubblicare sul palmo della tua mano. Crea una bozza di un haiku spontaneo dal divano. Scatta e pubblica una foto della tua pausa pranzo. Rispondi agli ultimi commenti o controlla le statistiche per scoprire da quale Paese provengono i nuovi visitatori di oggi. +Con WordPress per iOS, hai il potere di pubblicare a portata di mano. Scrivi la bozza di un haiku dal divano. Scatta e pubblica una foto in pausa pranzo. Rispondi agli ultimi commenti o controlla le statistiche per scoprire da quale Paese provengono i tuoi nuovi visitatori. WordPress per iOS è un progetto Open Source, il che significa che anche tu puoi contribuire al suo sviluppo. Scopri di più all'indirizzo https://apps.wordpress.com/contribute/. -WordPress per iOS supporta WordPress.com e i siti WordPress.org ospitati personalmente che eseguono WordPress 4.0 o versione successiva. +WordPress per iOS supporta WordPress.com e i siti WordPress.org ospitati personalmente che eseguono WordPress 4.0 o una versione successiva. Hai bisogno di aiuto con l'app? Visita i forum all'indirizzo https://wordpress.org/support/forum/mobile/ o mandaci un tweet a @WordPressiOS. -Consulta la nota sulla privacy per utenti in California su https://automattic.com/privacy/#california-consumer-privacy-act-ccpa. +Consulta la nota sulla privacy per gli utenti in California su https://automattic.com/privacy/#california-consumer-privacy-act-ccpa. diff --git a/fastlane/metadata/it/keywords.txt b/fastlane/metadata/it/keywords.txt index 0b09861ed2b9..77aeb3ed6261 100644 --- a/fastlane/metadata/it/keywords.txt +++ b/fastlane/metadata/it/keywords.txt @@ -1 +1 @@ -blogger,scrittura,blogging,web,realizzatore,online,negozio,attività,creare,scrivere,blog +blogger,scrittura,blogging,web,costruttore,online,negozio,attività,creare,scrivere,blog diff --git a/fastlane/metadata/it/name.txt b/fastlane/metadata/it/name.txt deleted file mode 100644 index 87bafecc5499..000000000000 --- a/fastlane/metadata/it/name.txt +++ /dev/null @@ -1 +0,0 @@ -WordPress – Costruzione siti diff --git a/fastlane/metadata/it/privacy_url.txt b/fastlane/metadata/it/privacy_url.txt deleted file mode 100644 index c65496c078ad..000000000000 --- a/fastlane/metadata/it/privacy_url.txt +++ /dev/null @@ -1 +0,0 @@ -https://automattic.com/privacy/ diff --git a/fastlane/metadata/it/release_notes.txt b/fastlane/metadata/it/release_notes.txt new file mode 100644 index 000000000000..4cdd5e40584e --- /dev/null +++ b/fastlane/metadata/it/release_notes.txt @@ -0,0 +1,5 @@ +Abbiamo aggiornato l'editor classico con nuovi contenuti multimediali per Foto e Media sito. Non preoccuparti, puoi ancora caricare immagini, video e altro sul tuo sito. + +A proposito di tipi di media, ora puoi aggiungere filtri per i contenuti multimediali nella schermata Media sito. Se usi un iPhone, noterai anche la nuova modalità di rapporto d'aspetto. Entrambe le opzioni sono disponibili quando clicchi sul titolo del menu. + +Infine, abbiamo sistemato il pop-up di conformità non funzionante che appare mentre si controllano le statistiche durante il processo di onboarding. Carino. diff --git a/fastlane/metadata/ja/privacy_url.txt b/fastlane/metadata/ja/privacy_url.txt deleted file mode 100644 index c65496c078ad..000000000000 --- a/fastlane/metadata/ja/privacy_url.txt +++ /dev/null @@ -1 +0,0 @@ -https://automattic.com/privacy/ diff --git a/fastlane/metadata/ko/privacy_url.txt b/fastlane/metadata/ko/privacy_url.txt deleted file mode 100644 index c65496c078ad..000000000000 --- a/fastlane/metadata/ko/privacy_url.txt +++ /dev/null @@ -1 +0,0 @@ -https://automattic.com/privacy/ diff --git a/fastlane/metadata/ko/release_notes.txt b/fastlane/metadata/ko/release_notes.txt new file mode 100644 index 000000000000..d70ab53a6fc7 --- /dev/null +++ b/fastlane/metadata/ko/release_notes.txt @@ -0,0 +1,5 @@ +사진 및 사이트 미디어에 대한 새로운 미디어 선택기로 구 버전 편집기를 업데이트했습니다. 걱정하지 마세요. 여전히 사이트에 이미지, 비디오 등을 업로드할 수 있습니다. + +미디어 유형의 경우 이제 사이트 미디어 화면에 미디어 필터를 추가할 수 있습니다. iPhone을 사용하는 경우 새로운 화면 비율 모드도 표시됩니다. 두 가지 옵션 모두 제목 메뉴를 눌러서 이용할 수 있습니다. + +마지막으로, 온보딩 프로세스 도중에 통계를 확인하는 동안 나타나는 손상된 규정 준수 팝업을 해결했습니다. 상쾌합니다. diff --git a/fastlane/metadata/nl-NL/privacy_url.txt b/fastlane/metadata/nl-NL/privacy_url.txt deleted file mode 100644 index c65496c078ad..000000000000 --- a/fastlane/metadata/nl-NL/privacy_url.txt +++ /dev/null @@ -1 +0,0 @@ -https://automattic.com/privacy/ diff --git a/fastlane/metadata/nl-NL/release_notes.txt b/fastlane/metadata/nl-NL/release_notes.txt new file mode 100644 index 000000000000..95f6dd24cb65 --- /dev/null +++ b/fastlane/metadata/nl-NL/release_notes.txt @@ -0,0 +1,5 @@ +We hebben de klassieke editor bijgewerkt met nieuwe mediakiezers voor foto's en sitemedia. Geen zorgen, je kan nog steeds afbeeldingen, video's en meer uploaden naar je site. + +Over mediatypen gesproken: je kan nu mediafilters toevoegen aan het scherm Sitemedia. Als je een iPhone gebruikt, zie je ook de nieuwe modus voor beeldverhouding. Beide opties zijn beschikbaar als je op het titelmenu tikt. + +Tot slot hebben we de defecte pop-up voor naleving gemaakt die verschijnt als je statistieken bekijkt tijdens het onboardingproces. Handig, toch? diff --git a/fastlane/metadata/no/privacy_url.txt b/fastlane/metadata/no/privacy_url.txt deleted file mode 100644 index c65496c078ad..000000000000 --- a/fastlane/metadata/no/privacy_url.txt +++ /dev/null @@ -1 +0,0 @@ -https://automattic.com/privacy/ diff --git a/fastlane/metadata/pt-BR/privacy_url.txt b/fastlane/metadata/pt-BR/privacy_url.txt deleted file mode 100644 index c65496c078ad..000000000000 --- a/fastlane/metadata/pt-BR/privacy_url.txt +++ /dev/null @@ -1 +0,0 @@ -https://automattic.com/privacy/ diff --git a/fastlane/metadata/pt-PT/privacy_url.txt b/fastlane/metadata/pt-PT/privacy_url.txt deleted file mode 100644 index c65496c078ad..000000000000 --- a/fastlane/metadata/pt-PT/privacy_url.txt +++ /dev/null @@ -1 +0,0 @@ -https://automattic.com/privacy/ diff --git a/fastlane/metadata/ru/privacy_url.txt b/fastlane/metadata/ru/privacy_url.txt deleted file mode 100644 index c65496c078ad..000000000000 --- a/fastlane/metadata/ru/privacy_url.txt +++ /dev/null @@ -1 +0,0 @@ -https://automattic.com/privacy/ diff --git a/fastlane/metadata/ru/release_notes.txt b/fastlane/metadata/ru/release_notes.txt new file mode 100644 index 000000000000..1cb58413a4ad --- /dev/null +++ b/fastlane/metadata/ru/release_notes.txt @@ -0,0 +1,5 @@ +Мы обновили классический редактор, добавив новые инструменты выбора медиафайлов в разделы «Фотографии» и «Медиафайлы сайта». Не беспокойтесь, вы по-прежнему можете загружать на свой сайт изображения, видео и всё остальное. + +Что касается типов медиафайлов, теперь можно добавлять их фильтры на экран «Медиафайлы сайта». Если вы пользуетесь iPhone, вы также заметите новый режим соотношения сторон. Доступ к обеим опциям открывается при нажатии меню заголовка. + +Ну и наконец, мы исправили сбой всплывающего окна с предупреждением о соответствии требованиям, которое появляется, когда вы проверяете статистику в процессе регистрации. Отлично. diff --git a/fastlane/metadata/sv/privacy_url.txt b/fastlane/metadata/sv/privacy_url.txt deleted file mode 100644 index c65496c078ad..000000000000 --- a/fastlane/metadata/sv/privacy_url.txt +++ /dev/null @@ -1 +0,0 @@ -https://automattic.com/privacy/ diff --git a/fastlane/metadata/sv/release_notes.txt b/fastlane/metadata/sv/release_notes.txt new file mode 100644 index 000000000000..8fe0309fd6e2 --- /dev/null +++ b/fastlane/metadata/sv/release_notes.txt @@ -0,0 +1,5 @@ +Vi har uppdaterat den klassiska redigeraren med nya mediaväljare för foton och webbplatsmedia. Oroa dig inte, du kan fortfarande ladda upp bilder, videoklipp och annat till din webbplats. + +På tal om olika typer av media – du kan nu lägga till mediafilter på skärmen Webbplatsmedia. Om du använder en iPhone kommer du även att märka det nya bildförhållandeläget. Båda alternativen är tillgängliga när du trycker på rubrikmenyn. + +Slutligen har vi åtgärdat det trasiga popup-fönstret rörande efterlevnad som visas när man kollar statistik under onboardingprocessen. Perfekt. diff --git a/fastlane/metadata/th/privacy_url.txt b/fastlane/metadata/th/privacy_url.txt deleted file mode 100644 index c65496c078ad..000000000000 --- a/fastlane/metadata/th/privacy_url.txt +++ /dev/null @@ -1 +0,0 @@ -https://automattic.com/privacy/ diff --git a/fastlane/metadata/tr/privacy_url.txt b/fastlane/metadata/tr/privacy_url.txt deleted file mode 100644 index c65496c078ad..000000000000 --- a/fastlane/metadata/tr/privacy_url.txt +++ /dev/null @@ -1 +0,0 @@ -https://automattic.com/privacy/ diff --git a/fastlane/metadata/tr/release_notes.txt b/fastlane/metadata/tr/release_notes.txt new file mode 100644 index 000000000000..2b3b1733658f --- /dev/null +++ b/fastlane/metadata/tr/release_notes.txt @@ -0,0 +1,5 @@ +Klasik düzenleyiciyi, Fotoğraflar ve Site ortamı için yeni medya seçicilerle güncelledik. Endişelenmeyin; yine de sitenize resim, video ve daha fazlasını yükleyebilirsiniz. + +Medya türlerinden bahsetmişken, artık Site ortamı ekranına medya filtreleri ekleyebilirsiniz. iPhone kullanıyorsanız yeni en-boy oranı modunu da fark edeceksiniz. Başlık menüsüne dokunduğunuzda her iki seçenek de kullanılabilir. + +Son olarak, katılım süreci sırasında istatistikleri kontrol ederken görünen bozuk uyumluluk açılır penceresini düzelttik. Tatlı. diff --git a/fastlane/metadata/zh-Hans/privacy_url.txt b/fastlane/metadata/zh-Hans/privacy_url.txt deleted file mode 100644 index c65496c078ad..000000000000 --- a/fastlane/metadata/zh-Hans/privacy_url.txt +++ /dev/null @@ -1 +0,0 @@ -https://automattic.com/privacy/ diff --git a/fastlane/metadata/zh-Hans/release_notes.txt b/fastlane/metadata/zh-Hans/release_notes.txt new file mode 100644 index 000000000000..cdcfcd2ae1b1 --- /dev/null +++ b/fastlane/metadata/zh-Hans/release_notes.txt @@ -0,0 +1,5 @@ +我们更新了经典编辑器,添加了用于照片和站点媒体的新媒体选择器。 别担心,您仍然可以将图片、视频等内容上传至您的站点。 + +至于媒体类型,您现在可以在“站点媒体”屏幕上添加媒体过滤器。 如果您使用的是 iPhone,您还会注意到新的宽高比模式。 轻点标题菜单,即可在两个选项中进行切换。 + +最后,我们修复了在入门流程中查看统计信息时出现的合规性弹窗不完整的问题。 很贴心。 diff --git a/fastlane/metadata/zh-Hant/privacy_url.txt b/fastlane/metadata/zh-Hant/privacy_url.txt deleted file mode 100644 index c65496c078ad..000000000000 --- a/fastlane/metadata/zh-Hant/privacy_url.txt +++ /dev/null @@ -1 +0,0 @@ -https://automattic.com/privacy/ diff --git a/fastlane/metadata/zh-Hant/release_notes.txt b/fastlane/metadata/zh-Hant/release_notes.txt new file mode 100644 index 000000000000..500df5b1ae76 --- /dev/null +++ b/fastlane/metadata/zh-Hant/release_notes.txt @@ -0,0 +1,5 @@ +我們更新了傳統編輯器,為照片和網站媒體加入全新的媒體選擇器。 別擔心,你仍可以上傳圖片、影片和更多內容到網站上。 + +說到媒體類型,你現在可以在「網站媒體」畫面新增媒體篩選條件。 若你使用 iPhone,也會注意到新加入的畫面比例模式。 點選標題選單時,會出現兩種選項。 + +最後,我們修復了在新手體驗流程中,當你在查看統計資料時,會出現的故障合規快顯視窗。 真是太棒了。